From 3bd3eb14c79c85991bba040f421810a90be39dbd Mon Sep 17 00:00:00 2001 From: Rickyanto Ang Date: Tue, 29 Mar 2022 08:42:03 -0700 Subject: [PATCH 001/108] UI Polish for Session viewer frame + buttons (#128188) * UI Polish for Session viewer frame + buttons * UI Update for Session Viewer + Toolbar * added Left Borders on Details panel + Fix Details panel button behaviour * Details Panel UI Polish + PR Comments * Find results interaction UI Polish, Def/Min/Max width for details panel, Timestamp bug fix * more conflict fixes * removed unused variable * fix for failed checks on buildkite #1 * Addressing PR comments * PR Comments + Search bar UI bug fix * pr comments Co-authored-by: Karl Godard --- .../detail_panel_accordion/styles.ts | 3 + .../detail_panel_description_list/styles.ts | 4 +- .../detail_panel_list_item/styles.ts | 9 +- .../public/components/process_tree/styles.ts | 2 + .../components/process_tree_node/index.tsx | 16 +- .../components/process_tree_node/styles.ts | 33 ++- .../process_tree_node/use_button_styles.ts | 20 +- .../public/components/session_view/index.tsx | 245 +++++++++--------- .../public/components/session_view/styles.ts | 19 +- .../session_view_detail_panel/index.tsx | 7 +- .../session_view_detail_panel/styles.ts | 25 ++ .../session_view_search_bar/index.tsx | 17 +- .../session_view_search_bar/styles.ts | 16 +- 13 files changed, 255 insertions(+), 161 deletions(-) create mode 100644 x-pack/plugins/session_view/public/components/session_view_detail_panel/styles.ts diff --git a/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts index c44e069c05c00..96eddb2b2bf98 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts +++ b/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts @@ -22,6 +22,9 @@ export const useStyles = () => { '&:last-child': { borderBottom: euiTheme.border.thin, }, + dl: { + paddingTop: '0px', + }, }; const accordionButton: CSSObject = { diff --git a/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts index d815cb2a48283..d1f3198a10c85 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts +++ b/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts @@ -14,19 +14,21 @@ export const useStyles = () => { const cached = useMemo(() => { const descriptionList: CSSObject = { - padding: euiTheme.size.s, + padding: `${euiTheme.size.base} ${euiTheme.size.s} `, }; const tabListTitle = { width: '40%', display: 'flex', alignItems: 'center', + marginTop: '0px', }; const tabListDescription = { width: '60%', display: 'flex', alignItems: 'center', + marginTop: '0px', }; return { diff --git a/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.ts index c370bd8adb6e2..22f5e6782288f 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.ts +++ b/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.ts @@ -20,11 +20,13 @@ export const useStyles = ({ display }: StylesDeps) => { const item: CSSObject = { display, alignItems: 'center', - padding: euiTheme.size.s, + padding: `0px ${euiTheme.size.s} `, width: '100%', - fontSize: 'inherit', fontWeight: 'inherit', - minHeight: '36px', + height: euiTheme.size.xl, + lineHeight: euiTheme.size.l, + letterSpacing: '0px', + textAlign: 'left', }; const copiableItem: CSSObject = { @@ -34,6 +36,7 @@ export const useStyles = ({ display }: StylesDeps) => { '&:hover': { background: transparentize(euiTheme.colors.primary, 0.1), }, + height: '100%', }; return { diff --git a/x-pack/plugins/session_view/public/components/process_tree/styles.ts b/x-pack/plugins/session_view/public/components/process_tree/styles.ts index 207cc55e49582..ed868b7203ccd 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/styles.ts @@ -22,6 +22,8 @@ export const useStyles = () => { overflow: 'auto', height: '100%', backgroundColor: colors.lightestShade, + paddingTop: size.base, + paddingLeft: size.s, }; const selectionArea: CSSObject = { diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index d2e5b1b899553..9d5a3b1c953cf 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -86,7 +86,8 @@ export function ProcessTreeNode({ ), [hasAlerts, alerts, jumpToAlertID] ); - const styles = useStyles({ depth, hasAlerts, hasInvestigatedAlert }); + const isSelected = selectedProcessId === process.id; + const styles = useStyles({ depth, hasAlerts, hasInvestigatedAlert, isSelected }); const buttonStyles = useButtonStyles({}); const nodeRef = useVisible({ @@ -249,15 +250,12 @@ export function ProcessTreeNode({ [exit_code: {exitCode}] )} - {timeStampOn && ( - - {timeStampsNormal} - - )} + {timeStampOn && ( + + {timeStampsNormal} + + )} )} diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts index 55afe5c28071a..c3122294e44fd 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts @@ -13,9 +13,10 @@ interface StylesDeps { depth: number; hasAlerts: boolean; hasInvestigatedAlert: boolean; + isSelected: boolean; } -export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps) => { +export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert, isSelected }: StylesDeps) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { @@ -25,14 +26,11 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps const darkText: CSSObject = { color: colors.text, + fontFamily: font.familyCode, + paddingLeft: size.xxs, + paddingRight: size.xs, }; - const searchHighlight = ` - background-color: ${colors.highlight}; - color: ${colors.fullShade}; - border-radius: ${border.radius.medium}; - `; - const children: CSSObject = { position: 'relative', color: colors.ghost, @@ -48,6 +46,7 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps let bgColor = 'none'; const hoverColor = transparentize(colors.primary, 0.04); let borderColor = 'transparent'; + let searchResColor = transparentize(colors.warning, 0.32); if (hasAlerts) { borderColor = colors.danger; @@ -57,10 +56,14 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps bgColor = transparentize(colors.danger, 0.04); } - return { bgColor, borderColor, hoverColor }; + if (isSelected) { + searchResColor = colors.warning; + } + + return { bgColor, borderColor, hoverColor, searchResColor }; }; - const { bgColor, borderColor, hoverColor } = getHighlightColors(); + const { bgColor, borderColor, hoverColor, searchResColor } = getHighlightColors(); const processNode: CSSObject = { display: 'block', @@ -84,6 +87,12 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps }, }; + const searchHighlight = ` + color: ${colors.fullShade}; + border-radius: '0px'; + background-color: ${searchResColor}; + `; + const wrapper: CSSObject = { paddingLeft: size.s, position: 'relative', @@ -96,6 +105,10 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps const workingDir: CSSObject = { color: colors.successText, + fontFamily: font.familyCode, + fontWeight: font.weight.medium, + paddingLeft: size.s, + paddingRight: size.xxs, }; const timeStamp: CSSObject = { @@ -124,7 +137,7 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps timeStamp, alertDetails, }; - }, [depth, euiTheme, hasAlerts, hasInvestigatedAlert]); + }, [depth, euiTheme, hasAlerts, hasInvestigatedAlert, isSelected]); return cached; }; diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts index 529a0ce5819f9..4c713b42a2d7b 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts @@ -18,7 +18,7 @@ export const useButtonStyles = ({ isExpanded }: ButtonStylesDeps) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { - const { colors, border, size } = euiTheme; + const { colors, border, size, font } = euiTheme; const button: CSSObject = { background: transparentize(theme.euiColorVis6, 0.04), @@ -26,14 +26,21 @@ export const useButtonStyles = ({ isExpanded }: ButtonStylesDeps) => { lineHeight: '18px', height: '20px', fontSize: size.m, - borderRadius: border.radius.medium, + fontFamily: font.family, + fontWeight: font.weight.medium, + borderRadius: border.radius.small, color: shade(theme.euiColorVis6, 0.25), - marginLeft: size.s, + marginLeft: size.xs, + marginRight: size.xs, minWidth: 0, + padding: `${size.s} ${size.xxs}`, + span: { + padding: `0px ${size.xxs} !important`, + }, }; const buttonArrow: CSSObject = { - marginLeft: size.s, + marginLeft: size.xs, }; const alertButton: CSSObject = { @@ -72,6 +79,10 @@ export const useButtonStyles = ({ isExpanded }: ButtonStylesDeps) => { textTransform: 'capitalize', }; + const buttonSize: CSSObject = { + padding: `0px ${euiTheme.size.xs}`, + }; + const expandedIcon = isExpanded ? 'arrowUp' : 'arrowDown'; return { @@ -81,6 +92,7 @@ export const useButtonStyles = ({ isExpanded }: ButtonStylesDeps) => { alertsCountNumber, userChangedButton, userChangedButtonUsername, + buttonSize, expandedIcon, }; }, [euiTheme, isExpanded]); diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index 1ec9441a2b1d1..2e1a598faedaa 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -12,6 +12,7 @@ import { EuiFlexItem, EuiResizableContainer, EuiPanel, + EuiHorizontalRule, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { SectionLoading } from '../../shared_imports'; @@ -132,133 +133,137 @@ export const SessionView = ({ return ( <> - - - - - - - - - - - - + + + - - - - - - - {(EuiResizablePanel, EuiResizableButton) => ( - <> - - {renderIsLoading && ( - - - - )} + - {hasError && ( - - - - } - body={ -

- -

- } + + + + + + + - )} + + + + + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + {renderIsLoading && ( + + + + )} - {hasData && ( -
- + + + } + body={ +

+ +

+ } /> -
- )} -
+ )} - {renderDetails ? ( - <> - - - - - - ) : ( - <> - {/* Returning an empty element here (instead of false) to avoid a bug in EuiResizableContainer */} - - )} - - )} -
+ {hasData && ( +
+ +
+ )} +
+ + {renderDetails ? ( + <> + + + + + + ) : ( + <> + {/* Returning an empty element here (instead of false) to avoid a bug in EuiResizableContainer */} + + )} + + )} +
+ ); }; diff --git a/x-pack/plugins/session_view/public/components/session_view/styles.ts b/x-pack/plugins/session_view/public/components/session_view/styles.ts index edfe2356d5aa2..3ca0594f57574 100644 --- a/x-pack/plugins/session_view/public/components/session_view/styles.ts +++ b/x-pack/plugins/session_view/public/components/session_view/styles.ts @@ -8,6 +8,7 @@ import { useMemo } from 'react'; import { useEuiTheme } from '@elastic/eui'; import { CSSObject } from '@emotion/react'; +import { euiLightVars as theme } from '@kbn/ui-theme'; interface StylesDeps { height: number | undefined; @@ -17,9 +18,7 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { - const { border, colors } = euiTheme; - - const thinBorder = `${border.width.thin} solid ${colors.lightShade}!important`; + const { border } = euiTheme; const processTree: CSSObject = { height: `${height}px`, @@ -28,8 +27,7 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const detailPanel: CSSObject = { height: `${height}px`, - borderLeft: thinBorder, - borderRight: thinBorder, + borderRightWidth: '0px', }; const resizeHandle: CSSObject = { @@ -45,12 +43,23 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { margin: `${euiTheme.size.m} ${euiTheme.size.xs} !important`, }; + const sessionViewerComponent: CSSObject = { + border: border.thin, + borderRadius: border.radius.medium, + }; + + const toolBar: CSSObject = { + backgroundColor: `${theme.euiFormBackgroundDisabledColor} !important`, + }; + return { processTree, detailPanel, resizeHandle, searchBar, buttonsEyeDetail, + sessionViewerComponent, + toolBar, }; }, [height, euiTheme]); diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx index 51eb65a38f835..e24409a98f8fd 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx @@ -12,6 +12,7 @@ import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { getDetailPanelProcess, getSelectedTabContent } from './helpers'; import { DetailPanelProcessTab } from '../detail_panel_process_tab'; import { DetailPanelHostTab } from '../detail_panel_host_tab'; +import { useStyles } from './styles'; import { DetailPanelAlertTab } from '../detail_panel_alert_tab'; import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; @@ -101,8 +102,10 @@ export const SessionViewDetailPanel = ({ [tabs, selectedTabId] ); + const styles = useStyles(); + return ( - <> +
{tabs.map((tab, index) => ( {tabContent} - +
); }; diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/styles.ts b/x-pack/plugins/session_view/public/components/session_view_detail_panel/styles.ts new file mode 100644 index 0000000000000..fbb196da3fa80 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/styles.ts @@ -0,0 +1,25 @@ +/* + * 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 { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + const cached = useMemo(() => { + const detailsPanelLeftBorder: CSSObject = { + borderLeft: euiTheme.border.thin, + }; + + return { + detailsPanelLeftBorder, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx index f4e4dac7a94c7..05154fca40769 100644 --- a/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx @@ -7,6 +7,7 @@ import React, { useState, useEffect } from 'react'; import { EuiSearchBar, EuiPagination } from '@elastic/eui'; import { EuiSearchBarOnChangeArgs } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Process } from '../../../common/types/process_tree'; import { useStyles } from './styles'; @@ -17,6 +18,12 @@ interface SessionViewSearchBarDeps { onProcessSelected(process: Process): void; } +const translatePlaceholder = { + placeholder: i18n.translate('xpack.sessionView.searchBar.searchBarKeyPlaceholder', { + defaultMessage: 'Find...', + }), +}; + /** * The main wrapper component for the session view. */ @@ -26,7 +33,9 @@ export const SessionViewSearchBar = ({ onProcessSelected, searchResults, }: SessionViewSearchBarDeps) => { - const styles = useStyles(); + const showPagination = !!searchResults?.length; + + const styles = useStyles({ hasSearchResults: showPagination }); const [selectedResult, setSelectedResult] = useState(0); @@ -50,11 +59,9 @@ export const SessionViewSearchBar = ({ } }, [searchResults, onProcessSelected, selectedResult]); - const showPagination = !!searchResults?.length; - return ( -
- +
+ {showPagination && ( { +interface StylesDeps { + hasSearchResults: boolean; +} + +export const useStyles = ({ hasSearchResults }: StylesDeps) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { @@ -19,10 +23,18 @@ export const useStyles = () => { right: euiTheme.size.xxl, }; + const searchBarWithResult: CSSObject = { + position: 'relative', + 'input.euiFieldSearch.euiFieldSearch-isClearable': { + paddingRight: hasSearchResults ? '200px' : euiTheme.size.xxl, + }, + }; + return { pagination, + searchBarWithResult, }; - }, [euiTheme]); + }, [euiTheme, hasSearchResults]); return cached; }; From 9f10418f34a1984665bcdc54098204544c1a51c2 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Tue, 29 Mar 2022 17:43:01 +0200 Subject: [PATCH 002/108] Sort content sources and connectors by display name (#128745) * Sort content sources and connectors by display name --- .../workplace_search/utils/index.ts | 1 + .../utils/sort_by_name.test.ts | 42 +++++++++++++++++++ .../workplace_search/utils/sort_by_name.ts | 14 +++++++ .../views/content_sources/sources_logic.ts | 28 +++++++------ .../views/settings/settings_logic.ts | 3 +- 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/sort_by_name.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/sort_by_name.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts index 86d3e4f844bbd..c66a6d1ca0fc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts @@ -13,3 +13,4 @@ export { readUploadedFileAsText } from './read_uploaded_file_as_text'; export { handlePrivateKeyUpload } from './handle_private_key_upload'; export { hasMultipleConnectorOptions } from './has_multiple_connector_options'; export { isNotNullish } from './is_not_nullish'; +export { sortByName } from './sort_by_name'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/sort_by_name.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/sort_by_name.test.ts new file mode 100644 index 0000000000000..185eeedda0512 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/sort_by_name.test.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 { sortByName } from './sort_by_name'; + +describe('sortByName', () => { + it('should sort by name', () => { + const unsorted = [ + { + name: 'aba', + }, + { + name: 'aaa', + }, + { + name: 'constant', + }, + { + name: 'beta', + }, + ]; + const sorted = [ + { + name: 'aaa', + }, + { + name: 'aba', + }, + { + name: 'beta', + }, + { + name: 'constant', + }, + ]; + expect(sortByName(unsorted)).toEqual(sorted); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/sort_by_name.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/sort_by_name.ts new file mode 100644 index 0000000000000..cb2f85b245166 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/sort_by_name.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +interface WithName { + name: string; +} + +export function sortByName(nameItems: T[]): T[] { + return [...nameItems].sort((a, b) => a.name.localeCompare(b.name)); +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts index 1651411fb9c5d..868831ab7c7fb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -15,6 +15,7 @@ import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_message import { HttpLogic } from '../../../shared/http'; import { AppLogic } from '../../app_logic'; import { Connector, ContentSourceDetails, ContentSourceStatus, SourceDataItem } from '../../types'; +import { sortByName } from '../../utils'; import { staticSourceData } from './source_data'; @@ -50,7 +51,7 @@ export interface IPermissionsModalProps { additionalConfiguration: boolean; } -type CombinedDataItem = SourceDataItem & ContentSourceDetails; +type CombinedDataItem = SourceDataItem & { connected: boolean }; export interface ISourcesValues { contentSources: ContentSourceDetails[]; @@ -144,11 +145,13 @@ export const SourcesLogic = kea>( selectors: ({ selectors }) => ({ availableSources: [ () => [selectors.sourceData], - (sourceData: SourceDataItem[]) => sourceData.filter(({ configured }) => !configured), + (sourceData: SourceDataItem[]) => + sortByName(sourceData.filter(({ configured }) => !configured)), ], configuredSources: [ () => [selectors.sourceData], - (sourceData: SourceDataItem[]) => sourceData.filter(({ configured }) => configured), + (sourceData: SourceDataItem[]) => + sortByName(sourceData.filter(({ configured }) => configured)), ], externalConfigured: [ () => [selectors.configuredSources], @@ -307,18 +310,17 @@ export const mergeServerAndStaticData = ( serverData: Connector[], staticData: SourceDataItem[], contentSources: ContentSourceDetails[] -) => { - const combined = [] as CombinedDataItem[]; - staticData.forEach((staticItem) => { - const type = staticItem.serviceType; - const serverItem = serverData.find(({ serviceType }) => serviceType === type); - const connectedSource = contentSources.find(({ serviceType }) => serviceType === type); - combined.push({ +): CombinedDataItem[] => { + const unsortedData = staticData.map((staticItem) => { + const serverItem = serverData.find(({ serviceType }) => serviceType === staticItem.serviceType); + const connectedSource = contentSources.find( + ({ serviceType }) => serviceType === staticItem.serviceType + ); + return { ...staticItem, ...serverItem, connected: !!connectedSource, - } as CombinedDataItem); + }; }); - - return combined; + return sortByName(unsortedData); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts index 64dfa3f8e13bb..8c6dee4cf7f9c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts @@ -20,6 +20,7 @@ import { AppLogic } from '../../app_logic'; import { ORG_UPDATED_MESSAGE, OAUTH_APP_UPDATED_MESSAGE } from '../../constants'; import { ORG_SETTINGS_CONNECTORS_PATH } from '../../routes'; import { Connector } from '../../types'; +import { sortByName } from '../../utils'; interface IOauthApplication { name: string; @@ -118,7 +119,7 @@ export const SettingsLogic = kea> connectors: [ [], { - onInitializeConnectors: (_, connectors) => connectors, + onInitializeConnectors: (_, connectors) => sortByName(connectors), }, ], orgNameInputValue: [ From ac4e96c606106f9abc7341b280134f01ab4f1d76 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Tue, 29 Mar 2022 08:59:53 -0700 Subject: [PATCH 003/108] [Security Solution][Alerts] Remove dead legacy signals code (#128328) * Remove dead legacy signals code * Remove unused import * Remove extra param --- ...eate_persistence_rule_type_wrapper.mock.ts | 22 + .../create_security_rule_type_wrapper.ts | 32 +- .../build_alert_group_from_sequence.test.ts | 344 +++++++++++++- .../utils/build_alert_group_from_sequence.ts | 52 ++- .../signals/build_bulk_body.ts | 251 ---------- .../signals/build_event_type_signal.test.ts | 101 ---- .../signals/build_event_type_signal.ts | 30 -- .../signals/build_rule.test.ts | 189 -------- .../detection_engine/signals/build_rule.ts | 91 ---- .../signals/build_signal.test.ts | 376 --------------- .../detection_engine/signals/build_signal.ts | 130 ------ .../signals/bulk_create_factory.ts | 107 ----- .../signals/bulk_create_ml_signals.ts | 2 +- .../signals/executors/query.ts | 1 - .../detection_engine/signals/get_filter.ts | 4 +- .../signals/search_after_bulk_create.test.ts | 441 ++++-------------- .../signals/search_after_bulk_create.ts | 3 +- .../threat_mapping/create_event_signal.ts | 1 - .../threat_mapping/create_threat_signal.ts | 1 - .../bulk_create_threshold_signals.test.ts | 20 +- .../bulk_create_threshold_signals.ts | 31 +- .../lib/detection_engine/signals/types.ts | 13 +- .../signals/wrap_hits_factory.ts | 37 -- .../signals/wrap_sequences_factory.ts | 39 -- 24 files changed, 539 insertions(+), 1779 deletions(-) create mode 100644 x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.mock.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.mock.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.mock.ts new file mode 100644 index 0000000000000..86ef14e491f4e --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.mock.ts @@ -0,0 +1,22 @@ +/* + * 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 { alertsMock } from '../../../alerting/server/mocks'; +import { PersistenceServices } from './persistence_types'; + +export const createPersistenceServicesMock = (): jest.Mocked => { + return { + alertWithPersistence: jest.fn(), + }; +}; + +export const createPersistenceExecutorOptionsMock = () => { + return { + ...alertsMock.createAlertServices(), + ...createPersistenceServicesMock(), + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 38300dff14558..f25bb16e90004 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -8,7 +8,6 @@ import { isEmpty } from 'lodash'; import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; -import { ListArray } from '@kbn/securitysolution-io-ts-list-types'; import agent from 'elastic-apm-node'; import { createPersistenceRuleTypeWrapper } from '../../../../../rule_registry/server'; @@ -19,11 +18,7 @@ import { getRuleRangeTuples, hasReadIndexPrivileges, hasTimestampFields, - isEqlParams, - isQueryParams, - isSavedQueryParams, - isThreatParams, - isThresholdParams, + isMachineLearningParams, } from '../signals/utils'; import { DEFAULT_MAX_SIGNALS, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants'; import { CreateSecurityRuleTypeWrapper } from './types'; @@ -133,22 +128,13 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ...params, name, id: alertId, - } as unknown as NotificationRuleTypeParams; + }; // check if rule has permissions to access given index pattern // move this collection of lines into a function in utils // so that we can use it in create rules route, bulk, etc. try { - // Typescript 4.1.3 can't figure out that `!isMachineLearningParams(params)` also excludes the only rule type - // of rule params that doesn't include `params.index`, but Typescript 4.3.5 does compute the stricter type correctly. - // When we update Typescript to >= 4.3.5, we can replace this logic with `!isMachineLearningParams(params)` again. - if ( - isEqlParams(params) || - isThresholdParams(params) || - isQueryParams(params) || - isSavedQueryParams(params) || - isThreatParams(params) - ) { + if (!isMachineLearningParams(params)) { const index = params.index; const hasTimestampOverride = !!timestampOverride; @@ -170,7 +156,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = { index, fields: hasTimestampOverride - ? ['@timestamp', timestampOverride as string] + ? ['@timestamp', timestampOverride] : ['@timestamp'], include_unmapped: true, }, @@ -178,9 +164,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ) ); wroteWarningStatus = await hasTimestampFields({ - timestampField: hasTimestampOverride - ? (timestampOverride as string) - : '@timestamp', + timestampField: hasTimestampOverride ? timestampOverride : '@timestamp', timestampFieldCapsResponse: timestampFieldCaps, inputIndices, ruleExecutionLogger, @@ -202,8 +186,8 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const { tuples, remainingGap } = getRuleRangeTuples({ logger, previousStartedAt, - from: from as string, - to: to as string, + from, + to, interval, maxSignals: maxSignals ?? DEFAULT_MAX_SIGNALS, buildRuleMessage, @@ -236,7 +220,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const exceptionItems = await getExceptions({ client: exceptionsClient, - lists: (params.exceptionsList as ListArray) ?? [], + lists: params.exceptionsList, }); const bulkCreate = bulkCreateFactory( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.test.ts index 969f7caab6456..b1b68829665fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.test.ts @@ -10,7 +10,11 @@ import { Logger } from 'kibana/server'; import { ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { sampleDocNoSortId, sampleRuleGuid } from '../../../signals/__mocks__/es_results'; -import { buildAlertGroupFromSequence } from './build_alert_group_from_sequence'; +import { + buildAlertGroupFromSequence, + objectArrayIntersection, + objectPairIntersection, +} from './build_alert_group_from_sequence'; import { SERVER_APP_ID } from '../../../../../../common/constants'; import { getCompleteRuleMock, getQueryRuleParams } from '../../../schemas/rule_schemas.mock'; import { QueryRuleParams } from '../../../schemas/rule_schemas'; @@ -134,4 +138,342 @@ describe('buildAlert', () => { expect(groupId).toEqual(groupIds[0]); } }); + + describe('recursive intersection between objects', () => { + test('should treat numbers and strings as unequal', () => { + const a = { + field1: 1, + field2: 1, + }; + const b = { + field1: 1, + field2: '1', + }; + const intersection = objectPairIntersection(a, b); + const expected = { + field1: 1, + }; + expect(intersection).toEqual(expected); + }); + + test('should strip unequal numbers and strings', () => { + const a = { + field1: 1, + field2: 1, + field3: 'abcd', + field4: 'abcd', + }; + const b = { + field1: 1, + field2: 100, + field3: 'abcd', + field4: 'wxyz', + }; + const intersection = objectPairIntersection(a, b); + const expected = { + field1: 1, + field3: 'abcd', + }; + expect(intersection).toEqual(expected); + }); + + test('should handle null values', () => { + const a = { + field1: 1, + field2: '1', + field3: null, + }; + const b = { + field1: null, + field2: null, + field3: null, + }; + const intersection = objectPairIntersection(a, b); + const expected = { + field3: null, + }; + expect(intersection).toEqual(expected); + }); + + test('should handle explicit undefined values and return undefined if left with only undefined fields', () => { + const a = { + field1: 1, + field2: '1', + field3: undefined, + }; + const b = { + field1: undefined, + field2: undefined, + field3: undefined, + }; + const intersection = objectPairIntersection(a, b); + const expected = undefined; + expect(intersection).toEqual(expected); + }); + + test('should strip arrays out regardless of whether they are equal', () => { + const a = { + array_field1: [1, 2], + array_field2: [1, 2], + }; + const b = { + array_field1: [1, 2], + array_field2: [3, 4], + }; + const intersection = objectPairIntersection(a, b); + const expected = undefined; + expect(intersection).toEqual(expected); + }); + + test('should strip fields that are not in both objects', () => { + const a = { + field1: 1, + }; + const b = { + field2: 1, + }; + const intersection = objectPairIntersection(a, b); + const expected = undefined; + expect(intersection).toEqual(expected); + }); + + test('should work on objects within objects', () => { + const a = { + container_field: { + field1: 1, + field2: 1, + field3: 10, + field5: 1, + field6: null, + array_field: [1, 2], + nested_container_field: { + field1: 1, + field2: 1, + }, + nested_container_field2: { + field1: undefined, + }, + }, + container_field_without_intersection: { + sub_field1: 1, + }, + }; + const b = { + container_field: { + field1: 1, + field2: 2, + field4: 10, + field5: '1', + field6: null, + array_field: [1, 2], + nested_container_field: { + field1: 1, + field2: 2, + }, + nested_container_field2: { + field1: undefined, + }, + }, + container_field_without_intersection: { + sub_field2: 1, + }, + }; + const intersection = objectPairIntersection(a, b); + const expected = { + container_field: { + field1: 1, + field6: null, + nested_container_field: { + field1: 1, + }, + }, + }; + expect(intersection).toEqual(expected); + }); + + test('should work on objects with a variety of fields', () => { + const a = { + field1: 1, + field2: 1, + field3: 10, + field5: 1, + field6: null, + array_field: [1, 2], + container_field: { + sub_field1: 1, + sub_field2: 1, + sub_field3: 10, + }, + container_field_without_intersection: { + sub_field1: 1, + }, + }; + const b = { + field1: 1, + field2: 2, + field4: 10, + field5: '1', + field6: null, + array_field: [1, 2], + container_field: { + sub_field1: 1, + sub_field2: 2, + sub_field4: 10, + }, + container_field_without_intersection: { + sub_field2: 1, + }, + }; + const intersection = objectPairIntersection(a, b); + const expected = { + field1: 1, + field6: null, + container_field: { + sub_field1: 1, + }, + }; + expect(intersection).toEqual(expected); + }); + }); + + describe('objectArrayIntersection', () => { + test('should return undefined if the array is empty', () => { + const intersection = objectArrayIntersection([]); + const expected = undefined; + expect(intersection).toEqual(expected); + }); + test('should return the initial object if there is only 1', () => { + const a = { + field1: 1, + field2: 1, + field3: 10, + field5: 1, + field6: null, + array_field: [1, 2], + container_field: { + sub_field1: 1, + sub_field2: 1, + sub_field3: 10, + }, + container_field_without_intersection: { + sub_field1: 1, + }, + }; + const intersection = objectArrayIntersection([a]); + const expected = { + field1: 1, + field2: 1, + field3: 10, + field5: 1, + field6: null, + array_field: [1, 2], + container_field: { + sub_field1: 1, + sub_field2: 1, + sub_field3: 10, + }, + container_field_without_intersection: { + sub_field1: 1, + }, + }; + expect(intersection).toEqual(expected); + }); + test('should work with exactly 2 objects', () => { + const a = { + field1: 1, + field2: 1, + field3: 10, + field5: 1, + field6: null, + array_field: [1, 2], + container_field: { + sub_field1: 1, + sub_field2: 1, + sub_field3: 10, + }, + container_field_without_intersection: { + sub_field1: 1, + }, + }; + const b = { + field1: 1, + field2: 2, + field4: 10, + field5: '1', + field6: null, + array_field: [1, 2], + container_field: { + sub_field1: 1, + sub_field2: 2, + sub_field4: 10, + }, + container_field_without_intersection: { + sub_field2: 1, + }, + }; + const intersection = objectArrayIntersection([a, b]); + const expected = { + field1: 1, + field6: null, + container_field: { + sub_field1: 1, + }, + }; + expect(intersection).toEqual(expected); + }); + + test('should work with 3 or more objects', () => { + const a = { + field1: 1, + field2: 1, + field3: 10, + field5: 1, + field6: null, + array_field: [1, 2], + container_field: { + sub_field1: 1, + sub_field2: 1, + sub_field3: 10, + }, + container_field_without_intersection: { + sub_field1: 1, + }, + }; + const b = { + field1: 1, + field2: 2, + field4: 10, + field5: '1', + field6: null, + array_field: [1, 2], + container_field: { + sub_field1: 1, + sub_field2: 2, + sub_field4: 10, + }, + container_field_without_intersection: { + sub_field2: 1, + }, + }; + const c = { + field1: 1, + field2: 2, + field4: 10, + field5: '1', + array_field: [1, 2], + container_field: { + sub_field2: 2, + sub_field4: 10, + }, + container_field_without_intersection: { + sub_field2: 1, + }, + }; + const intersection = objectArrayIntersection([a, b, c]); + const expected = { + field1: 1, + }; + expect(intersection).toEqual(expected); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.ts index 180494f9209dd..26e0289732bfb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.ts @@ -16,7 +16,6 @@ import { buildAlert, buildAncestors, generateAlertId } from './build_alert'; import { buildBulkBody } from './build_bulk_body'; import { EqlSequence } from '../../../../../../common/detection_engine/types'; import { generateBuildingBlockIds } from './generate_building_block_ids'; -import { objectArrayIntersection } from '../../../signals/build_bulk_body'; import { BuildReasonMessage } from '../../../signals/reason_formatters'; import { CompleteRule, RuleParams } from '../../../schemas/rule_schemas'; import { @@ -118,3 +117,54 @@ export const buildAlertRoot = ( [ALERT_GROUP_ID]: generateAlertId(doc), }; }; + +export const objectArrayIntersection = (objects: object[]) => { + if (objects.length === 0) { + return undefined; + } else if (objects.length === 1) { + return objects[0]; + } else { + return objects + .slice(1) + .reduce( + (acc: object | undefined, obj): object | undefined => objectPairIntersection(acc, obj), + objects[0] + ); + } +}; + +export const objectPairIntersection = (a: object | undefined, b: object | undefined) => { + if (a === undefined || b === undefined) { + return undefined; + } + const intersection: Record = {}; + Object.entries(a).forEach(([key, aVal]) => { + if (key in b) { + const bVal = (b as Record)[key]; + if ( + typeof aVal === 'object' && + !(aVal instanceof Array) && + aVal !== null && + typeof bVal === 'object' && + !(bVal instanceof Array) && + bVal !== null + ) { + intersection[key] = objectPairIntersection(aVal, bVal); + } else if (aVal === bVal) { + intersection[key] = aVal; + } + } + }); + // Count up the number of entries that are NOT undefined in the intersection + // If there are no keys OR all entries are undefined, return undefined + if ( + Object.values(intersection).reduce( + (acc: number, value) => (value !== undefined ? acc + 1 : acc), + 0 + ) === 0 + ) { + return undefined; + } else { + return intersection; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts deleted file mode 100644 index 21bfced47df42..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ /dev/null @@ -1,251 +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 { TIMESTAMP } from '@kbn/rule-data-utils'; -import { getMergeStrategy } from './source_fields_merging/strategies'; -import { - SignalSourceHit, - SignalHit, - Signal, - BaseSignalHit, - SignalSource, - WrappedSignalHit, -} from './types'; -import { buildRuleWithoutOverrides, buildRuleWithOverrides } from './build_rule'; -import { additionalSignalFields, buildSignal } from './build_signal'; -import { buildEventTypeSignal } from './build_event_type_signal'; -import { EqlSequence } from '../../../../common/detection_engine/types'; -import { generateSignalId, wrapBuildingBlocks, wrapSignal } from './utils'; -import type { ConfigType } from '../../../config'; -import { BuildReasonMessage } from './reason_formatters'; -import { CompleteRule, RuleParams } from '../schemas/rule_schemas'; - -/** - * Formats the search_after result for insertion into the signals index. We first create a - * "best effort" merged "fields" with the "_source" object, then build the signal object, - * then the event object, and finally we strip away any additional temporary data that was added - * such as the "threshold_result". - * @param completeRule The rule object to build overrides - * @param doc The SignalSourceHit with "_source", "fields", and additional data such as "threshold_result" - * @returns The body that can be added to a bulk call for inserting the signal. - */ -export const buildBulkBody = ( - completeRule: CompleteRule, - doc: SignalSourceHit, - mergeStrategy: ConfigType['alertMergeStrategy'], - ignoreFields: ConfigType['alertIgnoreFields'], - buildReasonMessage: BuildReasonMessage -): SignalHit => { - const mergedDoc = getMergeStrategy(mergeStrategy)({ doc, ignoreFields }); - const rule = buildRuleWithOverrides(completeRule, mergedDoc._source ?? {}); - const timestamp = new Date().toISOString(); - const reason = buildReasonMessage({ - name: completeRule.ruleConfig.name, - severity: completeRule.ruleParams.severity, - mergedDoc, - }); - const signal: Signal = { - ...buildSignal([mergedDoc], rule, reason), - ...additionalSignalFields(mergedDoc), - }; - const event = buildEventTypeSignal(mergedDoc); - // Filter out any kibana.* fields from the generated signal - kibana.* fields are aliases - // in siem-signals so we can't write to them, but for signals-on-signals they'll be returned - // in the fields API response and merged into the mergedDoc source - const { - threshold_result: thresholdResult, - kibana, - ...filteredSource - } = mergedDoc._source || { - threshold_result: null, - }; - const signalHit: SignalHit = { - ...filteredSource, - [TIMESTAMP]: timestamp, - event, - signal, - }; - return signalHit; -}; - -/** - * Takes N raw documents from ES that form a sequence and builds them into N+1 signals ready to be indexed - - * one signal for each event in the sequence, and a "shell" signal that ties them all together. All N+1 signals - * share the same signal.group.id to make it easy to query them. - * @param sequence The raw ES documents that make up the sequence - * @param completeRule rule object representing the rule that found the sequence - * @param outputIndex Index to write the resulting signals to - */ -export const buildSignalGroupFromSequence = ( - sequence: EqlSequence, - completeRule: CompleteRule, - outputIndex: string, - mergeStrategy: ConfigType['alertMergeStrategy'], - ignoreFields: ConfigType['alertIgnoreFields'], - buildReasonMessage: BuildReasonMessage -): WrappedSignalHit[] => { - const wrappedBuildingBlocks = wrapBuildingBlocks( - sequence.events.map((event) => { - const signal = buildSignalFromEvent( - event, - completeRule, - false, - mergeStrategy, - ignoreFields, - buildReasonMessage - ); - signal.signal.rule.building_block_type = 'default'; - return signal; - }), - outputIndex - ); - - if ( - wrappedBuildingBlocks.some((block) => - block._source.signal?.ancestors.some((ancestor) => ancestor.rule === completeRule.alertId) - ) - ) { - return []; - } - - // Now that we have an array of building blocks for the events in the sequence, - // we can build the signal that links the building blocks together - // and also insert the group id (which is also the "shell" signal _id) in each building block - const sequenceSignal = wrapSignal( - buildSignalFromSequence(wrappedBuildingBlocks, completeRule, buildReasonMessage), - outputIndex - ); - wrappedBuildingBlocks.forEach((block, idx) => { - // TODO: fix type of blocks so we don't have to check existence of _source.signal - if (block._source.signal) { - block._source.signal.group = { - id: sequenceSignal._id, - index: idx, - }; - } - }); - return [...wrappedBuildingBlocks, sequenceSignal]; -}; - -export const buildSignalFromSequence = ( - events: WrappedSignalHit[], - completeRule: CompleteRule, - buildReasonMessage: BuildReasonMessage -): SignalHit => { - const rule = buildRuleWithoutOverrides(completeRule); - const timestamp = new Date().toISOString(); - const mergedEvents = objectArrayIntersection(events.map((event) => event._source)); - const reason = buildReasonMessage({ - name: completeRule.ruleConfig.name, - severity: completeRule.ruleParams.severity, - mergedDoc: mergedEvents as SignalSourceHit, - }); - const signal: Signal = buildSignal(events, rule, reason); - return { - ...mergedEvents, - [TIMESTAMP]: timestamp, - event: { - kind: 'signal', - }, - signal: { - ...signal, - group: { - // This is the same function that is used later to generate the _id for the sequence signal document, - // so _id should equal signal.group.id for the "shell" document - id: generateSignalId(signal), - }, - }, - }; -}; - -export const buildSignalFromEvent = ( - event: BaseSignalHit, - completeRule: CompleteRule, - applyOverrides: boolean, - mergeStrategy: ConfigType['alertMergeStrategy'], - ignoreFields: ConfigType['alertIgnoreFields'], - buildReasonMessage: BuildReasonMessage -): SignalHit => { - const mergedEvent = getMergeStrategy(mergeStrategy)({ doc: event, ignoreFields }); - const rule = applyOverrides - ? buildRuleWithOverrides(completeRule, mergedEvent._source ?? {}) - : buildRuleWithoutOverrides(completeRule); - const timestamp = new Date().toISOString(); - const reason = buildReasonMessage({ - name: completeRule.ruleConfig.name, - severity: completeRule.ruleParams.severity, - mergedDoc: mergedEvent, - }); - const signal: Signal = { - ...buildSignal([mergedEvent], rule, reason), - ...additionalSignalFields(mergedEvent), - }; - const eventFields = buildEventTypeSignal(mergedEvent); - // Filter out any kibana.* fields from the generated signal - kibana.* fields are aliases - // in siem-signals so we can't write to them, but for signals-on-signals they'll be returned - // in the fields API response and merged into the mergedDoc source - const { kibana, ...filteredSource } = mergedEvent._source || {}; - // TODO: better naming for SignalHit - it's really a new signal to be inserted - const signalHit: SignalHit = { - ...filteredSource, - [TIMESTAMP]: timestamp, - event: eventFields, - signal, - }; - return signalHit; -}; - -export const objectArrayIntersection = (objects: object[]) => { - if (objects.length === 0) { - return undefined; - } else if (objects.length === 1) { - return objects[0]; - } else { - return objects - .slice(1) - .reduce( - (acc: object | undefined, obj): object | undefined => objectPairIntersection(acc, obj), - objects[0] - ); - } -}; - -export const objectPairIntersection = (a: object | undefined, b: object | undefined) => { - if (a === undefined || b === undefined) { - return undefined; - } - const intersection: Record = {}; - Object.entries(a).forEach(([key, aVal]) => { - if (key in b) { - const bVal = (b as Record)[key]; - if ( - typeof aVal === 'object' && - !(aVal instanceof Array) && - aVal !== null && - typeof bVal === 'object' && - !(bVal instanceof Array) && - bVal !== null - ) { - intersection[key] = objectPairIntersection(aVal, bVal); - } else if (aVal === bVal) { - intersection[key] = aVal; - } - } - }); - // Count up the number of entries that are NOT undefined in the intersection - // If there are no keys OR all entries are undefined, return undefined - if ( - Object.values(intersection).reduce( - (acc: number, value) => (value !== undefined ? acc + 1 : acc), - 0 - ) === 0 - ) { - return undefined; - } else { - return intersection; - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.test.ts deleted file mode 100644 index cc3456e7ab968..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.test.ts +++ /dev/null @@ -1,101 +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 { sampleDocNoSortId } from './__mocks__/es_results'; -import { buildEventTypeSignal, isEventTypeSignal } from './build_event_type_signal'; - -describe('buildEventTypeSignal', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('it returns the event appended of kind signal if it does not exist', () => { - const doc = sampleDocNoSortId(); - delete doc._source.event; - const eventType = buildEventTypeSignal(doc); - const expected: object = { kind: 'signal' }; - expect(eventType).toEqual(expected); - }); - - test('it returns the event appended of kind signal if it is an empty object', () => { - const doc = sampleDocNoSortId(); - doc._source.event = {}; - const eventType = buildEventTypeSignal(doc); - const expected: object = { kind: 'signal' }; - expect(eventType).toEqual(expected); - }); - - test('it returns the event with kind signal and other properties if they exist', () => { - const doc = sampleDocNoSortId(); - doc._source.event = { - action: 'socket_opened', - module: 'system', - dataset: 'socket', - }; - const eventType = buildEventTypeSignal(doc); - const expected: object = { - action: 'socket_opened', - module: 'system', - dataset: 'socket', - kind: 'signal', - }; - expect(eventType).toEqual(expected); - }); - - test('It validates a sample doc with no signal type as "false"', () => { - const doc = sampleDocNoSortId(); - expect(isEventTypeSignal(doc)).toEqual(false); - }); - - test('It validates a sample doc with a signal type as "true"', () => { - const doc = { - ...sampleDocNoSortId(), - _source: { - ...sampleDocNoSortId()._source, - signal: { - rule: { id: 'id-123' }, - }, - }, - }; - expect(isEventTypeSignal(doc)).toEqual(true); - }); - - test('It validates a numeric signal string as "false"', () => { - const doc = { - ...sampleDocNoSortId(), - _source: { - ...sampleDocNoSortId()._source, - signal: 'something', - }, - }; - expect(isEventTypeSignal(doc)).toEqual(false); - }); - - test('It validates an empty object as "false"', () => { - const doc = { - ...sampleDocNoSortId(), - _source: { - ...sampleDocNoSortId()._source, - signal: {}, - }, - }; - expect(isEventTypeSignal(doc)).toEqual(false); - }); - - test('It validates an empty rule object as "false"', () => { - const doc = { - ...sampleDocNoSortId(), - _source: { - ...sampleDocNoSortId()._source, - signal: { - rule: {}, - }, - }, - }; - expect(isEventTypeSignal(doc)).toEqual(false); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.ts deleted file mode 100644 index 0dd2acfb88ffe..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.ts +++ /dev/null @@ -1,30 +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 { BaseSignalHit, SimpleHit } from './types'; -import { getField } from './utils'; - -export const buildEventTypeSignal = (doc: BaseSignalHit): object => { - if (doc._source != null && doc._source.event instanceof Object) { - return { ...doc._source.event, kind: 'signal' }; - } else { - return { kind: 'signal' }; - } -}; - -/** - * Given a document this will return true if that document is a signal - * document. We can't guarantee the code will call this function with a document - * before adding the _source.event.kind = "signal" from "buildEventTypeSignal" - * so we do basic testing to ensure that if the object has the fields of: - * "signal.rule.id" then it will be one of our signals rather than a customer - * overwritten signal. - * @param doc The document which might be a signal or it might be a regular log - */ -export const isEventTypeSignal = (doc: SimpleHit): boolean => { - const ruleId = getField(doc, 'signal.rule.id'); - return ruleId != null && typeof ruleId === 'string'; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts deleted file mode 100644 index 9ae51688ee676..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ /dev/null @@ -1,189 +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 { buildRuleWithOverrides, buildRuleWithoutOverrides } from './build_rule'; -import { sampleDocNoSortId, expectedRule, sampleDocSeverity } from './__mocks__/es_results'; -import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; -import { - getCompleteRuleMock, - getQueryRuleParams, - getThreatRuleParams, -} from '../schemas/rule_schemas.mock'; -import { - CompleteRule, - QueryRuleParams, - RuleParams, - ThreatRuleParams, -} from '../schemas/rule_schemas'; - -describe('buildRuleWithoutOverrides', () => { - let params: RuleParams; - let completeRule: CompleteRule; - - beforeEach(() => { - params = getQueryRuleParams(); - completeRule = getCompleteRuleMock(params); - }); - - test('builds a rule using rule alert', () => { - const rule = buildRuleWithoutOverrides(completeRule); - expect(rule).toEqual(expectedRule()); - }); - - test('builds a rule and removes internal tags', () => { - completeRule.ruleConfig.tags = [ - 'some fake tag 1', - 'some fake tag 2', - `${INTERNAL_RULE_ID_KEY}:rule-1`, - `${INTERNAL_IMMUTABLE_KEY}:true`, - ]; - const rule = buildRuleWithoutOverrides(completeRule); - expect(rule.tags).toEqual(['some fake tag 1', 'some fake tag 2']); - }); - - test('it builds a rule as expected with filters present', () => { - const ruleFilters = [ - { - query: 'host.name: Rebecca', - }, - { - query: 'host.name: Evan', - }, - { - query: 'host.name: Braden', - }, - ]; - completeRule.ruleParams.filters = ruleFilters; - const rule = buildRuleWithoutOverrides(completeRule); - expect(rule.filters).toEqual(ruleFilters); - }); - - test('it creates a indicator/threat_mapping/threat_matching rule', () => { - const ruleParams: ThreatRuleParams = { - ...getThreatRuleParams(), - threatMapping: [ - { - entries: [ - { - field: 'host.name', - value: 'host.name', - type: 'mapping', - }, - ], - }, - ], - threatFilters: [ - { - query: { - bool: { - must: [ - { - query_string: { - query: 'host.name: linux', - analyze_wildcard: true, - time_zone: 'Zulu', - }, - }, - ], - }, - }, - }, - ], - threatIndicatorPath: 'some.path', - threatQuery: 'threat_query', - threatIndex: ['threat_index'], - threatLanguage: 'kuery', - }; - const threatMatchCompleteRule = getCompleteRuleMock(ruleParams); - const threatMatchRule = buildRuleWithoutOverrides(threatMatchCompleteRule); - const expected: Partial = { - threat_mapping: ruleParams.threatMapping, - threat_filters: ruleParams.threatFilters, - threat_indicator_path: ruleParams.threatIndicatorPath, - threat_query: ruleParams.threatQuery, - threat_index: ruleParams.threatIndex, - threat_language: ruleParams.threatLanguage, - }; - expect(threatMatchRule).toEqual(expect.objectContaining(expected)); - }); -}); - -describe('buildRuleWithOverrides', () => { - let params: RuleParams; - let completeRule: CompleteRule; - - beforeEach(() => { - params = getQueryRuleParams(); - completeRule = getCompleteRuleMock(params); - }); - - test('it applies rule name override in buildRule', () => { - completeRule.ruleParams.ruleNameOverride = 'someKey'; - const rule = buildRuleWithOverrides(completeRule, sampleDocNoSortId()._source!); - const expected = { - ...expectedRule(), - name: 'someValue', - rule_name_override: 'someKey', - meta: { - ruleNameOverridden: true, - someMeta: 'someField', - }, - }; - expect(rule).toEqual(expected); - }); - - test('it applies risk score override in buildRule', () => { - const newRiskScore = 79; - completeRule.ruleParams.riskScoreMapping = [ - { - field: 'new_risk_score', - // value and risk_score aren't used for anything but are required in the schema - value: '', - operator: 'equals', - risk_score: undefined, - }, - ]; - const doc = sampleDocNoSortId(); - doc._source.new_risk_score = newRiskScore; - const rule = buildRuleWithOverrides(completeRule, doc._source!); - const expected = { - ...expectedRule(), - risk_score: newRiskScore, - risk_score_mapping: completeRule.ruleParams.riskScoreMapping, - meta: { - riskScoreOverridden: true, - someMeta: 'someField', - }, - }; - expect(rule).toEqual(expected); - }); - - test('it applies severity override in buildRule', () => { - const eventSeverity = '42'; - completeRule.ruleParams.severityMapping = [ - { - field: 'event.severity', - value: eventSeverity, - operator: 'equals', - severity: 'critical', - }, - ]; - const doc = sampleDocSeverity(Number(eventSeverity)); - const rule = buildRuleWithOverrides(completeRule, doc._source!); - const expected = { - ...expectedRule(), - severity: 'critical', - severity_mapping: completeRule.ruleParams.severityMapping, - meta: { - severityOverrideField: 'event.severity', - someMeta: 'someField', - }, - }; - expect(rule).toEqual(expected); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts deleted file mode 100644 index ab40ce330370c..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ /dev/null @@ -1,91 +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 { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { buildRiskScoreFromMapping } from './mappings/build_risk_score_from_mapping'; -import { SignalSource } from './types'; -import { buildSeverityFromMapping } from './mappings/build_severity_from_mapping'; -import { buildRuleNameFromMapping } from './mappings/build_rule_name_from_mapping'; -import { CompleteRule, RuleParams } from '../schemas/rule_schemas'; -import { commonParamsCamelToSnake, typeSpecificCamelToSnake } from '../schemas/rule_converters'; -import { transformTags } from '../routes/rules/utils'; -import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions'; - -export const buildRuleWithoutOverrides = (completeRule: CompleteRule): RulesSchema => { - const ruleParams = completeRule.ruleParams; - const { - actions, - schedule, - name, - tags, - enabled, - createdBy, - updatedBy, - throttle, - createdAt, - updatedAt, - } = completeRule.ruleConfig; - return { - actions: actions.map(transformAlertToRuleAction), - created_at: createdAt.toISOString(), - created_by: createdBy ?? '', - enabled, - id: completeRule.alertId, - interval: schedule.interval, - name, - tags: transformTags(tags), - throttle: throttle ?? undefined, - updated_at: updatedAt.toISOString(), - updated_by: updatedBy ?? '', - ...commonParamsCamelToSnake(ruleParams), - ...typeSpecificCamelToSnake(ruleParams), - }; -}; - -export const buildRuleWithOverrides = ( - completeRule: CompleteRule, - eventSource: SignalSource -): RulesSchema => { - const ruleWithoutOverrides = buildRuleWithoutOverrides(completeRule); - return applyRuleOverrides(ruleWithoutOverrides, eventSource, completeRule.ruleParams); -}; - -export const applyRuleOverrides = ( - rule: RulesSchema, - eventSource: SignalSource, - ruleParams: RuleParams -): RulesSchema => { - const { riskScore, riskScoreMeta } = buildRiskScoreFromMapping({ - eventSource, - riskScore: ruleParams.riskScore, - riskScoreMapping: ruleParams.riskScoreMapping, - }); - - const { severity, severityMeta } = buildSeverityFromMapping({ - eventSource, - severity: ruleParams.severity, - severityMapping: ruleParams.severityMapping, - }); - - const { ruleName, ruleNameMeta } = buildRuleNameFromMapping({ - eventSource, - ruleName: rule.name, - ruleNameMapping: ruleParams.ruleNameOverride, - }); - - const meta = { ...ruleParams.meta, ...riskScoreMeta, ...severityMeta, ...ruleNameMeta }; - return { - ...rule, - risk_score: riskScore, - risk_score_mapping: ruleParams.riskScoreMapping ?? [], - severity, - severity_mapping: ruleParams.severityMapping ?? [], - name: ruleName, - rule_name_override: ruleParams.ruleNameOverride, - meta: Object.keys(meta).length > 0 ? meta : undefined, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts deleted file mode 100644 index e06e8a5cdcf76..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ /dev/null @@ -1,376 +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 { sampleDocNoSortId } from './__mocks__/es_results'; -import { - buildSignal, - buildParent, - buildAncestors, - additionalSignalFields, - removeClashes, -} from './build_signal'; -import { Signal, Ancestor, BaseSignalHit } from './types'; -import { - getRulesSchemaMock, - ANCHOR_DATE, -} from '../../../../common/detection_engine/schemas/response/rules_schema.mocks'; -import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; -import { SIGNALS_TEMPLATE_VERSION } from '../routes/index/get_signals_template'; - -describe('buildSignal', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('it builds a signal as expected without original_event if event does not exist', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - delete doc._source.event; - const rule = getRulesSchemaMock(); - const reason = 'signal reasonable reason'; - - const signal = { - ...buildSignal([doc], rule, reason), - ...additionalSignalFields(doc), - }; - const expected: Signal = { - _meta: { - version: SIGNALS_TEMPLATE_VERSION, - }, - parent: { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - parents: [ - { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], - ancestors: [ - { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], - original_time: '2020-04-20T21:27:45.000Z', - reason: 'signal reasonable reason', - status: 'open', - rule: { - author: [], - id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - created_at: new Date(ANCHOR_DATE).toISOString(), - updated_at: new Date(ANCHOR_DATE).toISOString(), - created_by: 'elastic', - description: 'some description', - enabled: true, - false_positives: ['false positive 1', 'false positive 2'], - from: 'now-6m', - immutable: false, - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - references: ['test 1', 'test 2'], - severity: 'high', - severity_mapping: [], - updated_by: 'elastic_kibana', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - threat: [], - version: 1, - output_index: '.siem-signals-default', - max_signals: 100, - risk_score: 55, - risk_score_mapping: [], - language: 'kuery', - rule_id: 'query-rule-id', - interval: '5m', - exceptions_list: getListArrayMock(), - }, - depth: 1, - }; - expect(signal).toEqual(expected); - }); - - test('it builds a signal as expected with original_event if is present', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - doc._source.event = { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }; - const rule = getRulesSchemaMock(); - const reason = 'signal reasonable reason'; - const signal = { - ...buildSignal([doc], rule, reason), - ...additionalSignalFields(doc), - }; - const expected: Signal = { - _meta: { - version: SIGNALS_TEMPLATE_VERSION, - }, - parent: { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - parents: [ - { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], - ancestors: [ - { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], - original_time: '2020-04-20T21:27:45.000Z', - reason: 'signal reasonable reason', - original_event: { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }, - status: 'open', - rule: { - author: [], - id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - created_at: new Date(ANCHOR_DATE).toISOString(), - updated_at: new Date(ANCHOR_DATE).toISOString(), - created_by: 'elastic', - description: 'some description', - enabled: true, - false_positives: ['false positive 1', 'false positive 2'], - from: 'now-6m', - immutable: false, - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - references: ['test 1', 'test 2'], - severity: 'high', - severity_mapping: [], - updated_by: 'elastic_kibana', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - threat: [], - version: 1, - output_index: '.siem-signals-default', - max_signals: 100, - risk_score: 55, - risk_score_mapping: [], - language: 'kuery', - rule_id: 'query-rule-id', - interval: '5m', - exceptions_list: getListArrayMock(), - }, - depth: 1, - }; - expect(signal).toEqual(expected); - }); - - test('it builds a ancestor correctly if the parent does not exist', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - doc._source.event = { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }; - const signal = buildParent(doc); - const expected: Ancestor = { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }; - expect(signal).toEqual(expected); - }); - - test('it builds a ancestor correctly if the parent does exist', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - doc._source.event = { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }; - doc._source.signal = { - parents: [ - { - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], - ancestors: [ - { - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], - depth: 1, - rule: { - id: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', - }, - }; - const signal = buildParent(doc); - const expected: Ancestor = { - rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'signal', - index: 'myFakeSignalIndex', - depth: 1, - }; - expect(signal).toEqual(expected); - }); - - test('it builds a signal ancestor correctly if the parent does not exist', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - doc._source.event = { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }; - const signal = buildAncestors(doc); - const expected: Ancestor[] = [ - { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ]; - expect(signal).toEqual(expected); - }); - - test('it builds a signal ancestor correctly if the parent does exist', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - doc._source.event = { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }; - doc._source.signal = { - parents: [ - { - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], - ancestors: [ - { - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], - rule: { - id: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', - }, - depth: 1, - }; - const signal = buildAncestors(doc); - const expected: Ancestor[] = [ - { - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - { - rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'signal', - index: 'myFakeSignalIndex', - depth: 1, - }, - ]; - expect(signal).toEqual(expected); - }); - - describe('removeClashes', () => { - test('it will call renameClashes with a regular doc and not mutate it if it does not have a signal clash', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - const output = removeClashes(doc); - expect(output).toBe(doc); // reference check - }); - - test('it will call renameClashes with a regular doc and not change anything', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - const output = removeClashes(doc); - expect(output).toEqual(doc); // deep equal check - }); - - test('it will remove a "signal" numeric clash', () => { - const sampleDoc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - const doc = { - ...sampleDoc, - _source: { - ...sampleDoc._source, - signal: 127, - }, - } as unknown as BaseSignalHit; - const output = removeClashes(doc); - expect(output).toEqual(sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71')); - }); - - test('it will remove a "signal" object clash', () => { - const sampleDoc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - const doc = { - ...sampleDoc, - _source: { - ...sampleDoc._source, - signal: { child_1: { child_2: 'Test nesting' } }, - }, - } as unknown as BaseSignalHit; - const output = removeClashes(doc); - expect(output).toEqual(sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71')); - }); - - test('it will not remove a "signal" if that is signal is one of our signals', () => { - const sampleDoc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - const doc = { - ...sampleDoc, - _source: { - ...sampleDoc._source, - signal: { rule: { id: '123' } }, - }, - } as unknown as BaseSignalHit; - const output = removeClashes(doc); - const expected = { - ...sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'), - _source: { - ...sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71')._source, - signal: { rule: { id: '123' } }, - }, - }; - expect(output).toEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts deleted file mode 100644 index 5e26466557217..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ /dev/null @@ -1,130 +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 { SearchTypes } from '../../../../common/detection_engine/types'; -import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { SIGNALS_TEMPLATE_VERSION } from '../routes/index/get_signals_template'; -import { isEventTypeSignal } from './build_event_type_signal'; -import { Signal, Ancestor, BaseSignalHit, ThresholdResult, SimpleHit } from './types'; -import { getValidDateFromDoc } from './utils'; - -/** - * Takes a parent signal or event document and extracts the information needed for the corresponding entry in the child - * signal's `signal.parents` array. - * @param doc The parent signal or event - */ -export const buildParent = (doc: BaseSignalHit): Ancestor => { - if (doc._source?.signal != null) { - return { - rule: doc._source?.signal.rule.id, - id: doc._id, - type: 'signal', - index: doc._index, - // We first look for signal.depth and use that if it exists. If it doesn't exist, this should be a pre-7.10 signal - // and should have signal.parent.depth instead. signal.parent.depth in this case is treated as equivalent to signal.depth. - depth: doc._source?.signal.depth ?? doc._source?.signal.parent?.depth ?? 1, - }; - } else { - return { - id: doc._id, - type: 'event', - index: doc._index, - depth: 0, - }; - } -}; - -/** - * Takes a parent signal or event document with N ancestors and adds the parent document to the ancestry array, - * creating an array of N+1 ancestors. - * @param doc The parent signal/event for which to extend the ancestry. - */ -export const buildAncestors = (doc: BaseSignalHit): Ancestor[] => { - const newAncestor = buildParent(doc); - const existingAncestors = doc._source?.signal?.ancestors; - if (existingAncestors != null) { - return [...existingAncestors, newAncestor]; - } else { - return [newAncestor]; - } -}; - -/** - * This removes any signal named clashes such as if a source index has - * "signal" but is not a signal object we put onto the object. If this - * is our "signal object" then we don't want to remove it. - * @param doc The source index doc to a signal. - */ -export const removeClashes = (doc: BaseSignalHit): BaseSignalHit => { - // @ts-expect-error @elastic/elasticsearch _source is optional - const { signal, ...noSignal } = doc._source; - if (signal == null || isEventTypeSignal(doc as SimpleHit)) { - return doc; - } else { - return { - ...doc, - _source: { ...noSignal }, - }; - } -}; - -/** - * Builds the `signal.*` fields that are common across all signals. - * @param docs The parent signals/events of the new signal to be built. - * @param rule The rule that is generating the new signal. - */ -export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema, reason: string): Signal => { - const _meta = { - version: SIGNALS_TEMPLATE_VERSION, - }; - const removedClashes = docs.map(removeClashes); - const parents = removedClashes.map(buildParent); - const depth = parents.reduce((acc, parent) => Math.max(parent.depth, acc), 0) + 1; - const ancestors = removedClashes.reduce( - (acc: Ancestor[], doc) => acc.concat(buildAncestors(doc)), - [] - ); - return { - _meta, - parents, - ancestors, - status: 'open', - rule, - reason, - depth, - }; -}; - -const isThresholdResult = (thresholdResult: SearchTypes): thresholdResult is ThresholdResult => { - return typeof thresholdResult === 'object'; -}; - -/** - * Creates signal fields that are only available in the special case where a signal has only 1 parent signal/event. - * We copy the original time from the document as "original_time" since we override the timestamp with the current date time. - * @param doc The parent signal/event of the new signal to be built. - */ -export const additionalSignalFields = (doc: BaseSignalHit) => { - const thresholdResult = doc._source?.threshold_result; - if (thresholdResult != null && !isThresholdResult(thresholdResult)) { - throw new Error(`threshold_result failed to validate: ${thresholdResult}`); - } - const originalTime = getValidDateFromDoc({ - doc, - timestampOverride: undefined, - }); - return { - parent: buildParent(removeClashes(doc)), - original_time: originalTime != null ? originalTime.toISOString() : undefined, - original_event: doc._source?.event ?? undefined, - threshold_result: thresholdResult, - original_signal: - doc._source?.signal != null && !isEventTypeSignal(doc as SimpleHit) - ? doc._source?.signal - : undefined, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts deleted file mode 100644 index a8334cf0a4396..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts +++ /dev/null @@ -1,107 +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 { performance } from 'perf_hooks'; -import { countBy, isEmpty, get } from 'lodash'; - -import { ElasticsearchClient, Logger } from 'kibana/server'; -import { BuildRuleMessage } from './rule_messages'; -import { RefreshTypes } from '../types'; -import { BaseHit } from '../../../../common/detection_engine/types'; -import { errorAggregator, makeFloatString } from './utils'; -import { withSecuritySpan } from '../../../utils/with_security_span'; - -export interface GenericBulkCreateResponse { - success: boolean; - bulkCreateDuration: string; - createdItemsCount: number; - createdItems: Array; - errors: string[]; -} - -export const bulkCreateFactory = - ( - logger: Logger, - esClient: ElasticsearchClient, - buildRuleMessage: BuildRuleMessage, - refreshForBulkCreate: RefreshTypes, - indexNameOverride?: string - ) => - async (wrappedDocs: Array>): Promise> => { - if (wrappedDocs.length === 0) { - return { - errors: [], - success: true, - bulkCreateDuration: '0', - createdItemsCount: 0, - createdItems: [], - }; - } - - const bulkBody = wrappedDocs.flatMap((wrappedDoc) => [ - { - create: { - _index: indexNameOverride ?? wrappedDoc._index, - _id: wrappedDoc._id, - }, - }, - wrappedDoc._source, - ]); - const start = performance.now(); - - const response = await withSecuritySpan('writeAlertsBulk', () => - esClient.bulk({ - refresh: refreshForBulkCreate, - body: bulkBody, - }) - ); - - const end = performance.now(); - logger.debug( - buildRuleMessage( - `individual bulk process time took: ${makeFloatString(end - start)} milliseconds` - ) - ); - logger.debug(buildRuleMessage(`took property says bulk took: ${response.took} milliseconds`)); - const createdItems = wrappedDocs - .map((doc, index) => ({ - _id: response.items[index].create?._id ?? '', - _index: response.items[index].create?._index ?? '', - ...doc._source, - })) - .filter((_, index) => get(response.items[index], 'create.status') === 201); - const createdItemsCount = createdItems.length; - const duplicateSignalsCount = countBy(response.items, 'create.status')['409']; - const errorCountByMessage = errorAggregator(response, [409]); - - logger.debug(buildRuleMessage(`bulk created ${createdItemsCount} signals`)); - if (duplicateSignalsCount > 0) { - logger.debug(buildRuleMessage(`ignored ${duplicateSignalsCount} duplicate signals`)); - } - if (!isEmpty(errorCountByMessage)) { - logger.error( - buildRuleMessage( - `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` - ) - ); - return { - errors: Object.keys(errorCountByMessage), - success: false, - bulkCreateDuration: makeFloatString(end - start), - createdItemsCount, - createdItems, - }; - } else { - return { - errors: [], - success: true, - bulkCreateDuration: makeFloatString(end - start), - createdItemsCount, - createdItems, - }; - } - }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 2453e92dc3c0a..a757e178ea48a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -14,7 +14,7 @@ import { AlertInstanceState, AlertServices, } from '../../../../../alerting/server'; -import { GenericBulkCreateResponse } from './bulk_create_factory'; +import { GenericBulkCreateResponse } from '../rule_types/factories'; import { AnomalyResults, Anomaly } from '../../machine_learning'; import { BuildRuleMessage } from './rule_messages'; import { BulkCreate, WrapHits } from './types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts index 120bf2c2ebfce..5f1ab1c2dd5ff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts @@ -84,7 +84,6 @@ export const queryExecutor = async ({ eventsTelemetry, id: completeRule.alertId, inputIndexPattern: inputIndex, - signalsIndex: ruleParams.outputIndex, filter: esFilter, pageSize: searchAfterSize, buildReasonMessage: buildReasonMessageForQueryAlert, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index f849900ec75e1..f113e84c88ba8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -21,8 +21,8 @@ import { AlertServices, } from '../../../../../alerting/server'; import { PartialFilter } from '../types'; -import { QueryFilter } from './types'; import { withSecuritySpan } from '../../../utils/with_security_span'; +import { ESBoolQuery } from '../../../../common/typed_json'; interface GetFilterArgs { type: Type; @@ -53,7 +53,7 @@ export const getFilter = async ({ type, query, lists, -}: GetFilterArgs): Promise => { +}: GetFilterArgs): Promise => { const queryFilter = () => { if (query != null && language != null && index != null) { return getQueryFilter(query, language, filters || [], index, lists); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 7d22d58efdd6f..52d0a04eee1ec 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -15,7 +15,6 @@ import { sampleDocWithSortId, } from './__mocks__/es_results'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; -import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import uuid from 'uuid'; import { listMock } from '../../../../../lists/server/mocks'; @@ -27,17 +26,19 @@ import { getRuleRangeTuples } from './utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { getCompleteRuleMock, getQueryRuleParams } from '../schemas/rule_schemas.mock'; -import { bulkCreateFactory } from './bulk_create_factory'; -import { wrapHitsFactory } from './wrap_hits_factory'; +import { bulkCreateFactory } from '../rule_types/factories/bulk_create_factory'; +import { wrapHitsFactory } from '../rule_types/factories/wrap_hits_factory'; import { mockBuildRuleMessage } from './__mocks__/build_rule_message.mock'; -import { errors as esErrors } from '@elastic/elasticsearch'; import { BuildReasonMessage } from './reason_formatters'; import { QueryRuleParams } from '../schemas/rule_schemas'; +import { createPersistenceServicesMock } from '../../../../../rule_registry/server/utils/create_persistence_rule_type_wrapper.mock'; +import { PersistenceServices } from '../../../../../rule_registry/server'; const buildRuleMessage = mockBuildRuleMessage; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; + let mockPersistenceServices: jest.Mocked; let buildReasonMessage: BuildReasonMessage; let bulkCreate: BulkCreate; let wrapHits: WrapHits; @@ -46,6 +47,9 @@ describe('searchAfterAndBulkCreate', () => { const someGuids = Array.from({ length: 13 }).map(() => uuid.v4()); const sampleParams = getQueryRuleParams(); const queryCompleteRule = getCompleteRuleMock(sampleParams); + const defaultFilter = { + match_all: {}, + }; sampleParams.maxSignals = 30; let tuple: RuleRangeTuple; beforeEach(() => { @@ -65,17 +69,18 @@ describe('searchAfterAndBulkCreate', () => { maxSignals: sampleParams.maxSignals, buildRuleMessage, }).tuples[0]; + mockPersistenceServices = createPersistenceServicesMock(); bulkCreate = bulkCreateFactory( mockLogger, - mockService.scopedClusterClient.asCurrentUser, + mockPersistenceServices.alertWithPersistence, buildRuleMessage, false ); wrapHits = wrapHitsFactory({ completeRule: queryCompleteRule, - signalsIndex: DEFAULT_SIGNALS_INDEX, mergeStrategy: 'missingFields', ignoreFields: [], + spaceId: 'default', }); }); @@ -86,17 +91,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -105,17 +102,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -124,17 +113,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -143,17 +124,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -185,9 +158,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -205,17 +177,9 @@ describe('searchAfterAndBulkCreate', () => { repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)) ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -224,17 +188,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -243,17 +199,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -284,9 +232,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -305,35 +252,14 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [ + { _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }, ], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -364,9 +290,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -424,9 +349,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -439,39 +363,14 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success when empty string sortId present', async () => { - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - create: { - _id: someGuids[0], - _index: 'myfakeindex', - status: 201, - }, - }, - { - create: { - _id: someGuids[1], - _index: 'myfakeindex', - status: 201, - }, - }, - { - create: { - _id: someGuids[2], - _index: 'myfakeindex', - status: 201, - }, - }, - { - create: { - _id: someGuids[3], - _index: 'myfakeindex', - status: 201, - }, - }, + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [ + { _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }, ], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search .mockResolvedValueOnce( @@ -502,9 +401,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -558,9 +456,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -579,35 +476,14 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [ + { _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }, ], + errors: {}, }); const exceptionItem = getExceptionListItemSchemaMock(); @@ -632,9 +508,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -653,35 +528,14 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [ + { _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }, + { _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }, ], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -708,9 +562,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -722,58 +575,6 @@ describe('searchAfterAndBulkCreate', () => { expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); - test('if unsuccessful first bulk create', async () => { - const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.entries = [ - { - field: 'source.ip', - operator: 'included', - type: 'list', - list: { - id: 'ci-badguys.txt', - type: 'ip', - }, - }, - ]; - mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)) - ) - ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockReturnValue( - elasticsearchClientMock.createErrorTransportRequestPromise( - new esErrors.ResponseError( - elasticsearchClientMock.createApiResponse({ - statusCode: 400, - body: { error: { type: 'bulk_error_type' } }, - }) - ) - ) - ); - const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - listClient, - exceptionsList: [exceptionItem], - tuple, - completeRule: queryCompleteRule, - services: mockService, - logger: mockLogger, - eventsTelemetry: undefined, - id: sampleRuleGuid, - inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, - pageSize: 1, - filter: undefined, - buildReasonMessage, - buildRuleMessage, - bulkCreate, - wrapHits, - }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(success).toEqual(false); - expect(createdSignalsCount).toEqual(0); - expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); - }); - test('should return success with 0 total hits', async () => { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ @@ -808,9 +609,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -855,9 +655,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -895,6 +694,16 @@ describe('searchAfterAndBulkCreate', () => { ) ); + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: { + 'error on creation': { + count: 1, + statusCode: 500, + }, + }, + }); + mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce(bulkItem); // adds the response with errors we are testing mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -903,17 +712,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -922,17 +723,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -941,17 +734,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -970,9 +755,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, @@ -992,17 +776,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -1011,17 +787,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -1030,17 +798,9 @@ describe('searchAfterAndBulkCreate', () => { ) ); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResponseOnce({ - took: 100, - errors: false, - items: [ - { - // @ts-expect-error not full response interface - create: { - status: 201, - }, - }, - ], + mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ + createdAlerts: [{ _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }], + errors: {}, }); mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( @@ -1061,9 +821,8 @@ describe('searchAfterAndBulkCreate', () => { eventsTelemetry: undefined, id: sampleRuleGuid, inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, - filter: undefined, + filter: defaultFilter, buildReasonMessage, buildRuleMessage, bulkCreate, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 99230627cb6b8..69c001898b217 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -76,7 +76,6 @@ export const searchAfterAndBulkCreate = async ({ to: tuple.to.toISOString(), services, logger, - // @ts-expect-error please, declare a type explicitly instead of unknown filter, pageSize: Math.ceil(Math.min(tuple.maxSignals, pageSize)), timestampOverride: ruleParams.timestampOverride, @@ -165,7 +164,7 @@ export const searchAfterAndBulkCreate = async ({ success: bulkSuccess, createdSignalsCount: createdCount, createdSignals: createdItems, - bulkCreateTimes: bulkDuration ? [bulkDuration] : undefined, + bulkCreateTimes: [bulkDuration], errors: bulkErrors, }), ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts index faa14bcbab309..c5d86c9ab460c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts @@ -135,7 +135,6 @@ export const createEventSignal = async ({ logger, pageSize: searchAfterSize, services, - signalsIndex: outputIndex, sortOrder: 'desc', trackTotalHits: false, tuple, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index 220bebbaa4d21..a07de583d8bab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -86,7 +86,6 @@ export const createThreatSignal = async ({ logger, pageSize: searchAfterSize, services, - signalsIndex: outputIndex, sortOrder: 'desc', trackTotalHits: false, tuple, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.test.ts index 2c14e4bed62a8..4f68be017ad67 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.test.ts @@ -5,10 +5,8 @@ * 2.0. */ -import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; import { ThresholdNormalized } from '../../../../../common/detection_engine/schemas/common/schemas'; import { sampleDocSearchResultsNoSortId } from '../__mocks__/es_results'; -import { sampleThresholdSignalHistory } from '../__mocks__/threshold_signal_history.mock'; import { calculateThresholdSignalUuid } from '../utils'; import { transformThresholdResultsToEcs } from './bulk_create_threshold_signals'; @@ -60,12 +58,8 @@ describe('transformThresholdNormalizedResultsToEcs', () => { 'test', startedAt, from, - undefined, - loggingSystemMock.createLogger(), threshold, - '1234', - undefined, - sampleThresholdSignalHistory() + '1234' ); const _id = calculateThresholdSignalUuid( '1234', @@ -158,12 +152,8 @@ describe('transformThresholdNormalizedResultsToEcs', () => { 'test', startedAt, from, - undefined, - loggingSystemMock.createLogger(), threshold, - '1234', - undefined, - sampleThresholdSignalHistory() + '1234' ); expect(transformedResults).toEqual({ took: 10, @@ -226,12 +216,8 @@ describe('transformThresholdNormalizedResultsToEcs', () => { 'test', startedAt, from, - undefined, - loggingSystemMock.createLogger(), threshold, - '1234', - undefined, - sampleThresholdSignalHistory() + '1234' ); const _id = calculateThresholdSignalUuid('1234', startedAt, [], ''); expect(transformedResults).toEqual({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index f098f33b2ffc7..2148d4feacdae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -9,10 +9,7 @@ import { TIMESTAMP } from '@kbn/rule-data-utils'; import { get } from 'lodash/fp'; import set from 'set-value'; -import { - ThresholdNormalized, - TimestampOverrideOrUndefined, -} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { ThresholdNormalized } from '../../../../../common/detection_engine/schemas/common/schemas'; import { Logger } from '../../../../../../../../src/core/server'; import { AlertInstanceContext, @@ -21,7 +18,7 @@ import { } from '../../../../../../alerting/server'; import { BaseHit } from '../../../../../common/detection_engine/types'; import { TermAggregationBucket } from '../../../types'; -import { GenericBulkCreateResponse } from '../bulk_create_factory'; +import { GenericBulkCreateResponse } from '../../rule_types/factories/bulk_create_factory'; import { calculateThresholdSignalUuid, getThresholdAggregationParts } from '../utils'; import { buildReasonMessageForThresholdAlert } from '../reason_formatters'; import type { @@ -54,12 +51,8 @@ const getTransformedHits = ( inputIndex: string, startedAt: Date, from: Date, - logger: Logger, threshold: ThresholdNormalized, - ruleId: string, - filter: unknown, - timestampOverride: TimestampOverrideOrUndefined, - signalHistory: ThresholdSignalHistory + ruleId: string ) => { if (results.aggregations == null) { return []; @@ -184,24 +177,16 @@ export const transformThresholdResultsToEcs = ( inputIndex: string, startedAt: Date, from: Date, - filter: unknown, - logger: Logger, threshold: ThresholdNormalized, - ruleId: string, - timestampOverride: TimestampOverrideOrUndefined, - signalHistory: ThresholdSignalHistory + ruleId: string ): SignalSearchResponse => { const transformedHits = getTransformedHits( results, inputIndex, startedAt, from, - logger, threshold, - ruleId, - filter, - timestampOverride, - signalHistory + ruleId ); const thresholdResults = { ...results, @@ -228,12 +213,8 @@ export const bulkCreateThresholdSignals = async ( params.inputIndexPattern.join(','), params.startedAt, params.from, - params.filter, - params.logger, ruleParams.threshold, - ruleParams.ruleId, - ruleParams.timestampOverride, - params.signalHistory + ruleParams.ruleId ); return params.bulkCreate( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index a5803dc354040..44154a8727f38 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -6,7 +6,6 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { BoolQuery } from '@kbn/es-query'; import moment from 'moment'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -31,7 +30,7 @@ import { Logger } from '../../../../../../../src/core/server'; import { BuildRuleMessage } from './rule_messages'; import { ITelemetryEventsSender } from '../../telemetry/sender'; import { CompleteRule, RuleParams } from '../schemas/rule_schemas'; -import { GenericBulkCreateResponse } from './bulk_create_factory'; +import { GenericBulkCreateResponse } from '../rule_types/factories'; import { EcsFieldMap } from '../../../../../rule_registry/common/assets/field_maps/ecs_field_map'; import { TypeOfFieldMap } from '../../../../../rule_registry/common/field_map'; import { BuildReasonMessage } from './reason_formatters'; @@ -275,13 +274,6 @@ export interface AlertAttributes { export type BulkResponseErrorAggregation = Record; -/** - * TODO: Remove this if/when the return filter has its own type exposed - */ -export interface QueryFilter { - bool: BoolQuery; -} - export type SignalsEnrichment = (signals: SignalSearchResponse) => Promise; export type BulkCreate = >( @@ -314,9 +306,8 @@ export interface SearchAfterAndBulkCreateParams { eventsTelemetry: ITelemetryEventsSender | undefined; id: string; inputIndexPattern: string[]; - signalsIndex: string; pageSize: number; - filter: unknown; + filter: estypes.QueryDslQueryContainer; buildRuleMessage: BuildRuleMessage; buildReasonMessage: BuildReasonMessage; enrichment?: SignalsEnrichment; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts deleted file mode 100644 index 22af4dcdb9f4a..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts +++ /dev/null @@ -1,37 +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 { WrapHits, WrappedSignalHit } from './types'; -import { generateId } from './utils'; -import { buildBulkBody } from './build_bulk_body'; -import { filterDuplicateSignals } from './filter_duplicate_signals'; -import type { ConfigType } from '../../../config'; -import { CompleteRule, RuleParams } from '../schemas/rule_schemas'; - -export const wrapHitsFactory = - ({ - completeRule, - signalsIndex, - mergeStrategy, - ignoreFields, - }: { - completeRule: CompleteRule; - signalsIndex: string; - mergeStrategy: ConfigType['alertMergeStrategy']; - ignoreFields: ConfigType['alertIgnoreFields']; - }): WrapHits => - (events, buildReasonMessage) => { - const wrappedDocs: WrappedSignalHit[] = events.flatMap((doc) => [ - { - _index: signalsIndex, - _id: generateId(doc._index, doc._id, String(doc._version), completeRule.alertId ?? ''), - _source: buildBulkBody(completeRule, doc, mergeStrategy, ignoreFields, buildReasonMessage), - }, - ]); - - return filterDuplicateSignals(completeRule.alertId, wrappedDocs, false); - }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts deleted file mode 100644 index 3b93ae824849a..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts +++ /dev/null @@ -1,39 +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 { WrappedSignalHit, WrapSequences } from './types'; -import { buildSignalGroupFromSequence } from './build_bulk_body'; -import { ConfigType } from '../../../config'; -import { CompleteRule, RuleParams } from '../schemas/rule_schemas'; - -export const wrapSequencesFactory = - ({ - completeRule, - signalsIndex, - mergeStrategy, - ignoreFields, - }: { - completeRule: CompleteRule; - signalsIndex: string; - mergeStrategy: ConfigType['alertMergeStrategy']; - ignoreFields: ConfigType['alertIgnoreFields']; - }): WrapSequences => - (sequences, buildReasonMessage) => - sequences.reduce( - (acc: WrappedSignalHit[], sequence) => [ - ...acc, - ...buildSignalGroupFromSequence( - sequence, - completeRule, - signalsIndex, - mergeStrategy, - ignoreFields, - buildReasonMessage - ), - ], - [] - ); From f4ed8e118f2200b56519f44461bc964e4c83be84 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 29 Mar 2022 17:06:09 +0100 Subject: [PATCH 004/108] [ML] Testing trained models in UI (#128359) * [ML] Testing trained models in UI * folder rename * code clean up * translations * adding comments * endpoint comments * small changes based on review * removing testing text * refactoring to remove duplicate code * changing misc entities * probably is now 3 sig figs * class refactor * another refactor * fixing enitiy highlighting * adding infer timeout * show class name for known types * refactoring highlighting * moving unknown entity type * removing default badge tooltips * fixing linting error * small import changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/ml/common/types/trained_models.ts | 22 +-- .../services/ml_api_service/index.ts | 2 + .../services/ml_api_service/trained_models.ts | 21 +++ .../models_management/models_list.tsx | 21 +++ .../models_management/test_models/index.ts | 9 + .../test_models/inference_error.tsx | 30 ++++ .../test_models/models/inference_base.ts | 30 ++++ .../models/inference_input_form.tsx | 131 ++++++++++++++ .../test_models/models/lang_ident/index.ts | 10 ++ .../models/lang_ident/lang_codes.ts | 124 +++++++++++++ .../models/lang_ident/lang_ident_inference.ts | 68 +++++++ .../models/lang_ident/lang_ident_output.tsx | 86 +++++++++ .../test_models/models/ner/index.ts | 10 ++ .../test_models/models/ner/ner_inference.ts | 59 +++++++ .../test_models/models/ner/ner_output.tsx | 167 ++++++++++++++++++ .../test_models/output_loading.tsx | 17 ++ .../test_models/selected_model.tsx | 51 ++++++ .../test_models/test_flyout.tsx | 46 +++++ .../models_management/test_models/utils.ts | 18 ++ .../ml/server/lib/ml_client/ml_client.ts | 4 + x-pack/plugins/ml/server/routes/apidoc.json | 2 + .../server/routes/schemas/inference_schema.ts | 16 ++ .../ml/server/routes/trained_models.ts | 77 ++++++++ 23 files changed, 1007 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/index.ts create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/inference_error.tsx create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/inference_base.ts create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/inference_input_form.tsx create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/index.ts create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_codes.ts create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_ident_inference.ts create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_ident_output.tsx create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/index.ts create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_inference.ts create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_output.tsx create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/output_loading.tsx create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/selected_model.tsx create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/test_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/test_models/utils.ts diff --git a/x-pack/plugins/ml/common/types/trained_models.ts b/x-pack/plugins/ml/common/types/trained_models.ts index ad59b7a917c49..182cf277d93cc 100644 --- a/x-pack/plugins/ml/common/types/trained_models.ts +++ b/x-pack/plugins/ml/common/types/trained_models.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { DataFrameAnalyticsConfig } from './data_frame_analytics'; import type { FeatureImportanceBaseline, TotalFeatureImportance } from './feature_importance'; import type { XOR } from './common'; @@ -87,14 +87,12 @@ export type PutTrainedModelConfig = { } >; // compressed_definition and definition are mutually exclusive -export interface TrainedModelConfigResponse { - description?: string; - created_by: string; - create_time: string; - default_field_map: Record; - estimated_heap_memory_usage_bytes: number; - estimated_operations: number; - license_level: string; +export type TrainedModelConfigResponse = estypes.MlTrainedModelConfig & { + /** + * Associated pipelines. Extends response from the ES endpoint. + */ + pipelines?: Record | null; + metadata?: { analytics_config: DataFrameAnalyticsConfig; input: unknown; @@ -107,11 +105,7 @@ export interface TrainedModelConfigResponse { tags: string[]; version: string; inference_config?: Record; - /** - * Associated pipelines. Extends response from the ES endpoint. - */ - pipelines?: Record | null; -} +}; export interface PipelineDefinition { processors?: Array>; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 40187f70f1680..87f1a8eec2478 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -17,6 +17,7 @@ import { resultsApiProvider } from './results'; import { jobsApiProvider } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; import { savedObjectsApiProvider } from './saved_objects'; +import { trainedModelsApiProvider } from './trained_models'; import type { MlServerDefaults, MlServerLimits, @@ -719,5 +720,6 @@ export function mlApiServicesProvider(httpService: HttpService) { jobs: jobsApiProvider(httpService), fileDatavisualizer, savedObjects: savedObjectsApiProvider(httpService), + trainedModels: trainedModelsApiProvider(httpService), }; } diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts index 97027b86a88e1..738f5e1ace74a 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts @@ -5,6 +5,8 @@ * 2.0. */ +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + import { useMemo } from 'react'; import { HttpFetchQuery } from 'kibana/public'; import { HttpService } from '../http_service'; @@ -138,6 +140,25 @@ export function trainedModelsApiProvider(httpService: HttpService) { query: { force }, }); }, + + inferTrainedModel(modelId: string, payload: any, timeout?: string) { + const body = JSON.stringify(payload); + return httpService.http({ + path: `${apiBasePath}/trained_models/infer/${modelId}`, + method: 'POST', + body, + ...(timeout ? { query: { timeout } as HttpFetchQuery } : {}), + }); + }, + + ingestPipelineSimulate(payload: estypes.IngestSimulateRequest['body']) { + const body = JSON.stringify(payload); + return httpService.http({ + path: `${apiBasePath}/trained_models/ingest_pipeline_simulate`, + method: 'POST', + body, + }); + }, }; } diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx index bd3e3638e8310..1604e265b1617 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx @@ -53,6 +53,7 @@ import { DEPLOYMENT_STATE, TRAINED_MODEL_TYPE } from '../../../../common/constan import { getUserConfirmationProvider } from './force_stop_dialog'; import { MLSavedObjectsSpacesList } from '../../components/ml_saved_objects_spaces_list'; import { SavedObjectsWarning } from '../../components/saved_objects_warning'; +import { TestTrainedModelFlyout, isTestable } from './test_models'; type Stats = Omit; @@ -134,6 +135,7 @@ export const ModelsList: FC = ({ const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( {} ); + const [showTestFlyout, setShowTestFlyout] = useState(null); const getUserConfirmation = useMemo(() => getUserConfirmationProvider(overlays, theme), []); const navigateToPath = useNavigateToPath(); @@ -470,6 +472,19 @@ export const ModelsList: FC = ({ return !isPopulatedObject(item.pipelines); }, }, + { + name: i18n.translate('xpack.ml.inference.modelsList.testModelActionLabel', { + defaultMessage: 'Test model', + }), + description: i18n.translate('xpack.ml.inference.modelsList.testModelActionLabel', { + defaultMessage: 'Test model', + }), + icon: 'inputOutput', + type: 'icon', + isPrimary: true, + available: isTestable, + onClick: setShowTestFlyout, + }, ] as Array>) ); } @@ -769,6 +784,12 @@ export const ModelsList: FC = ({ modelIds={modelIdsToDelete} /> )} + {showTestFlyout === null ? null : ( + + )} ); }; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/index.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/index.ts new file mode 100644 index 0000000000000..da7c12c1c0c58 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { TestTrainedModelFlyout } from './test_flyout'; +export { isTestable } from './utils'; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/inference_error.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/inference_error.tsx new file mode 100644 index 0000000000000..dc7ae508ab270 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/inference_error.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; + +interface Props { + errorText: string | null; +} + +export const ErrorMessage: FC = ({ errorText }) => { + return errorText === null ? null : ( + <> + +

{errorText}

+
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/inference_base.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/inference_base.ts new file mode 100644 index 0000000000000..777ca2d314c4d --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/inference_base.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models'; + +const DEFAULT_INPUT_FIELD = 'text_field'; + +export type FormattedNerResp = Array<{ + value: string; + entity: estypes.MlTrainedModelEntities | null; +}>; + +export abstract class InferenceBase { + protected readonly inputField: string; + + constructor( + protected trainedModelsApi: ReturnType, + protected model: estypes.MlTrainedModelConfig + ) { + this.inputField = model.input?.field_names[0] ?? DEFAULT_INPUT_FIELD; + } + + protected abstract infer(inputText: string): Promise; +} diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/inference_input_form.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/inference_input_form.tsx new file mode 100644 index 0000000000000..6503486d98211 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/inference_input_form.tsx @@ -0,0 +1,131 @@ +/* + * 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, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiSpacer, EuiTextArea, EuiButton, EuiTabs, EuiTab } from '@elastic/eui'; + +import { LangIdentInference } from './lang_ident/lang_ident_inference'; +import { NerInference } from './ner/ner_inference'; +import type { FormattedLangIdentResp } from './lang_ident/lang_ident_inference'; +import type { FormattedNerResp } from './ner/ner_inference'; + +import { MLJobEditor } from '../../../../jobs/jobs_list/components/ml_job_editor'; +import { extractErrorMessage } from '../../../../../../common/util/errors'; +import { ErrorMessage } from '../inference_error'; +import { OutputLoadingContent } from '../output_loading'; + +interface Props { + inferrer: LangIdentInference | NerInference; + getOutputComponent(output: any): JSX.Element; +} + +enum TAB { + TEXT, + RAW, +} + +export const InferenceInputForm: FC = ({ inferrer, getOutputComponent }) => { + const [inputText, setInputText] = useState(''); + const [isRunning, setIsRunning] = useState(false); + const [output, setOutput] = useState(null); + const [rawOutput, setRawOutput] = useState(null); + const [selectedTab, setSelectedTab] = useState(TAB.TEXT); + const [showOutput, setShowOutput] = useState(false); + const [errorText, setErrorText] = useState(null); + + async function run() { + setShowOutput(true); + setOutput(null); + setRawOutput(null); + setIsRunning(true); + setErrorText(null); + try { + const { response, rawResponse } = await inferrer.infer(inputText); + setOutput(response); + setRawOutput(JSON.stringify(rawResponse, null, 2)); + } catch (e) { + setIsRunning(false); + setOutput(null); + setErrorText(extractErrorMessage(e)); + setRawOutput(JSON.stringify(e.body ?? e, null, 2)); + } + setIsRunning(false); + } + + return ( + <> + { + setInputText(e.target.value); + }} + /> + +
+ + + +
+ {showOutput === true ? ( + <> + + + + + + + + + + + + + {selectedTab === TAB.TEXT ? ( + <> + {errorText !== null ? ( + + ) : output === null ? ( + + ) : ( + <>{getOutputComponent(output)} + )} + + ) : ( + + )} + + ) : null} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/index.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/index.ts new file mode 100644 index 0000000000000..b3439d90e8828 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { FormattedLangIdentResp } from './lang_ident_inference'; +export { LangIdentInference } from './lang_ident_inference'; +export { LangIdentOutput } from './lang_ident_output'; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_codes.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_codes.ts new file mode 100644 index 0000000000000..eff2fdcdd94e7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_codes.ts @@ -0,0 +1,124 @@ +/* + * 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. + */ + +const langCodes: Record = { + af: 'Afrikaans', + hr: 'Croatian', + pa: 'Punjabi', + am: 'Amharic', + ht: 'Haitian', + pl: 'Polish', + ar: 'Arabic', + hu: 'Hungarian', + ps: 'Pashto', + az: 'Azerbaijani', + hy: 'Armenian', + pt: 'Portuguese', + be: 'Belarusian', + id: 'Indonesian', + ro: 'Romanian', + bg: 'Bulgarian', + ig: 'Igbo', + ru: 'Russian', + 'bg-Latn': 'Bulgarian', + is: 'Icelandic', + 'ru-Latn': 'Russian', + bn: 'Bengali', + it: 'Italian', + sd: 'Sindhi', + bs: 'Bosnian', + iw: 'Hebrew', + si: 'Sinhala', + ca: 'Catalan', + ja: 'Japanese', + sk: 'Slovak', + ceb: 'Cebuano', + 'ja-Latn': 'Japanese', + sl: 'Slovenian', + co: 'Corsican', + jv: 'Javanese', + sm: 'Samoan', + cs: 'Czech', + ka: 'Georgian', + sn: 'Shona', + cy: 'Welsh', + kk: 'Kazakh', + so: 'Somali', + da: 'Danish', + km: 'Central Khmer', + sq: 'Albanian', + de: 'German', + kn: 'Kannada', + sr: 'Serbian', + el: 'Greek,modern', + ko: 'Korean', + st: 'Southern Sotho', + 'el-Latn': 'Greek,modern', + ku: 'Kurdish', + su: 'Sundanese', + en: 'English', + ky: 'Kirghiz', + sv: 'Swedish', + eo: 'Esperanto', + la: 'Latin', + sw: 'Swahili', + es: 'Spanish,Castilian', + lb: 'Luxembourgish', + ta: 'Tamil', + et: 'Estonian', + lo: 'Lao', + te: 'Telugu', + eu: 'Basque', + lt: 'Lithuanian', + tg: 'Tajik', + fa: 'Persian', + lv: 'Latvian', + th: 'Thai', + fi: 'Finnish', + mg: 'Malagasy', + tr: 'Turkish', + fil: 'Filipino', + mi: 'Maori', + uk: 'Ukrainian', + fr: 'French', + mk: 'Macedonian', + ur: 'Urdu', + fy: 'Western Frisian', + ml: 'Malayalam', + uz: 'Uzbek', + ga: 'Irish', + mn: 'Mongolian', + vi: 'Vietnamese', + gd: 'Gaelic', + mr: 'Marathi', + xh: 'Xhosa', + gl: 'Galician', + ms: 'Malay', + yi: 'Yiddish', + gu: 'Gujarati', + mt: 'Maltese', + yo: 'Yoruba', + ha: 'Hausa', + my: 'Burmese', + zh: 'Chinese', + haw: 'Hawaiian', + ne: 'Nepali', + 'zh-Latn': 'Chinese', + hi: 'Hindi', + nl: 'Dutch,Flemish', + zu: 'Zulu', + 'hi-Latn': 'Hindi', + no: 'Norwegian', + hmn: 'Hmong', + ny: 'Chichewa', + + zxx: 'unknown', +}; + +export function getLanguage(code: string) { + return langCodes[code] ?? 'unknown'; +} diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_ident_inference.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_ident_inference.ts new file mode 100644 index 0000000000000..9108a59197617 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_ident_inference.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. + */ + +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { InferenceBase } from '../inference_base'; + +export type FormattedLangIdentResp = Array<{ + className: string; + classProbability: number; + classScore: number; +}>; + +interface InferResponse { + response: FormattedLangIdentResp; + rawResponse: estypes.IngestSimulateResponse; +} + +export class LangIdentInference extends InferenceBase { + public async infer(inputText: string) { + const payload: estypes.IngestSimulateRequest['body'] = { + pipeline: { + processors: [ + { + inference: { + model_id: this.model.model_id, + inference_config: { + // @ts-expect-error classification missing from type + classification: { + num_top_classes: 3, + }, + }, + field_mappings: { + contents: this.inputField, + }, + target_field: '_ml.lang_ident', + }, + }, + ], + }, + docs: [ + { + _source: { + contents: inputText, + }, + }, + ], + }; + const resp = await this.trainedModelsApi.ingestPipelineSimulate(payload); + if (resp.docs.length) { + const topClasses = resp.docs[0].doc?._source._ml?.lang_ident?.top_classes ?? []; + + return { + response: topClasses.map((t: any) => ({ + className: t.class_name, + classProbability: t.class_probability, + classScore: t.class_score, + })), + rawResponse: resp, + }; + } + return { response: [], rawResponse: resp }; + } +} diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_ident_output.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_ident_output.tsx new file mode 100644 index 0000000000000..e4968bc516f83 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/lang_ident/lang_ident_output.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 React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiBasicTable, EuiTitle } from '@elastic/eui'; + +import type { FormattedLangIdentResp } from './lang_ident_inference'; +import { getLanguage } from './lang_codes'; + +const PROBABILITY_SIG_FIGS = 3; + +export const LangIdentOutput: FC<{ result: FormattedLangIdentResp }> = ({ result }) => { + if (result.length === 0) { + return null; + } + + const lang = getLanguage(result[0].className); + + const items = result.map(({ className, classProbability }, i) => { + return { + noa: `${i + 1}`, + className: getLanguage(className), + classProbability: `${Number(classProbability).toPrecision(PROBABILITY_SIG_FIGS)}`, + }; + }); + + const columns = [ + { + field: 'noa', + name: '#', + width: '5%', + truncateText: false, + isExpander: false, + }, + { + field: 'className', + name: i18n.translate( + 'xpack.ml.trainedModels.testModelsFlyout.langIdent.output.language_title', + { + defaultMessage: 'Language', + } + ), + width: '30%', + truncateText: false, + isExpander: false, + }, + { + field: 'classProbability', + name: i18n.translate( + 'xpack.ml.trainedModels.testModelsFlyout.langIdent.output.probability_title', + { + defaultMessage: 'Probability', + } + ), + truncateText: false, + isExpander: false, + }, + ]; + + const title = + lang !== 'unknown' + ? i18n.translate('xpack.ml.trainedModels.testModelsFlyout.langIdent.output.title', { + defaultMessage: 'This looks like {lang}', + values: { lang }, + }) + : i18n.translate('xpack.ml.trainedModels.testModelsFlyout.langIdent.output.titleUnknown', { + defaultMessage: 'Language code unknown: {code}', + values: { code: result[0].className }, + }); + + return ( + <> + +

{title}

+
+ + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/index.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/index.ts new file mode 100644 index 0000000000000..38ddad8bdeb80 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { FormattedNerResp } from './ner_inference'; +export { NerInference } from './ner_inference'; +export { NerOutput } from './ner_output'; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_inference.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_inference.ts new file mode 100644 index 0000000000000..e4dcfcc2c6333 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_inference.ts @@ -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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { InferenceBase } from '../inference_base'; + +export type FormattedNerResp = Array<{ + value: string; + entity: estypes.MlTrainedModelEntities | null; +}>; + +interface InferResponse { + response: FormattedNerResp; + rawResponse: estypes.MlInferTrainedModelDeploymentResponse; +} + +export class NerInference extends InferenceBase { + public async infer(inputText: string) { + const payload = { docs: { [this.inputField]: inputText } }; + const resp = await this.trainedModelsApi.inferTrainedModel(this.model.model_id, payload, '30s'); + + return { response: parseResponse(resp), rawResponse: resp }; + } +} + +function parseResponse(resp: estypes.MlInferTrainedModelDeploymentResponse): FormattedNerResp { + const { predicted_value: predictedValue, entities } = resp; + const splitWordsAndEntitiesRegex = /(\[.*?\]\(.*?&.*?\))/; + const matchEntityRegex = /(\[.*?\])\((.*?)&(.*?)\)/; + if (predictedValue === undefined || entities === undefined) { + return []; + } + + const sentenceChunks = (predictedValue as unknown as string).split(splitWordsAndEntitiesRegex); + let count = 0; + return sentenceChunks.map((chunk) => { + const matchedEntity = chunk.match(matchEntityRegex); + if (matchedEntity) { + const entityValue = matchedEntity[3]; + const entity = entities[count]; + if (entityValue !== entity.entity && entityValue.replaceAll('+', ' ') !== entity.entity) { + // entityValue may not equal entity.entity if the entity is comprised of + // two words as they are joined with a plus symbol + // Replace any plus symbols and check again. If they still don't match, log an error + + // eslint-disable-next-line no-console + console.error('mismatch entity', entity); + } + count++; + return { value: entity.entity, entity }; + } + return { value: chunk, entity: null }; + }); +} diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_output.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_output.tsx new file mode 100644 index 0000000000000..e9db3fa8efd36 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/ner/ner_output.tsx @@ -0,0 +1,167 @@ +/* + * 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 estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import React, { FC, ReactNode } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiHorizontalRule, + EuiBadge, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, +} from '@elastic/eui'; + +import { + useCurrentEuiTheme, + EuiThemeType, +} from '../../../../../components/color_range_legend/use_color_range'; +import type { FormattedNerResp } from './ner_inference'; + +const ICON_PADDING = '2px'; +const PROBABILITY_SIG_FIGS = 3; + +const ENTITY_TYPES = { + PER: { + label: 'Person', + icon: 'user', + color: 'euiColorVis5_behindText', + borderColor: 'euiColorVis5', + }, + LOC: { + label: 'Location', + icon: 'visMapCoordinate', + color: 'euiColorVis1_behindText', + borderColor: 'euiColorVis1', + }, + ORG: { + label: 'Organization', + icon: 'home', + color: 'euiColorVis0_behindText', + borderColor: 'euiColorVis0', + }, + MISC: { + label: 'Miscellaneous', + icon: 'questionInCircle', + color: 'euiColorVis7_behindText', + borderColor: 'euiColorVis7', + }, +}; + +const UNKNOWN_ENTITY_TYPE = { + label: '', + icon: 'questionInCircle', + color: 'euiColorVis5_behindText', + borderColor: 'euiColorVis5', +}; + +export const NerOutput: FC<{ result: FormattedNerResp }> = ({ result }) => { + const { euiTheme } = useCurrentEuiTheme(); + const lineSplit: JSX.Element[] = []; + result.forEach(({ value, entity }) => { + if (entity === null) { + const lines = value + .split(/(\n)/) + .map((line) => (line === '\n' ?
: {line})); + + lineSplit.push(...lines); + } else { + lineSplit.push( + +
+ + {value} +
+ +
+
+ + : {getClassLabel(entity.class_name)} +
+
+ + : {Number(entity.class_probability).toPrecision(PROBABILITY_SIG_FIGS)} +
+
+
+ } + > + {value} + + ); + } + }); + return
{lineSplit}
; +}; + +const EntityBadge = ({ + entity, + children, +}: { + entity: estypes.MlTrainedModelEntities; + children: ReactNode; +}) => { + const { euiTheme } = useCurrentEuiTheme(); + return ( + + + + + + {children} + + + ); +}; + +function getClassIcon(className: string) { + const entity = ENTITY_TYPES[className as keyof typeof ENTITY_TYPES]; + return entity?.icon ?? UNKNOWN_ENTITY_TYPE.icon; +} + +function getClassLabel(className: string) { + const entity = ENTITY_TYPES[className as keyof typeof ENTITY_TYPES]; + return entity?.label ?? className; +} + +function getClassColor(euiTheme: EuiThemeType, className: string, border: boolean = false) { + const entity = ENTITY_TYPES[className as keyof typeof ENTITY_TYPES]; + let color = entity?.color ?? UNKNOWN_ENTITY_TYPE.color; + if (border) { + color = entity?.borderColor ?? UNKNOWN_ENTITY_TYPE.borderColor; + } + return euiTheme[color as keyof typeof euiTheme]; +} diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/output_loading.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/output_loading.tsx new file mode 100644 index 0000000000000..4cceed23edd25 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/output_loading.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { EuiLoadingContent } from '@elastic/eui'; +import { LineRange } from '@elastic/eui/src/components/loading/loading_content'; + +export const OutputLoadingContent: FC<{ text: string }> = ({ text }) => { + const actualLines = text.split(/\r\n|\r|\n/).length + 1; + const lines = actualLines > 4 && actualLines <= 10 ? actualLines : 4; + + return ; +}; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/selected_model.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/selected_model.tsx new file mode 100644 index 0000000000000..cab0826d5584a --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/selected_model.tsx @@ -0,0 +1,51 @@ +/* + * 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 estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import React, { FC } from 'react'; + +import { NerOutput, NerInference } from './models/ner'; +import type { FormattedNerResp } from './models/ner'; +import { LangIdentOutput, LangIdentInference } from './models/lang_ident'; +import type { FormattedLangIdentResp } from './models/lang_ident'; + +import { TRAINED_MODEL_TYPE } from '../../../../../common/constants/trained_models'; +import { useMlApiContext } from '../../../contexts/kibana'; +import { InferenceInputForm } from './models/inference_input_form'; + +interface Props { + model: estypes.MlTrainedModelConfig | null; +} + +export const SelectedModel: FC = ({ model }) => { + const { trainedModels } = useMlApiContext(); + + if (model === null) { + return null; + } + + if (model.model_type === TRAINED_MODEL_TYPE.PYTORCH) { + const inferrer = new NerInference(trainedModels, model); + return ( + } + /> + ); + } + if (model.model_type === TRAINED_MODEL_TYPE.LANG_IDENT) { + const inferrer = new LangIdentInference(trainedModels, model); + return ( + } + /> + ); + } + + return null; +}; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/test_flyout.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/test_flyout.tsx new file mode 100644 index 0000000000000..343cd32addce7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/test_flyout.tsx @@ -0,0 +1,46 @@ +/* + * 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 estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import React, { FC } from 'react'; + +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiSpacer } from '@elastic/eui'; + +import { SelectedModel } from './selected_model'; + +interface Props { + model: estypes.MlTrainedModelConfig; + onClose: () => void; +} +export const TestTrainedModelFlyout: FC = ({ model, onClose }) => { + return ( + <> + + + +

+ +

+
+
+ + +

{model.model_id}

+
+ + + + +
+
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/utils.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/utils.ts new file mode 100644 index 0000000000000..ccddd960349d2 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/utils.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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { TRAINED_MODEL_TYPE } from '../../../../../common/constants/trained_models'; + +const TESTABLE_MODEL_TYPES: estypes.MlTrainedModelType[] = [ + TRAINED_MODEL_TYPE.PYTORCH, + TRAINED_MODEL_TYPE.LANG_IDENT, +]; + +export function isTestable(model: estypes.MlTrainedModelConfig) { + return model.model_type && TESTABLE_MODEL_TYPES.includes(model.model_type); +} diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts index 342a3913a6cba..122162777d9a5 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts @@ -494,6 +494,10 @@ export function getMlClient( await modelIdsCheck(p); return mlClient.stopTrainedModelDeployment(...p); }, + async inferTrainedModelDeployment(...p: Parameters) { + await modelIdsCheck(p); + return mlClient.inferTrainedModelDeployment(...p); + }, async info(...p: Parameters) { return mlClient.info(...p); }, diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 59ed08664da3b..ac09aee7fcbb9 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -171,6 +171,8 @@ "StopTrainedModelDeployment", "PutTrainedModel", "DeleteTrainedModel", + "InferTrainedModelDeployment", + "IngestPipelineSimulate", "Alerting", "PreviewAlert" diff --git a/x-pack/plugins/ml/server/routes/schemas/inference_schema.ts b/x-pack/plugins/ml/server/routes/schemas/inference_schema.ts index 941edb31c79fa..1b9a865dcfca9 100644 --- a/x-pack/plugins/ml/server/routes/schemas/inference_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/inference_schema.ts @@ -30,3 +30,19 @@ export const getInferenceQuerySchema = schema.object({ export const putTrainedModelQuerySchema = schema.object({ defer_definition_decompression: schema.maybe(schema.boolean()), }); + +export const pipelineSchema = schema.object({ + pipeline: schema.object({ + description: schema.maybe(schema.string()), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), + }), + docs: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + verbose: schema.maybe(schema.boolean()), +}); + +export const inferTrainedModelQuery = schema.object({ timeout: schema.maybe(schema.string()) }); +export const inferTrainedModelBody = schema.object({ + docs: schema.any(), +}); diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts index 887ad47f1ceb2..27a062b45767c 100644 --- a/x-pack/plugins/ml/server/routes/trained_models.ts +++ b/x-pack/plugins/ml/server/routes/trained_models.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { schema } from '@kbn/config-schema'; import { RouteInitialization } from '../types'; import { wrapError } from '../client/error_wrapper'; @@ -13,6 +14,9 @@ import { modelIdSchema, optionalModelIdSchema, putTrainedModelQuerySchema, + pipelineSchema, + inferTrainedModelQuery, + inferTrainedModelBody, } from './schemas/inference_schema'; import { modelsProvider } from '../models/data_frame_analytics'; import { TrainedModelConfigResponse } from '../../common/types/trained_models'; @@ -352,4 +356,77 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) } }) ); + + /** + * @apiGroup TrainedModels + * + * @api {post} /api/ml/trained_models/infer/:modelId Evaluates a trained model + * @apiName InferTrainedModelDeployment + * @apiDescription Evaluates a trained model. + */ + router.post( + { + path: '/api/ml/trained_models/infer/{modelId}', + validate: { + params: modelIdSchema, + query: inferTrainedModelQuery, + body: inferTrainedModelBody, + }, + options: { + tags: ['access:ml:canStartStopTrainedModels'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { + try { + const { modelId } = request.params; + const body = await mlClient.inferTrainedModelDeployment({ + model_id: modelId, + docs: request.body.docs, + ...(request.query.timeout ? { timeout: request.query.timeout } : {}), + }); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup TrainedModels + * + * @api {post} /api/ml/trained_models/ingest_pipeline_simulate Ingest pipeline simulate + * @apiName IngestPipelineSimulate + * @apiDescription Simulates an ingest pipeline call using supplied documents + */ + router.post( + { + path: '/api/ml/trained_models/ingest_pipeline_simulate', + validate: { + body: pipelineSchema, + }, + options: { + tags: ['access:ml:canStartStopTrainedModels'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, request, response }) => { + try { + const { pipeline, docs, verbose } = request.body; + + const body = await client.asCurrentUser.ingest.simulate({ + verbose, + body: { + pipeline, + docs: docs as estypes.IngestSimulateDocument[], + }, + }); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } From 261819208dc9573f544933cf607dde9c98cf33dd Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 29 Mar 2022 19:27:48 +0300 Subject: [PATCH 005/108] [TSVB] fixes steps behavior to happen at the change point (#128741) --- .../timeseries/utils/__snapshots__/series_styles.test.js.snap | 2 +- .../visualizations/views/timeseries/utils/series_styles.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap index 8f0d602e09c7a..054ca0f0d8193 100644 --- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap @@ -20,7 +20,7 @@ Object { "visible": true, }, }, - "curve": 6, + "curve": 7, } `; diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js index a1e85d17bc5fa..a3076a30f69ed 100644 --- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js @@ -29,7 +29,7 @@ export const getAreaStyles = ({ points, lines, color }) => ({ visible: points.lineWidth > 0 && Boolean(points.show), }, }, - curve: lines.steps ? CurveType.CURVE_STEP : CurveType.LINEAR, + curve: lines.steps ? CurveType.CURVE_STEP_AFTER : CurveType.LINEAR, }); export const getBarStyles = ({ show = true, lineWidth = 0, fill = 1 }, color) => ({ From 0181e5aeb96eeae3f126cfb33d290d49a1142dc6 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Tue, 29 Mar 2022 17:27:55 +0100 Subject: [PATCH 006/108] add link to ilm docs (#128656) --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + .../step_define_package_policy.tsx | 28 +++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index c31aaab76e2ad..1d9406ac37447 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -584,6 +584,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { elasticAgent: `${FLEET_DOCS}elastic-agent-installation.html`, beatsAgentComparison: `${FLEET_DOCS}beats-agent-comparison.html`, datastreams: `${FLEET_DOCS}data-streams.html`, + datastreamsILM: `${FLEET_DOCS}data-streams.html#data-streams-ilm`, datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`, installElasticAgent: `${FLEET_DOCS}install-fleet-managed-elastic-agent.html`, installElasticAgentStandalone: `${FLEET_DOCS}install-standalone-elastic-agent.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index a5875ce03cf5b..c2e485e1003e6 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -345,6 +345,7 @@ export interface DocLinks { troubleshooting: string; elasticAgent: string; datastreams: string; + datastreamsILM: string; datastreamsNamingScheme: string; installElasticAgent: string; installElasticAgentStandalone: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 7fcddf4439557..7f67452e2f230 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -318,6 +318,34 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ /> + + + } + helpText={ + + {i18n.translate( + 'xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyDataRetentionLearnMoreLink', + { defaultMessage: 'Learn more' } + )} + + ), + }} + /> + } + > +
+ + {/* Advanced vars */} {advancedVars.map((varDef) => { const { name: varName, type: varType } = varDef; From 7039878471f33ba276e5838914f5f42c1bf816b7 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 29 Mar 2022 18:30:10 +0200 Subject: [PATCH 007/108] [Lens] Remove unused and duplicated code for XY chart expression (#128716) * [Lens] remove unused and duplicated code * lowering the limits for page bundle --- packages/kbn-optimizer/limits.yml | 2 +- .../public/components/annotations.tsx | 132 +------------ .../public/components/reference_lines.tsx | 178 ++++------------- .../xy_chart_renderer.tsx | 8 +- .../public/helpers/annotations.tsx | 181 ++++++++++++------ 5 files changed, 170 insertions(+), 331 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 25656a3977fea..e137bd0055900 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -124,5 +124,5 @@ pageLoadAssetSize: sessionView: 77750 cloudSecurityPosture: 19109 visTypeGauge: 24113 - expressionXY: 41392 + expressionXY: 26500 eventAnnotation: 19334 diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index 4e8fa1b95775f..e8b99a36664df 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -18,18 +18,12 @@ import { Position, } from '@elastic/charts'; import moment from 'moment'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui'; -import classnames from 'classnames'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { EventAnnotationArgs } from '../../../../event_annotation/common'; import type { FieldFormat } from '../../../../field_formats/common'; import { defaultAnnotationColor } from '../../../../../../src/plugins/event_annotation/public'; -import type { - AnnotationLayerArgs, - AnnotationLayerConfigResult, - IconPosition, - YAxisMode, -} from '../../common/types'; -import { annotationsIconSet, hasIcon, isNumericalString } from '../helpers'; +import type { AnnotationLayerArgs, AnnotationLayerConfigResult } from '../../common/types'; +import { AnnotationIcon, hasIcon, Marker, MarkerBody } from '../helpers'; import { mapVerticalToHorizontalPlacement, LINES_MARKER_SIZE } from '../helpers'; const getRoundedTimestamp = (timestamp: number, firstTimestamp?: number, minInterval?: number) => { @@ -235,123 +229,3 @@ export const Annotations = ({ ); }; - -export function MarkerBody({ - label, - isHorizontal, -}: { - label: string | undefined; - isHorizontal: boolean; -}) { - if (!label) { - return null; - } - if (isHorizontal) { - return ( -
- {label} -
- ); - } - return ( -
-
- {label} -
-
- ); -} - -function NumberIcon({ number }: { number: number }) { - return ( - - - {number < 10 ? number : `9+`} - - - ); -} - -export const AnnotationIcon = ({ - type, - rotateClassName = '', - isHorizontal, - renderedInChart, - ...rest -}: { - type: string; - rotateClassName?: string; - isHorizontal?: boolean; - renderedInChart?: boolean; -} & EuiIconProps) => { - if (isNumericalString(type)) { - return ; - } - const iconConfig = annotationsIconSet.find((i) => i.value === type); - if (!iconConfig) { - return null; - } - return ( - - ); -}; - -interface MarkerConfig { - axisMode?: YAxisMode; - icon?: string; - textVisibility?: boolean; - iconPosition?: IconPosition; -} - -export function Marker({ - config, - isHorizontal, - hasReducedPadding, - label, - rotateClassName, -}: { - config: MarkerConfig; - isHorizontal: boolean; - hasReducedPadding: boolean; - label?: string; - rotateClassName?: string; -}) { - if (hasIcon(config.icon)) { - return ( - - ); - } - - // if there's some text, check whether to show it as marker, or just show some padding for the icon - if (config.textVisibility) { - if (hasReducedPadding) { - return ; - } - return ; - } - return null; -} diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx index 65bc91c06efe5..d71e56bcef6d0 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx @@ -10,15 +10,17 @@ import './reference_lines.scss'; import React from 'react'; import { groupBy } from 'lodash'; -import { EuiIcon } from '@elastic/eui'; import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts'; import { euiLightVars } from '@kbn/ui-theme'; import type { FieldFormat } from '../../../../field_formats/common'; import type { IconPosition, ReferenceLineLayerArgs, YAxisMode } from '../../common/types'; import type { LensMultiTable } from '../../common/types'; -import { hasIcon } from '../helpers'; - -export const REFERENCE_LINE_MARKER_SIZE = 20; +import { + LINES_MARKER_SIZE, + mapVerticalToHorizontalPlacement, + Marker, + MarkerBody, +} from '../helpers'; export const computeChartMargins = ( referenceLinePaddings: Partial>, @@ -54,66 +56,24 @@ export const computeChartMargins = ( return result; }; -// Note: it does not take into consideration whether the reference line is in view or not -export const getReferenceLineRequiredPaddings = ( - referenceLineLayers: ReferenceLineLayerArgs[], - axesMap: Record<'left' | 'right', unknown> -) => { - // collect all paddings for the 4 axis: if any text is detected double it. - const paddings: Partial> = {}; - const icons: Partial> = {}; - referenceLineLayers.forEach((layer) => { - layer.yConfig?.forEach(({ axisMode, icon, iconPosition, textVisibility }) => { - if (axisMode && (hasIcon(icon) || textVisibility)) { - const placement = getBaseIconPlacement(iconPosition, axisMode, axesMap); - paddings[placement] = Math.max( - paddings[placement] || 0, - REFERENCE_LINE_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text - ); - icons[placement] = (icons[placement] || 0) + (hasIcon(icon) ? 1 : 0); - } - }); - }); - // post-process the padding based on the icon presence: - // if no icon is present for the placement, just reduce the padding - (Object.keys(paddings) as Position[]).forEach((placement) => { - if (!icons[placement]) { - paddings[placement] = REFERENCE_LINE_MARKER_SIZE; - } - }); - - return paddings; -}; - -function mapVerticalToHorizontalPlacement(placement: Position) { - switch (placement) { - case Position.Top: - return Position.Right; - case Position.Bottom: - return Position.Left; - case Position.Left: - return Position.Bottom; - case Position.Right: - return Position.Top; - } -} - // if there's just one axis, put it on the other one // otherwise use the same axis // this function assume the chart is vertical -function getBaseIconPlacement( +export function getBaseIconPlacement( iconPosition: IconPosition | undefined, - axisMode: YAxisMode | undefined, - axesMap: Record + axesMap?: Record, + axisMode?: YAxisMode ) { if (iconPosition === 'auto') { if (axisMode === 'bottom') { return Position.Top; } - if (axisMode === 'left') { - return axesMap.right ? Position.Left : Position.Right; + if (axesMap) { + if (axisMode === 'left') { + return axesMap.right ? Position.Left : Position.Right; + } + return axesMap.left ? Position.Right : Position.Left; } - return axesMap.left ? Position.Right : Position.Left; } if (iconPosition === 'left') { @@ -128,65 +88,6 @@ function getBaseIconPlacement( return Position.Top; } -function getMarkerBody(label: string | undefined, isHorizontal: boolean) { - if (!label) { - return; - } - if (isHorizontal) { - return ( -
- {label} -
- ); - } - return ( -
-
- {label} -
-
- ); -} - -interface MarkerConfig { - axisMode?: YAxisMode; - icon?: string; - textVisibility?: boolean; -} - -function getMarkerToShow( - markerConfig: MarkerConfig, - label: string | undefined, - isHorizontal: boolean, - hasReducedPadding: boolean -) { - // show an icon if present - if (hasIcon(markerConfig.icon)) { - return ; - } - // if there's some text, check whether to show it as marker, or just show some padding for the icon - if (markerConfig.textVisibility) { - if (hasReducedPadding) { - return getMarkerBody( - label, - (!isHorizontal && markerConfig.axisMode === 'bottom') || - (isHorizontal && markerConfig.axisMode !== 'bottom') - ); - } - return ; - } -} - export interface ReferenceLineAnnotationsProps { layers: ReferenceLineLayerArgs[]; data: LensMultiTable; @@ -243,27 +144,34 @@ export const ReferenceLineAnnotations = ({ // get the position for vertical chart const markerPositionVertical = getBaseIconPlacement( yConfig.iconPosition, - yConfig.axisMode, - axesMap + axesMap, + yConfig.axisMode ); // the padding map is built for vertical chart - const hasReducedPadding = - paddingMap[markerPositionVertical] === REFERENCE_LINE_MARKER_SIZE; + const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; const props = { groupId, - marker: getMarkerToShow( - yConfig, - columnToLabelMap[yConfig.forAccessor], - isHorizontal, - hasReducedPadding + marker: ( + ), - markerBody: getMarkerBody( - yConfig.textVisibility && !hasReducedPadding - ? columnToLabelMap[yConfig.forAccessor] - : undefined, - (!isHorizontal && yConfig.axisMode === 'bottom') || - (isHorizontal && yConfig.axisMode !== 'bottom') + markerBody: ( + ), // rotate the position if required markerPosition: isHorizontal @@ -272,17 +180,15 @@ export const ReferenceLineAnnotations = ({ }; const annotations = []; - const dashStyle = - yConfig.lineStyle === 'dashed' - ? [(yConfig.lineWidth || 1) * 3, yConfig.lineWidth || 1] - : yConfig.lineStyle === 'dotted' - ? [yConfig.lineWidth || 1, yConfig.lineWidth || 1] - : undefined; - const sharedStyle = { strokeWidth: yConfig.lineWidth || 1, stroke: yConfig.color || defaultColor, - dash: dashStyle, + dash: + yConfig.lineStyle === 'dashed' + ? [(yConfig.lineWidth || 1) * 3, yConfig.lineWidth || 1] + : yConfig.lineStyle === 'dotted' + ? [yConfig.lineWidth || 1, yConfig.lineWidth || 1] + : undefined, }; annotations.push( diff --git a/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx index 70acc25330b87..5e4921e85af62 100644 --- a/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx @@ -11,14 +11,14 @@ import { I18nProvider } from '@kbn/i18n-react'; import { ThemeServiceStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; -import { ChartsPluginStart, PaletteRegistry } from '../../../../charts/public'; +import type { ChartsPluginStart, PaletteRegistry } from '../../../../charts/public'; import { EventAnnotationServiceType } from '../../../../event_annotation/public'; import { ExpressionRenderDefinition } from '../../../../expressions'; import { FormatFactory } from '../../../../field_formats/common'; import { KibanaThemeProvider } from '../../../../kibana_react/public'; -import { XYChartProps } from '../../common'; -import { calculateMinInterval } from '../helpers'; -import { BrushEvent, FilterEvent } from '../types'; +import type { XYChartProps } from '../../common'; +import { calculateMinInterval } from '../helpers/interval'; +import type { BrushEvent, FilterEvent } from '../types'; export type GetStartDepsFn = () => Promise<{ formatFactory: FormatFactory; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx index 8da38af10f5d9..5035855647147 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx @@ -5,46 +5,17 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import React from 'react'; import { Position } from '@elastic/charts'; +import { EuiFlexGroup, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui'; +import classnames from 'classnames'; import type { IconPosition, YAxisMode, YConfig } from '../../common/types'; +import { getBaseIconPlacement } from '../components'; import { hasIcon } from './icon'; +import { annotationsIconSet } from './annotations_icon_set'; export const LINES_MARKER_SIZE = 20; -export const computeChartMargins = ( - referenceLinePaddings: Partial>, - labelVisibility: Partial>, - titleVisibility: Partial>, - axesMap: Record<'left' | 'right', unknown>, - isHorizontal: boolean -) => { - const result: Partial> = {}; - if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; - result[placement] = referenceLinePaddings.bottom; - } - if ( - referenceLinePaddings.left && - (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; - result[placement] = referenceLinePaddings.left; - } - if ( - referenceLinePaddings.right && - (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; - result[placement] = referenceLinePaddings.right; - } - // there's no top axis, so just check if a margin has been computed - if (referenceLinePaddings.top) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; - result[placement] = referenceLinePaddings.top; - } - return result; -}; - // Note: it does not take into consideration whether the reference line is in view or not export const getLinesCausedPaddings = ( @@ -93,36 +64,124 @@ export function mapVerticalToHorizontalPlacement(placement: Position) { } } -// if there's just one axis, put it on the other one -// otherwise use the same axis -// this function assume the chart is vertical -export function getBaseIconPlacement( - iconPosition: IconPosition | undefined, - axesMap?: Record, - axisMode?: YAxisMode -) { - if (iconPosition === 'auto') { - if (axisMode === 'bottom') { - return Position.Top; - } - if (axesMap) { - if (axisMode === 'left') { - return axesMap.right ? Position.Left : Position.Right; - } - return axesMap.left ? Position.Right : Position.Left; - } +export function MarkerBody({ + label, + isHorizontal, +}: { + label: string | undefined; + isHorizontal: boolean; +}) { + if (!label) { + return null; } - - if (iconPosition === 'left') { - return Position.Left; + if (isHorizontal) { + return ( +
+ {label} +
+ ); } - if (iconPosition === 'right') { - return Position.Right; + return ( +
+
+ {label} +
+
+ ); +} + +function NumberIcon({ number }: { number: number }) { + return ( + + + {number < 10 ? number : `9+`} + + + ); +} + +const isNumericalString = (value: string) => !isNaN(Number(value)); + +export const AnnotationIcon = ({ + type, + rotateClassName = '', + isHorizontal, + renderedInChart, + ...rest +}: { + type: string; + rotateClassName?: string; + isHorizontal?: boolean; + renderedInChart?: boolean; +} & EuiIconProps) => { + if (isNumericalString(type)) { + return ; } - if (iconPosition === 'below') { - return Position.Bottom; + const iconConfig = annotationsIconSet.find((i) => i.value === type); + if (!iconConfig) { + return null; } - return Position.Top; + return ( + + ); +}; + +interface MarkerConfig { + axisMode?: YAxisMode; + icon?: string; + textVisibility?: boolean; + iconPosition?: IconPosition; } -export const isNumericalString = (value: string) => !isNaN(Number(value)); +export function Marker({ + config, + isHorizontal, + hasReducedPadding, + label, + rotateClassName, +}: { + config: MarkerConfig; + isHorizontal: boolean; + hasReducedPadding: boolean; + label?: string; + rotateClassName?: string; +}) { + if (hasIcon(config.icon)) { + return ( + + ); + } + + // if there's some text, check whether to show it as marker, or just show some padding for the icon + if (config.textVisibility) { + if (hasReducedPadding) { + return ; + } + return ; + } + return null; +} From c3fba606d26b3c136cab2450aa30747beb00de6a Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 29 Mar 2022 10:07:51 -0700 Subject: [PATCH 008/108] [DOCS] Add get case comments API (#128694) --- docs/api/cases.asciidoc | 4 +- .../cases/cases-api-delete-comments.asciidoc | 6 +- .../api/cases/cases-api-get-comments.asciidoc | 80 +++++++++++++++++++ docs/api/cases/cases-api-update.asciidoc | 3 +- 4 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 docs/api/cases/cases-api-get-comments.asciidoc diff --git a/docs/api/cases.asciidoc b/docs/api/cases.asciidoc index 273f3a0b51cc2..a92951d7d6d1a 100644 --- a/docs/api/cases.asciidoc +++ b/docs/api/cases.asciidoc @@ -13,9 +13,8 @@ these APIs: * {security-guide}/cases-api-find-cases-by-alert.html[Find cases by alert] * <> * {security-guide}/cases-api-get-case-activity.html[Get all case activity] -* {security-guide}/cases-api-get-all-case-comments.html[Get all case comments] * <> -* {security-guide}/cases-api-get-comment.html[Get comment] +* <> * {security-guide}/cases-get-connector.html[Get current connector] * {security-guide}/cases-api-get-reporters.html[Get reporters] * {security-guide}/cases-api-get-status.html[Get status] @@ -36,5 +35,6 @@ include::cases/cases-api-find-cases.asciidoc[leveloffset=+1] include::cases/cases-api-find-connectors.asciidoc[leveloffset=+1] //GET include::cases/cases-api-get-case.asciidoc[leveloffset=+1] +include::cases/cases-api-get-comments.asciidoc[leveloffset=+1] //UPDATE include::cases/cases-api-update.asciidoc[leveloffset=+1] diff --git a/docs/api/cases/cases-api-delete-comments.asciidoc b/docs/api/cases/cases-api-delete-comments.asciidoc index 66421944ac1be..0b02786e6659d 100644 --- a/docs/api/cases/cases-api-delete-comments.asciidoc +++ b/docs/api/cases/cases-api-delete-comments.asciidoc @@ -30,9 +30,9 @@ You must have `all` privileges for the *Cases* feature in the *Management*, <>. ``:: -(Optional, string) The identifier for the comment. -//To retrieve comment IDs, use <>. -If it is not specified, all comments are deleted. +(Optional, string) The identifier for the comment. To retrieve comment IDs, use +<> or <>. If it is not specified, all +comments are deleted. :: (Optional, string) An identifier for the space. If it is not specified, the diff --git a/docs/api/cases/cases-api-get-comments.asciidoc b/docs/api/cases/cases-api-get-comments.asciidoc new file mode 100644 index 0000000000000..6e88b6ffdf004 --- /dev/null +++ b/docs/api/cases/cases-api-get-comments.asciidoc @@ -0,0 +1,80 @@ +[[cases-api-get-comments]] +== Get comments API +++++ +Get comments +++++ + +Gets a comment or all comments for a case. + +=== Request + +`GET :/api/cases//comments/` + +`GET :/s//api/cases//comments/` + +`GET :/api/cases//comments` deprecated:[8.1.0] + +`GET :/s//api/cases//comments` deprecated:[8.1.0] + +=== Prerequisite + +You must have `read` privileges for the *Cases* feature in the *Management*, +*{observability}*, or *Security* section of the +<>, depending on the +`owner` of the cases with the comments you're seeking. + +=== Path parameters + +``:: +(Required, string) The identifier for the case. To retrieve case IDs, use +<>. + +``:: +(Optional, string) The identifier for the comment. To retrieve comment IDs, use +<>. ++ +If it is not specified, all comments are retrieved. +deprecated:[8.1.0,The comment identifier will no longer be optional.] + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +Retrieves comment ID `71ec1870-725b-11ea-a0b2-c51ea50a58e2` from case ID +`a18b38a0-71b0-11ea-a0b2-c51ea50a58e2`: + +[source,sh] +-------------------------------------------------- +GET api/cases/a18b38a0-71b0-11ea-a0b2-c51ea50a58e2/comments/71ec1870-725b-11ea-a0b2-c51ea50a58e2 +-------------------------------------------------- +// KIBANA + +The API returns the requested comment JSON object. For example: + +[source,json] +-------------------------------------------------- +{ + "id":"8acb3a80-ab0a-11ec-985f-97e55adae8b9", + "version":"Wzc5NzYsM10=", + "comment":"Start operation bubblegum immediately! And chew fast!", + "type":"user", + "owner":"cases", + "created_at":"2022-03-24T00:37:10.832Z", + "created_by": { + "email": "classified@hms.oo.gov.uk", + "full_name": "Classified", + "username": "M" + }, + "pushed_at": null, + "pushed_by": null, + "updated_at": null, + "updated_by": null +} +-------------------------------------------------- \ No newline at end of file diff --git a/docs/api/cases/cases-api-update.asciidoc b/docs/api/cases/cases-api-update.asciidoc index ed0ef069e15f4..522300591d8b7 100644 --- a/docs/api/cases/cases-api-update.asciidoc +++ b/docs/api/cases/cases-api-update.asciidoc @@ -156,8 +156,7 @@ and `open`. (Optional, string) A title for the case. `version`:: -(Required, string) The current version of the case. -//To determine this value, use <> or <> +(Required, string) The current version of the case. To determine this value, use <> or <>. ==== === Response code From da0294cd5a60ea80e0afe13ffd900106288d219e Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 29 Mar 2022 18:11:40 +0100 Subject: [PATCH 009/108] [ML] Changing ML management page title (#128767) * [ML] Changing ML management page title * updating sync callout text * reverting link id change to fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../saved_objects_warning/saved_objects_warning.tsx | 4 ++-- x-pack/plugins/ml/public/application/management/index.ts | 2 +- .../jobs_list/components/jobs_list_page/jobs_list_page.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/saved_objects_warning/saved_objects_warning.tsx b/x-pack/plugins/ml/public/application/components/saved_objects_warning/saved_objects_warning.tsx index ccbf0c1082d0f..9e5123fa275f2 100644 --- a/x-pack/plugins/ml/public/application/components/saved_objects_warning/saved_objects_warning.tsx +++ b/x-pack/plugins/ml/public/application/components/saved_objects_warning/saved_objects_warning.tsx @@ -108,7 +108,7 @@ export const SavedObjectsWarning: FC = ({ ), @@ -117,7 +117,7 @@ export const SavedObjectsWarning: FC = ({ ) : ( )} {showSyncFlyout && } diff --git a/x-pack/plugins/ml/public/application/management/index.ts b/x-pack/plugins/ml/public/application/management/index.ts index 9d6376a668c1d..9dd71f915e03e 100644 --- a/x-pack/plugins/ml/public/application/management/index.ts +++ b/x-pack/plugins/ml/public/application/management/index.ts @@ -22,7 +22,7 @@ export function registerManagementSection( return management.sections.section.insightsAndAlerting.registerApp({ id: 'jobsListLink', title: i18n.translate('xpack.ml.management.jobsListTitle', { - defaultMessage: 'Machine Learning Jobs', + defaultMessage: 'Machine Learning', }), order: 2, async mount(params: ManagementAppMountParams) { diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 5620902ee768b..e107e5bfcdfe6 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -271,13 +271,13 @@ export const JobsListPage: FC<{ pageTitle={ } description={ } rightSideItems={[docsLink]} From 14b0cde1ca04e134c265cff973500aed12e80e93 Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Tue, 29 Mar 2022 19:27:59 +0200 Subject: [PATCH 010/108] [Security Solution][Endpoint] Show event filter duplicate fields warning (#128736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Show warning on duplicate field entries Since conditions are conjuntive (ANDed) it implies having multiple entries of the idential fields with different values would result in an ineffective exception. For instance a (a ^ ¬a) would never be true. fixes elastic/security-team/issues/3199 * typo --- .../view/components/form/index.test.tsx | 62 ++++++++++++++++++- .../view/components/form/index.tsx | 26 ++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx index b5d9f1fd2cecf..96cdef21244d0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx @@ -202,7 +202,7 @@ describe('Event filter form', () => { expect(component.getByTestId('effectedPolicies-select-policiesSelectable')).toBeTruthy(); }); - it('should call onChange when a policy is selected from the policy selectiion', async () => { + it('should call onChange when a policy is selected from the policy selection', async () => { component = await renderWithData(); const policyId = policiesRequest.items[0].id; @@ -274,4 +274,64 @@ describe('Event filter form', () => { expect(component.queryByTestId('effectedPolicies-select-policiesSelectable')).toBeFalsy(); expect(getState().form.entry?.tags).toEqual([`policy:all`]); }); + + it('should not show warning text when unique fields are added', async () => { + component = await renderWithData({ + entries: [ + { + field: 'event.category', + operator: 'included', + type: 'match', + value: 'some value', + }, + { + field: 'file.name', + operator: 'excluded', + type: 'match', + value: 'some other value', + }, + ], + }); + expect(component.queryByTestId('duplicate-fields-warning-message')).toBeNull(); + }); + + it('should not show warning text when field values are not added', async () => { + component = await renderWithData({ + entries: [ + { + field: 'event.category', + operator: 'included', + type: 'match', + value: '', + }, + { + field: 'event.category', + operator: 'excluded', + type: 'match', + value: '', + }, + ], + }); + expect(component.queryByTestId('duplicate-fields-warning-message')).toBeNull(); + }); + + it('should show warning text when duplicate fields are added with values', async () => { + component = await renderWithData({ + entries: [ + { + field: 'event.category', + operator: 'included', + type: 'match', + value: 'some value', + }, + { + field: 'event.category', + operator: 'excluded', + type: 'match', + value: 'some other value', + }, + ], + }); + expect(component.queryByTestId('duplicate-fields-warning-message')).not.toBeNull(); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index 6d24b9558ea53..027d6e2bacfa6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -69,6 +69,18 @@ const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ OperatingSystem.LINUX, ]; +const getAddedFieldsCounts = (formFields: string[]): { [k: string]: number } => + formFields.reduce<{ [k: string]: number }>((allFields, field) => { + if (field in allFields) { + allFields[field]++; + } else { + allFields[field] = 1; + } + return allFields; + }, {}); + +const computeHasDuplicateFields = (formFieldsList: Record): boolean => + Object.values(formFieldsList).some((e) => e > 1); interface EventFiltersFormProps { allowSelectOs?: boolean; policies: PolicyData[]; @@ -85,6 +97,7 @@ export const EventFiltersForm: React.FC = memo( const [hasBeenInputNameVisited, setHasBeenInputNameVisited] = useState(false); const isPlatinumPlus = useLicense().isPlatinumPlus(); const [hasFormChanged, setHasFormChanged] = useState(false); + const [hasDuplicateFields, setHasDuplicateFields] = useState(false); // This value has to be memoized to avoid infinite useEffect loop on useFetchIndex const indexNames = useMemo(() => ['logs-endpoint.events.*'], []); @@ -131,6 +144,8 @@ export const EventFiltersForm: React.FC = memo( (!hasFormChanged && arg.exceptionItems[0] === undefined) || isEqual(arg.exceptionItems[0]?.entries, exception?.entries) ) { + const addedFields = arg.exceptionItems[0]?.entries.map((e) => e.field) || ['']; + setHasDuplicateFields(computeHasDuplicateFields(getAddedFieldsCounts(addedFields))); setHasFormChanged(true); return; } @@ -446,6 +461,17 @@ export const EventFiltersForm: React.FC = memo( {detailsSection} {criteriaSection} + {hasDuplicateFields && ( + <> + + + + + + )} {showAssignmentSection && ( <> {policiesSection} From 6c27fb8c2ad52041fbc3a59a35d7f5a7616651b6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 29 Mar 2022 20:32:45 +0300 Subject: [PATCH 011/108] [Cases] Add more e2e tests (#128347) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 2 + .../all_cases/all_cases_list.test.tsx | 2 +- .../public/components/all_cases/columns.tsx | 2 +- .../public/components/all_cases/header.tsx | 2 +- .../all_cases/table_filters.test.tsx | 12 +- .../cases/public/components/app/index.tsx | 2 +- .../components/filter_popover/index.tsx | 2 +- .../integration/cases/creation.spec.ts | 4 +- .../cypress/screens/all_cases.ts | 4 +- x-pack/test/functional/services/cases/api.ts | 11 +- .../test/functional/services/cases/common.ts | 142 ++---------- .../test/functional/services/cases/create.ts | 63 ++++++ .../test/functional/services/cases/index.ts | 6 + x-pack/test/functional/services/cases/list.ts | 121 +++++++++++ .../functional/services/cases/navigation.ts | 25 +++ .../apps/cases/configure.ts | 52 +++++ .../apps/cases/create_case_form.ts | 8 +- .../apps/cases/edit_case_form.ts | 169 --------------- .../apps/cases/index.ts | 3 +- .../apps/cases/list_view.ts | 204 ++++++++++++------ .../apps/cases/view_case.ts | 192 +++++++++++++++++ x-pack/test/functional_with_es_ssl/config.ts | 3 + 22 files changed, 655 insertions(+), 376 deletions(-) create mode 100644 x-pack/test/functional/services/cases/create.ts create mode 100644 x-pack/test/functional/services/cases/list.ts create mode 100644 x-pack/test/functional/services/cases/navigation.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/cases/configure.ts delete mode 100644 x-pack/test/functional_with_es_ssl/apps/cases/edit_case_form.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1d4eb2ba39cd5..c89fe8be4a654 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -353,6 +353,8 @@ #CC# /x-pack/plugins/stack_alerts @elastic/response-ops /x-pack/plugins/cases/ @elastic/response-ops /x-pack/test/cases_api_integration/ @elastic/response-ops +/x-pack/test/functional/services/cases/ @elastic/response-ops +/x-pack/test/functional_with_es_ssl/apps/cases/ @elastic/response-ops # Enterprise Search /x-pack/plugins/enterprise_search @elastic/enterprise-search-frontend diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 43a36188fcf52..88aad5fb64408 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -189,7 +189,7 @@ describe('AllCasesListGeneric', () => { useGetCasesMockState.data.cases[0].title ); expect( - wrapper.find(`span[data-test-subj="case-table-column-tags-0"]`).first().prop('title') + wrapper.find(`span[data-test-subj="case-table-column-tags-coke"]`).first().prop('title') ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); expect(wrapper.find(`[data-test-subj="case-table-column-createdBy"]`).first().text()).toEqual( useGetCasesMockState.data.cases[0].createdBy.username diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index f92f1605c4c51..a05673d3e095a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -230,7 +230,7 @@ export const useCasesColumns = ({ {tag} diff --git a/x-pack/plugins/cases/public/components/all_cases/header.tsx b/x-pack/plugins/cases/public/components/all_cases/header.tsx index 19a1a897221e7..4e66083711e2b 100644 --- a/x-pack/plugins/cases/public/components/all_cases/header.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/header.tsx @@ -42,7 +42,7 @@ export const CasesTableHeader: FunctionComponent = ({ refresh, userCanCrud, }) => ( - + {userCanCrud ? ( <> diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx index 8089c85ee578b..b6dc66e6dee49 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx @@ -64,7 +64,7 @@ describe('CasesTableFilters ', () => { ); wrapper.find(`[data-test-subj="options-filter-popover-button-Tags"]`).last().simulate('click'); - wrapper.find(`[data-test-subj="options-filter-popover-item-0"]`).last().simulate('click'); + wrapper.find(`[data-test-subj="options-filter-popover-item-coke"]`).last().simulate('click'); expect(onFilterChanged).toBeCalledWith({ tags: ['coke'] }); }); @@ -80,7 +80,10 @@ describe('CasesTableFilters ', () => { .last() .simulate('click'); - wrapper.find(`[data-test-subj="options-filter-popover-item-0"]`).last().simulate('click'); + wrapper + .find(`[data-test-subj="options-filter-popover-item-casetester"]`) + .last() + .simulate('click'); expect(onFilterChanged).toBeCalledWith({ reporters: [{ username: 'casetester' }] }); }); @@ -212,7 +215,10 @@ describe('CasesTableFilters ', () => { .last() .simulate('click'); - wrapper.find(`[data-test-subj="options-filter-popover-item-0"]`).last().simulate('click'); + wrapper + .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`) + .last() + .simulate('click'); expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] }); }); diff --git a/x-pack/plugins/cases/public/components/app/index.tsx b/x-pack/plugins/cases/public/components/app/index.tsx index ba2a61ec6691f..c98c40ed9ba78 100644 --- a/x-pack/plugins/cases/public/components/app/index.tsx +++ b/x-pack/plugins/cases/public/components/app/index.tsx @@ -19,7 +19,7 @@ const CasesAppComponent: React.FC = () => { const userCapabilities = useApplicationCapabilities(); return ( - + {getCasesLazy({ owner: [APP_OWNER], useFetchAlertData: () => [false, {}], diff --git a/x-pack/plugins/cases/public/components/filter_popover/index.tsx b/x-pack/plugins/cases/public/components/filter_popover/index.tsx index c28d00b08912a..bd4f665bcd7fd 100644 --- a/x-pack/plugins/cases/public/components/filter_popover/index.tsx +++ b/x-pack/plugins/cases/public/components/filter_popover/index.tsx @@ -91,7 +91,7 @@ export const FilterPopoverComponent = ({ {options.map((option, index) => ( diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts index 32121537aacdd..d13f97d6cd6f3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts @@ -87,8 +87,8 @@ describe('Cases', () => { cy.get(ALL_CASES_TAGS_COUNT).should('have.text', 'Tags2'); cy.get(ALL_CASES_NAME).should('have.text', this.mycase.name); cy.get(ALL_CASES_REPORTER).should('have.text', this.mycase.reporter); - (this.mycase as TestCase).tags.forEach((tag, index) => { - cy.get(ALL_CASES_TAGS(index)).should('have.text', tag); + (this.mycase as TestCase).tags.forEach((tag) => { + cy.get(ALL_CASES_TAGS(tag)).should('have.text', tag); }); cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', '0'); cy.get(ALL_CASES_OPENED_ON).should('include.text', 'ago'); diff --git a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts index 58b496a651f3b..3f5bcb912ee44 100644 --- a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts +++ b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts @@ -44,8 +44,8 @@ export const ALL_CASES_SERVICE_NOW_INCIDENT = export const ALL_CASES_IN_PROGRESS_STATUS = '[data-test-subj="status-badge-in-progress"]'; -export const ALL_CASES_TAGS = (index: number) => { - return `[data-test-subj="case-table-column-tags-${index}"]`; +export const ALL_CASES_TAGS = (tag: string) => { + return `[data-test-subj="case-table-column-tags-${tag}"]`; }; export const ALL_CASES_TAGS_COUNT = '[data-test-subj="options-filter-popover-button-Tags"]'; diff --git a/x-pack/test/functional/services/cases/api.ts b/x-pack/test/functional/services/cases/api.ts index bacb08cd19b2d..e863504c2add3 100644 --- a/x-pack/test/functional/services/cases/api.ts +++ b/x-pack/test/functional/services/cases/api.ts @@ -7,7 +7,10 @@ import pMap from 'p-map'; import { CasePostRequest } from '../../../../plugins/cases/common/api'; -import { createCase, deleteAllCaseItems } from '../../../cases_api_integration/common/lib/utils'; +import { + createCase as createCaseAPI, + deleteAllCaseItems, +} from '../../../cases_api_integration/common/lib/utils'; import { FtrProviderContext } from '../../ftr_provider_context'; import { generateRandomCaseWithoutConnector } from './helpers'; @@ -16,12 +19,12 @@ export function CasesAPIServiceProvider({ getService }: FtrProviderContext) { const es = getService('es'); return { - async createCaseWithData(overwrites: { title?: string } = {}) { + async createCase(overwrites: Partial = {}) { const caseData = { ...generateRandomCaseWithoutConnector(), ...overwrites, } as CasePostRequest; - await createCase(kbnSupertest, caseData); + await createCaseAPI(kbnSupertest, caseData); }, async createNthRandomCases(amount: number = 3) { @@ -32,7 +35,7 @@ export function CasesAPIServiceProvider({ getService }: FtrProviderContext) { await pMap( cases, (caseData) => { - return createCase(kbnSupertest, caseData); + return createCaseAPI(kbnSupertest, caseData); }, { concurrency: 4 } ); diff --git a/x-pack/test/functional/services/cases/common.ts b/x-pack/test/functional/services/cases/common.ts index ad5fbb7be7233..bf5b8e6e8b0be 100644 --- a/x-pack/test/functional/services/cases/common.ts +++ b/x-pack/test/functional/services/cases/common.ts @@ -6,15 +6,14 @@ */ import expect from '@kbn/expect'; -import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; -import uuid from 'uuid'; +import { CaseStatuses } from '../../../../plugins/cases/common'; import { FtrProviderContext } from '../../ftr_provider_context'; export function CasesCommonServiceProvider({ getService, getPageObject }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); - const comboBox = getService('comboBox'); const header = getPageObject('header'); + return { /** * Opens the create case page pressing the "create case" button. @@ -29,125 +28,11 @@ export function CasesCommonServiceProvider({ getService, getPageObject }: FtrPro }); }, - /** - * it creates a new case from the create case page - * and leaves the navigation in the case view page - * - * Doesn't do navigation. Only works if you are already inside a cases app page. - * Does not work with the cases flyout. - */ - async createCaseFromCreateCasePage({ - title = 'test-' + uuid.v4(), - description = 'desc' + uuid.v4(), - tag = 'tagme', - }: { - title: string; - description: string; - tag: string; - }) { - await this.openCreateCasePage(); - - // case name - await testSubjects.setValue('input', title); - - // case tag - await comboBox.setCustom('comboBoxInput', tag); - - // case description - const descriptionArea = await find.byCssSelector('textarea.euiMarkdownEditorTextArea'); - await descriptionArea.focus(); - await descriptionArea.type(description); - - // save - await testSubjects.click('create-case-submit'); - - await testSubjects.existOrFail('case-view-title'); - }, - - /** - * Goes to the first case listed on the table. - * - * This will fail if the table doesn't have any case - */ - async goToFirstListedCase() { - await testSubjects.existOrFail('cases-table'); - await testSubjects.click('case-details-link'); - await testSubjects.existOrFail('case-view-title'); - }, - - /** - * Marks a case in progress via the status dropdown - */ - async markCaseInProgressViaDropdown() { - await this.openCaseSetStatusDropdown(); - - await testSubjects.click('case-view-status-dropdown-in-progress'); - - // wait for backend response - await testSubjects.existOrFail('header-page-supplements > status-badge-in-progress', { - timeout: 5000, - }); - }, - - /** - * Marks a case closed via the status dropdown - */ - async markCaseClosedViaDropdown() { - this.openCaseSetStatusDropdown(); - - await testSubjects.click('case-view-status-dropdown-closed'); - - // wait for backend response - await testSubjects.existOrFail('header-page-supplements > status-badge-closed', { - timeout: 5000, - }); - }, - - /** - * Marks a case open via the status dropdown - */ - async markCaseOpenViaDropdown() { + async changeCaseStatusViaDropdownAndVerify(status: CaseStatuses) { this.openCaseSetStatusDropdown(); - - await testSubjects.click('case-view-status-dropdown-open'); - - // wait for backend response - await testSubjects.existOrFail('header-page-supplements > status-badge-open', { - timeout: 5000, - }); - }, - - async bulkDeleteAllCases() { - await testSubjects.setCheckbox('checkboxSelectAll', 'check'); - const button = await find.byCssSelector('[aria-label="Bulk actions"]'); - await button.click(); - await testSubjects.click('cases-bulk-delete-button'); - await testSubjects.click('confirmModalConfirmButton'); - }, - - async selectAndDeleteAllCases() { + await testSubjects.click(`case-view-status-dropdown-${status}`); await header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('cases-table', { timeout: 20 * 1000 }); - let rows: WebElementWrapper[]; - do { - await header.waitUntilLoadingHasFinished(); - await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); - rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100); - if (rows.length > 0) { - await this.bulkDeleteAllCases(); - // wait for a second - await new Promise((r) => setTimeout(r, 1000)); - await header.waitUntilLoadingHasFinished(); - } - } while (rows.length > 0); - }, - - async validateCasesTableHasNthRows(nrRows: number) { - await header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('cases-table', { timeout: 20 * 1000 }); - await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); - const rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100); - expect(rows.length).equal(nrRows); + await testSubjects.existOrFail(`status-badge-${status}`); }, async openCaseSetStatusDropdown() { @@ -156,5 +41,22 @@ export function CasesCommonServiceProvider({ getService, getPageObject }: FtrPro ); await button.click(); }, + + async assertRadioGroupValue(testSubject: string, expectedValue: string) { + const assertRadioGroupValue = await testSubjects.find(testSubject); + const input = await assertRadioGroupValue.findByCssSelector(':checked'); + const selectedOptionId = await input.getAttribute('id'); + expect(selectedOptionId).to.eql( + expectedValue, + `Expected the radio group value to equal "${expectedValue}" (got "${selectedOptionId}")` + ); + }, + + async selectRadioGroupValue(testSubject: string, value: string) { + const radioGroup = await testSubjects.find(testSubject); + const label = await radioGroup.findByCssSelector(`label[for="${value}"]`); + await label.click(); + await this.assertRadioGroupValue(testSubject, value); + }, }; } diff --git a/x-pack/test/functional/services/cases/create.ts b/x-pack/test/functional/services/cases/create.ts new file mode 100644 index 0000000000000..f1a54aec75438 --- /dev/null +++ b/x-pack/test/functional/services/cases/create.ts @@ -0,0 +1,63 @@ +/* + * 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 uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function CasesCreateViewServiceProvider({ getService, getPageObject }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const comboBox = getService('comboBox'); + + return { + /** + * Opens the create case page pressing the "create case" button. + * + * Doesn't do navigation. Only works if you are already inside a cases app page. + * Does not work with the cases flyout. + */ + async openCreateCasePage() { + await testSubjects.click('createNewCaseBtn'); + await testSubjects.existOrFail('create-case-submit', { + timeout: 5000, + }); + }, + + /** + * it creates a new case from the create case page + * and leaves the navigation in the case view page + * + * Doesn't do navigation. Only works if you are already inside a cases app page. + * Does not work with the cases flyout. + */ + async createCaseFromCreateCasePage({ + title = 'test-' + uuid.v4(), + description = 'desc' + uuid.v4(), + tag = 'tagme', + }: { + title: string; + description: string; + tag: string; + }) { + // case name + await testSubjects.setValue('input', title); + + // case tag + await comboBox.setCustom('comboBoxInput', tag); + + // case description + const descriptionArea = await find.byCssSelector('textarea.euiMarkdownEditorTextArea'); + await descriptionArea.focus(); + await descriptionArea.type(description); + + // save + await testSubjects.click('create-case-submit'); + + await testSubjects.existOrFail('case-view-title'); + }, + }; +} diff --git a/x-pack/test/functional/services/cases/index.ts b/x-pack/test/functional/services/cases/index.ts index afe244a21842e..886750c4ebfab 100644 --- a/x-pack/test/functional/services/cases/index.ts +++ b/x-pack/test/functional/services/cases/index.ts @@ -8,10 +8,16 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { CasesAPIServiceProvider } from './api'; import { CasesCommonServiceProvider } from './common'; +import { CasesCreateViewServiceProvider } from './create'; +import { CasesTableServiceProvider } from './list'; +import { CasesNavigationProvider } from './navigation'; export function CasesServiceProvider(context: FtrProviderContext) { return { api: CasesAPIServiceProvider(context), common: CasesCommonServiceProvider(context), + casesTable: CasesTableServiceProvider(context), + create: CasesCreateViewServiceProvider(context), + navigation: CasesNavigationProvider(context), }; } diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts new file mode 100644 index 0000000000000..87e2fdcb91edc --- /dev/null +++ b/x-pack/test/functional/services/cases/list.ts @@ -0,0 +1,121 @@ +/* + * 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 { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; +import { CaseStatuses } from '../../../../plugins/cases/common'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function CasesTableServiceProvider({ getService, getPageObject }: FtrProviderContext) { + const common = getPageObject('common'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const header = getPageObject('header'); + const retry = getService('retry'); + + return { + /** + * Goes to the first case listed on the table. + * + * This will fail if the table doesn't have any case + */ + async goToFirstListedCase() { + await testSubjects.existOrFail('cases-table'); + await testSubjects.click('case-details-link'); + await testSubjects.existOrFail('case-view-title'); + }, + + async bulkDeleteAllCases() { + await testSubjects.setCheckbox('checkboxSelectAll', 'check'); + const button = await find.byCssSelector('[aria-label="Bulk actions"]'); + await button.click(); + await testSubjects.click('cases-bulk-delete-button'); + await testSubjects.click('confirmModalConfirmButton'); + }, + + async selectAndDeleteAllCases() { + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('cases-table', { timeout: 20 * 1000 }); + let rows: WebElementWrapper[]; + do { + await header.waitUntilLoadingHasFinished(); + await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); + rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100); + if (rows.length > 0) { + await this.bulkDeleteAllCases(); + // wait for a second + await new Promise((r) => setTimeout(r, 1000)); + await header.waitUntilLoadingHasFinished(); + } + } while (rows.length > 0); + }, + + async validateCasesTableHasNthRows(nrRows: number) { + await retry.tryForTime(2000, async () => { + const rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100); + expect(rows.length).equal(nrRows); + }); + }, + + async waitForCasesToBeListed() { + await retry.waitFor('cases to appear on the all cases table', async () => { + this.refreshTable(); + return await testSubjects.exists('case-details-link'); + }); + }, + + async waitForCasesToBeDeleted() { + await retry.waitFor('the cases table to be empty', async () => { + this.refreshTable(); + const rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100); + return rows.length === 0; + }); + }, + + async waitForTableToFinishLoading() { + await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); + }, + + async getCaseFromTable(index: number) { + const rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100); + + if (index > rows.length) { + throw new Error('Cannot get case from table. Index is greater than the length of all rows'); + } + + return rows[index] ?? null; + }, + + async filterByTag(tag: string) { + await common.clickAndValidate( + 'options-filter-popover-button-Tags', + `options-filter-popover-item-${tag}` + ); + + await testSubjects.click(`options-filter-popover-item-${tag}`); + }, + + async filterByStatus(status: CaseStatuses) { + await common.clickAndValidate('case-status-filter', `case-status-filter-${status}`); + + await testSubjects.click(`case-status-filter-${status}`); + }, + + async filterByReporter(reporter: string) { + await common.clickAndValidate( + 'options-filter-popover-button-Reporter', + `options-filter-popover-item-${reporter}` + ); + + await testSubjects.click(`options-filter-popover-item-${reporter}`); + }, + + async refreshTable() { + await testSubjects.click('all-cases-refresh'); + }, + }; +} diff --git a/x-pack/test/functional/services/cases/navigation.ts b/x-pack/test/functional/services/cases/navigation.ts new file mode 100644 index 0000000000000..4aca20c01aaf1 --- /dev/null +++ b/x-pack/test/functional/services/cases/navigation.ts @@ -0,0 +1,25 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function CasesNavigationProvider({ getPageObject, getService }: FtrProviderContext) { + const common = getPageObject('common'); + const testSubjects = getService('testSubjects'); + + return { + async navigateToApp() { + await common.navigateToApp('cases'); + await testSubjects.existOrFail('cases-app', { timeout: 2000 }); + }, + + async navigateToConfigurationPage() { + await this.navigateToApp(); + await common.clickAndValidate('configure-case-button', 'case-configure-title'); + }, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/configure.ts b/x-pack/test/functional_with_es_ssl/apps/cases/configure.ts new file mode 100644 index 0000000000000..0c826baa252b1 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/cases/configure.ts @@ -0,0 +1,52 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObject, getService }: FtrProviderContext) => { + const common = getPageObject('common'); + const testSubjects = getService('testSubjects'); + const cases = getService('cases'); + const toasts = getService('toasts'); + + describe('Configure', function () { + before(async () => { + await cases.navigation.navigateToConfigurationPage(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + }); + + describe('Closure options', function () { + it('defaults the closure option correctly', async () => { + await cases.common.assertRadioGroupValue('closure-options-radio-group', 'close-by-user'); + }); + + it('change closure option successfully', async () => { + await cases.common.selectRadioGroupValue('closure-options-radio-group', 'close-by-pushing'); + const toast = await toasts.getToastElement(1); + expect(await toast.getVisibleText()).to.be('Saved external connection settings'); + await toasts.dismissAllToasts(); + }); + }); + + describe('Connectors', function () { + it('defaults the connector to none correctly', async () => { + expect(await testSubjects.exists('dropdown-connector-no-connector')).to.be(true); + }); + + it('opens and closes the connectors flyout correctly', async () => { + await common.clickAndValidate('dropdown-connectors', 'dropdown-connector-add-connector'); + await common.clickAndValidate('dropdown-connector-add-connector', 'euiFlyoutCloseButton'); + await testSubjects.click('euiFlyoutCloseButton'); + expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false); + }); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts index 252f639feef48..c5aed361aba3e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts @@ -9,15 +9,14 @@ import expect from '@kbn/expect'; import uuid from 'uuid'; import { FtrProviderContext } from '../../ftr_provider_context'; -export default ({ getPageObject, getService }: FtrProviderContext) => { +export default ({ getService }: FtrProviderContext) => { describe('Create case', function () { - const common = getPageObject('common'); const find = getService('find'); const cases = getService('cases'); const testSubjects = getService('testSubjects'); before(async () => { - await common.navigateToApp('cases'); + await cases.navigation.navigateToApp(); }); after(async () => { @@ -26,7 +25,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { it('creates a case from the stack management page', async () => { const caseTitle = 'test-' + uuid.v4(); - await cases.common.createCaseFromCreateCasePage({ + await cases.create.openCreateCasePage(); + await cases.create.createCaseFromCreateCasePage({ title: caseTitle, description: 'test description', tag: 'tagme', diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/edit_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/edit_case_form.ts deleted file mode 100644 index adc7c3401aa96..0000000000000 --- a/x-pack/test/functional_with_es_ssl/apps/cases/edit_case_form.ts +++ /dev/null @@ -1,169 +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 expect from '@kbn/expect'; -import uuid from 'uuid'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default ({ getPageObject, getService }: FtrProviderContext) => { - const common = getPageObject('common'); - const header = getPageObject('header'); - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const cases = getService('cases'); - const retry = getService('retry'); - const comboBox = getService('comboBox'); - - describe('Edit case', () => { - // create the case to test on - before(async () => { - await common.navigateToApp('cases'); - await cases.api.createNthRandomCases(1); - }); - - after(async () => { - await cases.api.deleteAllCases(); - }); - - beforeEach(async () => { - await common.navigateToApp('cases'); - await cases.common.goToFirstListedCase(); - await header.waitUntilLoadingHasFinished(); - }); - - it('edits a case title from the case view page', async () => { - const newTitle = `test-${uuid.v4()}`; - - await testSubjects.click('editable-title-edit-icon'); - await testSubjects.setValue('editable-title-input-field', newTitle); - await testSubjects.click('editable-title-submit-btn'); - - // wait for backend response - await retry.tryForTime(5000, async () => { - const title = await find.byCssSelector('[data-test-subj="header-page-title"]'); - expect(await title.getVisibleText()).equal(newTitle); - }); - - // validate user action - await find.byCssSelector('[data-test-subj*="title-update-action"]'); - }); - - it('adds a comment to a case', async () => { - const commentArea = await find.byCssSelector( - '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' - ); - await commentArea.focus(); - await commentArea.type('Test comment from automation'); - await testSubjects.click('submit-comment'); - - // validate user action - const newComment = await find.byCssSelector( - '[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]' - ); - expect(await newComment.getVisibleText()).equal('Test comment from automation'); - }); - - it('adds a tag to a case', async () => { - const tag = uuid.v4(); - await testSubjects.click('tag-list-edit-button'); - await comboBox.setCustom('comboBoxInput', tag); - await testSubjects.click('edit-tags-submit'); - - // validate tag was added - await testSubjects.existOrFail('tag-' + tag); - - // validate user action - await find.byCssSelector('[data-test-subj*="tags-add-action"]'); - }); - - it('deletes a tag from a case', async () => { - await testSubjects.click('tag-list-edit-button'); - // find the tag button and click the close button - const button = await find.byCssSelector('[data-test-subj="comboBoxInput"] button'); - await button.click(); - await testSubjects.click('edit-tags-submit'); - - // validate user action - await find.byCssSelector('[data-test-subj*="tags-delete-action"]'); - }); - - it('changes a case status to in-progress via dropdown menu', async () => { - await cases.common.markCaseInProgressViaDropdown(); - // validate user action - await find.byCssSelector( - '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-in-progress"]' - ); - // validates dropdown tag - await testSubjects.existOrFail('case-view-status-dropdown > status-badge-in-progress'); - }); - - it('changes a case status to closed via dropdown-menu', async () => { - await cases.common.markCaseClosedViaDropdown(); - - // validate user action - await find.byCssSelector( - '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-closed"]' - ); - // validates dropdown tag - await testSubjects.existOrFail('case-view-status-dropdown > status-badge-closed'); - }); - - it("reopens a case from the 'reopen case' button", async () => { - await cases.common.markCaseClosedViaDropdown(); - await header.waitUntilLoadingHasFinished(); - await testSubjects.click('case-view-status-action-button'); - await header.waitUntilLoadingHasFinished(); - - await testSubjects.existOrFail('header-page-supplements > status-badge-open', { - timeout: 5000, - }); - - // validate user action - await find.byCssSelector( - '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-open"]' - ); - // validates dropdown tag - await testSubjects.existOrFail('case-view-status-dropdown > status-badge-open'); - }); - - it("marks in progress a case from the 'mark in progress' button", async () => { - await cases.common.markCaseOpenViaDropdown(); - await header.waitUntilLoadingHasFinished(); - await testSubjects.click('case-view-status-action-button'); - await header.waitUntilLoadingHasFinished(); - - await testSubjects.existOrFail('header-page-supplements > status-badge-in-progress', { - timeout: 5000, - }); - - // validate user action - await find.byCssSelector( - '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-in-progress"]' - ); - // validates dropdown tag - await testSubjects.existOrFail('case-view-status-dropdown > status-badge-in-progress'); - }); - - it("closes a case from the 'close case' button", async () => { - await cases.common.markCaseInProgressViaDropdown(); - await header.waitUntilLoadingHasFinished(); - await testSubjects.click('case-view-status-action-button'); - await header.waitUntilLoadingHasFinished(); - - await testSubjects.existOrFail('header-page-supplements > status-badge-closed', { - timeout: 5000, - }); - - // validate user action - await find.byCssSelector( - '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-closed"]' - ); - // validates dropdown tag - await testSubjects.existOrFail('case-view-status-dropdown > status-badge-closed'); - }); - }); -}; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/index.ts b/x-pack/test/functional_with_es_ssl/apps/cases/index.ts index 583fce960fbbd..53d2c2d9767f1 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/index.ts @@ -11,7 +11,8 @@ export default ({ loadTestFile }: FtrProviderContext) => { describe('Cases', function () { this.tags('ciGroup27'); loadTestFile(require.resolve('./create_case_form')); - loadTestFile(require.resolve('./edit_case_form')); + loadTestFile(require.resolve('./view_case')); loadTestFile(require.resolve('./list_view')); + loadTestFile(require.resolve('./configure')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts index 40fd69246710b..c1ec21b694b49 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts @@ -5,118 +5,190 @@ * 2.0. */ +import expect from '@kbn/expect'; import uuid from 'uuid'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { CaseStatuses } from '../../../../plugins/cases/common'; export default ({ getPageObject, getService }: FtrProviderContext) => { - const common = getPageObject('common'); const header = getPageObject('header'); const testSubjects = getService('testSubjects'); const cases = getService('cases'); const retry = getService('retry'); const browser = getService('browser'); - // Failing: See https://github.com/elastic/kibana/issues/128468 - describe.skip('cases list', () => { + describe('cases list', () => { before(async () => { - await common.navigateToApp('cases'); - await cases.api.deleteAllCases(); + await cases.navigation.navigateToApp(); }); after(async () => { await cases.api.deleteAllCases(); + await cases.casesTable.waitForCasesToBeDeleted(); }); - beforeEach(async () => { - await common.navigateToApp('cases'); + describe('empty state', () => { + it('displays an empty list with an add button correctly', async () => { + await testSubjects.existOrFail('cases-table-add-case'); + }); }); - it('displays an empty list with an add button correctly', async () => { - await testSubjects.existOrFail('cases-table-add-case'); - }); + describe('listing', () => { + before(async () => { + await cases.api.createNthRandomCases(2); + await header.waitUntilLoadingHasFinished(); + await cases.casesTable.waitForCasesToBeListed(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + await cases.casesTable.waitForCasesToBeDeleted(); + }); - it('lists cases correctly', async () => { - const NUMBER_CASES = 2; - await cases.api.createNthRandomCases(NUMBER_CASES); - await common.navigateToApp('cases'); - await cases.common.validateCasesTableHasNthRows(NUMBER_CASES); + it('lists cases correctly', async () => { + await cases.casesTable.validateCasesTableHasNthRows(2); + }); }); - it('deletes a case correctly from the list', async () => { - await cases.api.createNthRandomCases(1); - await common.navigateToApp('cases'); - await testSubjects.click('action-delete'); - await testSubjects.click('confirmModalConfirmButton'); - await testSubjects.existOrFail('euiToastHeader'); + describe('deleting', () => { + before(async () => { + await cases.api.createNthRandomCases(8); + await cases.api.createCase({ title: 'delete me', tags: ['one'] }); + await header.waitUntilLoadingHasFinished(); + await cases.casesTable.waitForCasesToBeListed(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + await cases.casesTable.waitForCasesToBeDeleted(); + }); + + it('deletes a case correctly from the list', async () => { + await testSubjects.click('action-delete'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.existOrFail('euiToastHeader'); + await cases.casesTable.waitForTableToFinishLoading(); + + await retry.tryForTime(2000, async () => { + const firstRow = await testSubjects.find('case-details-link'); + expect(await firstRow.getVisibleText()).not.to.be('delete me'); + }); + }); + + it('bulk delete cases from the list', async () => { + await cases.casesTable.selectAndDeleteAllCases(); + await cases.casesTable.waitForTableToFinishLoading(); + await cases.casesTable.validateCasesTableHasNthRows(0); + }); }); - it('filters cases from the list with partial match', async () => { - await cases.api.deleteAllCases(); - await cases.api.createNthRandomCases(5); + describe('filtering', () => { const id = uuid.v4(); const caseTitle = 'matchme-' + id; - await cases.api.createCaseWithData({ title: caseTitle }); - await common.navigateToApp('cases'); - await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); - // search - const input = await testSubjects.find('search-cases'); - await input.type(caseTitle); - await input.pressKeys(browser.keys.ENTER); + before(async () => { + await cases.api.createNthRandomCases(2); + await cases.api.createCase({ title: caseTitle, tags: ['one'] }); + await cases.api.createCase({ tags: ['two'] }); + await header.waitUntilLoadingHasFinished(); + await cases.casesTable.waitForCasesToBeListed(); + }); - await retry.tryForTime(20000, async () => { - await cases.common.validateCasesTableHasNthRows(1); + beforeEach(async () => { + /** + * There is no easy way to clear the filtering. + * Refreshing the page seems to be easier. + */ + await cases.navigation.navigateToApp(); }); - }); - it('paginates cases correctly', async () => { - await cases.api.deleteAllCases(); - await cases.api.createNthRandomCases(8); - await common.navigateToApp('cases'); - await testSubjects.click('tablePaginationPopoverButton'); - await testSubjects.click('tablePagination-5-rows'); - await testSubjects.isEnabled('pagination-button-1'); - await testSubjects.click('pagination-button-1'); - await testSubjects.isEnabled('pagination-button-0'); + after(async () => { + await cases.api.deleteAllCases(); + await cases.casesTable.waitForCasesToBeDeleted(); + }); + + it('filters cases from the list with partial match', async () => { + await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); + + // search + const input = await testSubjects.find('search-cases'); + await input.type(caseTitle); + await input.pressKeys(browser.keys.ENTER); + + await cases.casesTable.validateCasesTableHasNthRows(1); + await testSubjects.click('clearSearchButton'); + await cases.casesTable.validateCasesTableHasNthRows(4); + }); + + it('filters cases by tags', async () => { + await cases.casesTable.filterByTag('one'); + await cases.casesTable.refreshTable(); + await cases.casesTable.validateCasesTableHasNthRows(1); + const row = await cases.casesTable.getCaseFromTable(0); + const tags = await row.findByCssSelector('[data-test-subj="case-table-column-tags-one"]'); + expect(await tags.getVisibleText()).to.be('one'); + }); + + it('filters cases by status', async () => { + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); + await cases.casesTable.filterByStatus(CaseStatuses['in-progress']); + await cases.casesTable.validateCasesTableHasNthRows(1); + }); + + /** + * TODO: Improve the test by creating a case from a + * different user and filter by the new user + * and not the default one + */ + it('filters cases by reporter', async () => { + await cases.casesTable.filterByReporter('elastic'); + await cases.casesTable.validateCasesTableHasNthRows(4); + }); }); - it('bulk delete cases from the list', async () => { - // deletes them from the API - await cases.api.deleteAllCases(); - await cases.api.createNthRandomCases(8); - await common.navigateToApp('cases'); - // deletes them from the UI - await cases.common.selectAndDeleteAllCases(); - await cases.common.validateCasesTableHasNthRows(0); + describe('pagination', () => { + before(async () => { + await cases.api.createNthRandomCases(8); + await header.waitUntilLoadingHasFinished(); + await cases.casesTable.waitForCasesToBeListed(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + await cases.casesTable.waitForCasesToBeDeleted(); + }); + + it('paginates cases correctly', async () => { + await testSubjects.click('tablePaginationPopoverButton'); + await testSubjects.click('tablePagination-5-rows'); + await testSubjects.isEnabled('pagination-button-1'); + await testSubjects.click('pagination-button-1'); + await testSubjects.isEnabled('pagination-button-0'); + }); }); describe('changes status from the list', () => { before(async () => { - await common.navigateToApp('cases'); - await cases.api.deleteAllCases(); await cases.api.createNthRandomCases(1); - await common.navigateToApp('cases'); + await header.waitUntilLoadingHasFinished(); + await cases.casesTable.waitForCasesToBeListed(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + await cases.casesTable.waitForCasesToBeDeleted(); }); it('to in progress', async () => { - await cases.common.openCaseSetStatusDropdown(); - await testSubjects.click('case-view-status-dropdown-in-progress'); - await header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('status-badge-in-progress'); + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); }); it('to closed', async () => { - await cases.common.openCaseSetStatusDropdown(); - await testSubjects.click('case-view-status-dropdown-closed'); - await header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('status-badge-closed'); + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses.closed); }); it('to open', async () => { - await cases.common.openCaseSetStatusDropdown(); - await testSubjects.click('case-view-status-dropdown-open'); - await header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('status-badge-open'); + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses.open); }); }); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts new file mode 100644 index 0000000000000..8c14e7e1263ee --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts @@ -0,0 +1,192 @@ +/* + * 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 uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { CaseStatuses } from '../../../../plugins/cases/common'; + +export default ({ getPageObject, getService }: FtrProviderContext) => { + const common = getPageObject('common'); + const header = getPageObject('header'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const cases = getService('cases'); + const retry = getService('retry'); + const comboBox = getService('comboBox'); + + describe('View case', () => { + describe('properties', () => { + // create the case to test on + before(async () => { + await cases.navigation.navigateToApp(); + await cases.api.createNthRandomCases(1); + await cases.casesTable.waitForCasesToBeListed(); + await cases.casesTable.goToFirstListedCase(); + await header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + }); + + it('edits a case title from the case view page', async () => { + const newTitle = `test-${uuid.v4()}`; + + await testSubjects.click('editable-title-edit-icon'); + await testSubjects.setValue('editable-title-input-field', newTitle); + await testSubjects.click('editable-title-submit-btn'); + + // wait for backend response + await retry.tryForTime(5000, async () => { + const title = await find.byCssSelector('[data-test-subj="header-page-title"]'); + expect(await title.getVisibleText()).equal(newTitle); + }); + + // validate user action + await find.byCssSelector('[data-test-subj*="title-update-action"]'); + }); + + it('adds a comment to a case', async () => { + const commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + await commentArea.focus(); + await commentArea.type('Test comment from automation'); + await testSubjects.click('submit-comment'); + + // validate user action + const newComment = await find.byCssSelector( + '[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]' + ); + expect(await newComment.getVisibleText()).equal('Test comment from automation'); + }); + + it('adds a tag to a case', async () => { + const tag = uuid.v4(); + await testSubjects.click('tag-list-edit-button'); + await comboBox.setCustom('comboBoxInput', tag); + await testSubjects.click('edit-tags-submit'); + + // validate tag was added + await testSubjects.existOrFail('tag-' + tag); + + // validate user action + await find.byCssSelector('[data-test-subj*="tags-add-action"]'); + }); + + it('deletes a tag from a case', async () => { + await testSubjects.click('tag-list-edit-button'); + // find the tag button and click the close button + const button = await find.byCssSelector('[data-test-subj="comboBoxInput"] button'); + await button.click(); + await testSubjects.click('edit-tags-submit'); + + // validate user action + await find.byCssSelector('[data-test-subj*="tags-delete-action"]'); + }); + + it('changes a case status to in-progress via dropdown menu', async () => { + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-in-progress"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-in-progress'); + }); + + it('changes a case status to closed via dropdown-menu', async () => { + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses.closed); + + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-closed"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-closed'); + }); + + it("reopens a case from the 'reopen case' button", async () => { + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses.closed); + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('case-view-status-action-button'); + await header.waitUntilLoadingHasFinished(); + + await testSubjects.existOrFail('header-page-supplements > status-badge-open', { + timeout: 5000, + }); + + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-open"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-open'); + }); + + it("marks in progress a case from the 'mark in progress' button", async () => { + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses.open); + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('case-view-status-action-button'); + await header.waitUntilLoadingHasFinished(); + + await testSubjects.existOrFail('header-page-supplements > status-badge-in-progress', { + timeout: 5000, + }); + + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-in-progress"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-in-progress'); + }); + + it("closes a case from the 'close case' button", async () => { + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('case-view-status-action-button'); + await header.waitUntilLoadingHasFinished(); + + await testSubjects.existOrFail('header-page-supplements > status-badge-closed', { + timeout: 5000, + }); + + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-closed"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-closed'); + }); + }); + + describe('actions', () => { + // create the case to test on + before(async () => { + await cases.navigation.navigateToApp(); + await cases.api.createNthRandomCases(1); + await cases.casesTable.waitForCasesToBeListed(); + await cases.casesTable.goToFirstListedCase(); + await header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + }); + + it('deletes the case successfully', async () => { + await common.clickAndValidate('property-actions-ellipses', 'property-actions-trash'); + await common.clickAndValidate('property-actions-trash', 'confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.existOrFail('cases-all-title', { timeout: 2000 }); + await cases.casesTable.validateCasesTableHasNthRows(0); + }); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index dc2f6b8618ce3..cee793c8c8bb9 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -17,7 +17,10 @@ const enabledActionTypes = [ '.index', '.pagerduty', '.swimlane', + '.jira', + '.resilient', '.servicenow', + '.servicenow-sir', '.slack', '.webhook', 'test.authorization', From 196061e8ab850aa1b515814f6a70379162244eb5 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 29 Mar 2022 11:02:37 -0700 Subject: [PATCH 012/108] [DOCS] Case comment APIs (#128443) --- docs/api/cases.asciidoc | 7 +- docs/api/cases/cases-api-add-comment.asciidoc | 164 ++++++++++++++++ .../cases/cases-api-update-comment.asciidoc | 178 ++++++++++++++++++ 3 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 docs/api/cases/cases-api-add-comment.asciidoc create mode 100644 docs/api/cases/cases-api-update-comment.asciidoc diff --git a/docs/api/cases.asciidoc b/docs/api/cases.asciidoc index a92951d7d6d1a..88d4f4d668baa 100644 --- a/docs/api/cases.asciidoc +++ b/docs/api/cases.asciidoc @@ -4,7 +4,7 @@ You can create, manage, configure, and send cases to external systems with these APIs: -* {security-guide}/cases-api-add-comment.html[Add comment] +* <> * <> * <> * <> @@ -23,8 +23,10 @@ these APIs: * {security-guide}/assign-connector.html[Set default Elastic Security UI connector] * {security-guide}/case-api-update-connector.html[Update case configurations] * <> -* {security-guide}/cases-api-update-comment.html[Update comment] +* <> +//ADD +include::cases/cases-api-add-comment.asciidoc[leveloffset=+1] //CREATE include::cases/cases-api-create.asciidoc[leveloffset=+1] //DELETE @@ -38,3 +40,4 @@ include::cases/cases-api-get-case.asciidoc[leveloffset=+1] include::cases/cases-api-get-comments.asciidoc[leveloffset=+1] //UPDATE include::cases/cases-api-update.asciidoc[leveloffset=+1] +include::cases/cases-api-update-comment.asciidoc[leveloffset=+1] diff --git a/docs/api/cases/cases-api-add-comment.asciidoc b/docs/api/cases/cases-api-add-comment.asciidoc new file mode 100644 index 0000000000000..20b558a89c683 --- /dev/null +++ b/docs/api/cases/cases-api-add-comment.asciidoc @@ -0,0 +1,164 @@ +[[cases-api-add-comment]] +== Add comment to case API +++++ +Add comment +++++ + +Adds a comment to a case. + +=== Request + +`POST :/api/cases//comments` + +`POST :/s//api/cases//comments` + +=== Prerequisite + +You must have `all` privileges for the *Cases* feature in the *Management*, +*{observability}*, or *Security* section of the +<>, depending on the +`owner` of the case you're updating. + + +=== Path parameters + +``:: +(Required,string) The identifier for the case. To retrieve case IDs, use +<>. + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== Request body + +`alertId`:: +(Required*, string) The alert identifier. It is required only when `type` is +`alert`. preview:[] + +`comment`:: +(Required*, string) The new comment. It is required only when `type` is `user`. + +`index`:: +(Required*, string) The alert index. It is required only when `type` is `alert`. +preview:[] + +`owner`:: +(Required, string) The application that owns the case. Valid values are: +`cases`, `observability`, or `securitySolution`. + +`rule`:: +(Required*, object) The rule that is associated with the alert. It is required +only when `type` is `alert`. preview:[] ++ +.Properties of `rule` +[%collapsible%open] +==== +`id`:: +(Required, string) The rule identifier. preview:[] + +`name`:: +(Required, string) The rule name. preview:[] + +==== + +`type`:: +(Required, string) The comment type, which must be `user` or `alert`. + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +Add a comment to case ID `293f1bc0-74f6-11ea-b83a-553aecdb28b6`: + +[source,sh] +-------------------------------------------------- +POST api/cases/293f1bc0-74f6-11ea-b83a-553aecdb28b6/comments +{ + "type": "user", + "comment": "That is nothing - Ethan Hunt answered a targeted social media campaign promoting phishy pension schemes to IMF operatives.", + "owner": "cases" +} +-------------------------------------------------- +// KIBANA + +The API returns details about the case and its comments. For example: + +[source,json] +-------------------------------------------------- +{ + "comments":[ + { + "id": "8af6ac20-74f6-11ea-b83a-553aecdb28b6", + "version": "WzIwNDMxLDFd", + "type":"user", + "owner":"cases", + "comment":"That is nothing - Ethan Hunt answered a targeted social media campaign promoting phishy pension schemes to IMF operatives.", + "created_at":"2022-03-24T00:49:47.716Z", + "created_by": { + "email": "moneypenny@hms.gov.uk", + "full_name": "Ms Moneypenny", + "username": "moneypenny" + }, + "pushed_at":null, + "pushed_by":null, + "updated_at":null, + "updated_by":null + } + ], + "totalAlerts":0, + "id":"293f1bc0-74f6-11ea-b83a-553aecdb28b6", + "version":"WzIzMzgsMV0=", + "totalComment":1, + "title": "This case will self-destruct in 5 seconds", + "tags": ["phishing","social engineering"], + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants.", + "settings": { + "syncAlerts":false + }, + "owner": "cases", + "closed_at": null, + "closed_by": null, + "created_at": "2022-03-24T00:37:03.906Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": "2022-03-24T00:49:47.716Z", + "updated_by": { + "email": "moneypenny@hms.gov.uk", + "full_name": "Ms Moneypenny", + "username": "moneypenny" + }, + "connector": { + "id": "none", + "name": "none", + "type": ".none", + "fields": null + }, + "external_service": null +} +-------------------------------------------------- + +Add an alert to the case: + +[source,sh] +-------------------------------------------------- +POST api/cases/293f1bc0-74f6-11ea-b83a-553aecdb28b6/comments +{ +"alertId": "6b24c4dc44bc720cfc92797f3d61fff952f2b2627db1fb4f8cc49f4530c4ff42", +"index": ".internal.alerts-security.alerts-default-000001", +"type": "alert", +"owner": "cases", +"rule": { + "id":"94d80550-aaf4-11ec-985f-97e55adae8b9", + "name":"security_rule" + } +} +-------------------------------------------------- +// KIBANA diff --git a/docs/api/cases/cases-api-update-comment.asciidoc b/docs/api/cases/cases-api-update-comment.asciidoc new file mode 100644 index 0000000000000..98d426cb0c86d --- /dev/null +++ b/docs/api/cases/cases-api-update-comment.asciidoc @@ -0,0 +1,178 @@ +[[cases-api-update-comment]] +== Update case comment API +++++ +Update comment +++++ + +Updates a comment in a case. + +=== Request + +`PATCH :/api/cases//comments` + +`PATCH :/s//api/cases//comments` + +=== Prerequisite + +You must have `all` privileges for the *Cases* feature in the *Management*, +*{observability}*, or *Security* section of the +<>, depending on the +`owner` of the case you're updating. + +=== Path parameters + +``:: +The identifier for the case. To retrieve case IDs, use +<>. + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== Request body + +`alertId`:: +(Required*, string) The identifier for the alert. It is required only when +`type` is `alert`. preview:[] + +`comment`:: +(Required*, string) The updated comment. It is required only when `type` is +`user`. + +`id`:: +(Required, string) The identifier for the comment. +//To retrieve comment IDs, use <>. + +`index`:: +(Required*, string) The alert index. It is required only when `type` is `alert`. +preview:[] + +`owner`:: +(Required, string) The application that owns the case. It can be `cases`, +`observability`, or `securitySolution`. ++ +NOTE: You cannot change the owner of a comment. + +`rule`:: +(Required*, object) +The rule that is associated with the alert. It is required only when `type` is +`alert`. preview:[] ++ +.Properties of `rule` +[%collapsible%open] +==== +`id`:: +(Required, string) The rule identifier. preview:[] + +`name`:: +(Required, string) The rule name. preview:[] + +==== + +`type`:: +(Required, string) The comment type, which must be `user` or `alert`. ++ +NOTE: You cannot change the comment type. + +`version`:: +(Required, string) The current comment version. +//To retrieve version values, use <>. + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +Update comment ID `8af6ac20-74f6-11ea-b83a-553aecdb28b6` (associated with case +ID `293f1bc0-74f6-11ea-b83a-553aecdb28b6`): + +[source,sh] +-------------------------------------------------- +PATCH api/cases/293f1bc0-74f6-11ea-b83a-553aecdb28b6/comments +{ + "id": "8af6ac20-74f6-11ea-b83a-553aecdb28b6", + "version": "Wzk1LDFd", + "type": "user", + "comment": "That is nothing - Ethan Hunt answered a targeted social media campaign promoting phishy pension schemes to IMF operatives. Even worse, he likes baked beans." +} +-------------------------------------------------- +// KIBANA + +The API returns details about the case and its comments. For example: + +[source,json] +-------------------------------------------------- +{ + "comments":[{ + "id": "8af6ac20-74f6-11ea-b83a-553aecdb28b6", + "version": "WzIwNjM3LDFd", + "comment":"That is nothing - Ethan Hunt answered a targeted social media campaign promoting phishy pension schemes to IMF operatives. Even worse, he likes baked beans.", + "type":"user", + "owner":"cases", + "created_at":"2022-03-24T00:37:10.832Z", + "created_by": { + "email": "moneypenny@hms.gov.uk", + "full_name": "Ms Moneypenny", + "username": "moneypenny" + }, + "pushed_at":null, + "pushed_by":null, + "updated_at":"2022-03-24T01:27:06.210Z", + "updated_by": { + "email": "jbond@hms.gov.uk", + "full_name": "James Bond", + "username": "_007" + } + } + ], + "totalAlerts":0, + "id": "293f1bc0-74f6-11ea-b83a-553aecdb28b6", + "version": "WzIwNjM2LDFd", + "totalComment": 1, + "title": "This case will self-destruct in 5 seconds", + "tags": ["phishing","social engineering"], + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants.", + "settings": {"syncAlerts":false}, + "owner": "cases"," + closed_at": null, + "closed_by": null, + "created_at": "2022-03-24T00:37:03.906Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": "2022-03-24T01:27:06.210Z", + "updated_by": { + "email": "jbond@hms.gov.uk", + "full_name": "James Bond", + "username": "_007" + }, + "connector": {"id":"none","name":"none","type":".none","fields":null}, + "external_service": null +} +-------------------------------------------------- + +Update an alert in the case: + +[source,sh] +-------------------------------------------------- +PATCH api/cases/293f1bc0-74f6-11ea-b83a-553aecdb28b6/comments +{ +"id": "73362370-ab1a-11ec-985f-97e55adae8b9", +"version": "WzMwNDgsMV0=", +"type": "alert", +"owner": "cases", +"alertId": "c8789278659fdf88b7bf7709b90a082be070d0ba4c23c9c4b552e476c2a667c4", +"index": ".internal.alerts-security.alerts-default-000001", +"rule": +{ + "id":"94d80550-aaf4-11ec-985f-97e55adae8b9", + "name":"security_rule" + } +} +-------------------------------------------------- +// KIBANA From c11a44eb3b19a662e34a58aface197f0418fe6c6 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 29 Mar 2022 20:19:56 +0200 Subject: [PATCH 013/108] [Transform] Extend Transform Health alerting rule with error messages check (#128731) --- x-pack/plugins/transform/common/constants.ts | 11 ++ .../transform/common/types/alerting.ts | 3 + .../plugins/transform/common/utils/alerts.ts | 3 + .../register_transform_health_rule.ts | 20 +++- .../tests_selection_control.tsx | 1 - .../register_transform_health_rule_type.ts | 8 +- .../transform_health_rule_type/schema.ts | 5 + .../transform_health_service.ts | 104 +++++++++++++++++- .../routes/api/transforms_audit_messages.ts | 2 +- 9 files changed, 149 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts index ab25b73fa9555..b378b1ac4ef13 100644 --- a/x-pack/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -132,4 +132,15 @@ export const TRANSFORM_HEALTH_CHECK_NAMES: Record< } ), }, + errorMessages: { + name: i18n.translate('xpack.transform.alertTypes.transformHealth.errorMessagesCheckName', { + defaultMessage: 'Errors in transform messages', + }), + description: i18n.translate( + 'xpack.transform.alertTypes.transformHealth.errorMessagesCheckDescription', + { + defaultMessage: 'Get alerts if a transform contains errors in the transform messages.', + } + ), + }, }; diff --git a/x-pack/plugins/transform/common/types/alerting.ts b/x-pack/plugins/transform/common/types/alerting.ts index dde9a360d9473..07d6bbdbe4152 100644 --- a/x-pack/plugins/transform/common/types/alerting.ts +++ b/x-pack/plugins/transform/common/types/alerting.ts @@ -14,6 +14,9 @@ export type TransformHealthRuleParams = { notStarted?: { enabled: boolean; } | null; + errorMessages?: { + enabled: boolean; + } | null; } | null; } & AlertTypeParams; diff --git a/x-pack/plugins/transform/common/utils/alerts.ts b/x-pack/plugins/transform/common/utils/alerts.ts index 9b3cb2757100a..88c6fc64a35b2 100644 --- a/x-pack/plugins/transform/common/utils/alerts.ts +++ b/x-pack/plugins/transform/common/utils/alerts.ts @@ -12,5 +12,8 @@ export function getResultTestConfig(config: TransformHealthRuleTestsConfig) { notStarted: { enabled: config?.notStarted?.enabled ?? true, }, + errorMessages: { + enabled: config?.errorMessages?.enabled ?? true, + }, }; } diff --git a/x-pack/plugins/transform/public/alerting/transform_health_rule_type/register_transform_health_rule.ts b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/register_transform_health_rule.ts index 779496da59501..ffbc0f6021481 100644 --- a/x-pack/plugins/transform/public/alerting/transform_health_rule_type/register_transform_health_rule.ts +++ b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/register_transform_health_rule.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { TRANSFORM_RULE_TYPE } from '../../../common'; import type { TransformHealthRuleParams } from '../../../common/types/alerting'; import type { RuleTypeModel } from '../../../../triggers_actions_ui/public'; +import { getResultTestConfig } from '../../../common/utils/alerts'; export function getTransformHealthRuleType(): RuleTypeModel { return { @@ -26,11 +27,12 @@ export function getTransformHealthRuleType(): RuleTypeModel(), + testsConfig: new Array(), } as Record, }; if (!ruleParams.includeTransforms?.length) { - validationResult.errors.includeTransforms?.push( + validationResult.errors.includeTransforms.push( i18n.translate( 'xpack.transform.alertTypes.transformHealth.includeTransforms.errorMessage', { @@ -40,6 +42,19 @@ export function getTransformHealthRuleType(): RuleTypeModel !v.enabled); + if (allTestDisabled) { + validationResult.errors.testsConfig.push( + i18n.translate( + 'xpack.transform.alertTypes.transformHealth.testsConfigTransforms.errorMessage', + { + defaultMessage: 'At least one health check has to be selected', + } + ) + ); + } + return validationResult; }, requiresAppContext: false, @@ -56,7 +71,8 @@ export function getTransformHealthRuleType(): RuleTypeModel = React.memo( > ; +} + +export type TransformHealthResult = NotStartedTransformResponse | ErrorMessagesTransformResponse; export type TransformHealthAlertContext = { results: TransformHealthResult[]; @@ -105,7 +109,7 @@ export function getTransformHealthRuleType(): RuleType< } = options; const transformHealthService = transformHealthServiceProvider( - scopedClusterClient.asInternalUser + scopedClusterClient.asCurrentUser ); const executionResult = await transformHealthService.getHealthChecksResults(params); diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/schema.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/schema.ts index 5a7af83b120d6..e98d6edd294ac 100644 --- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/schema.ts +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/schema.ts @@ -17,6 +17,11 @@ export const transformHealthRuleParams = schema.object({ enabled: schema.boolean({ defaultValue: true }), }) ), + errorMessages: schema.nullable( + schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }) + ), }) ), }); diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts index 7aebf83b27cca..90b6c2fbcba60 100644 --- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts @@ -14,15 +14,18 @@ import { ALL_TRANSFORMS_SELECTION, TRANSFORM_HEALTH_CHECK_NAMES, TRANSFORM_RULE_TYPE, + TRANSFORM_STATE, } from '../../../../common/constants'; import { getResultTestConfig } from '../../../../common/utils/alerts'; import { + ErrorMessagesTransformResponse, NotStartedTransformResponse, TransformHealthAlertContext, } from './register_transform_health_rule_type'; import type { RulesClient } from '../../../../../alerting/server'; import type { TransformHealthAlertRule } from '../../../../common/types/alerting'; import { isContinuousTransform } from '../../../../common/types/transform'; +import { ML_DF_NOTIFICATION_INDEX_PATTERN } from '../../../routes/api/transforms_audit_messages'; interface TestResult { name: string; @@ -101,7 +104,7 @@ export function transformHealthServiceProvider( ).transforms; return transformsStats - .filter((t) => t.state !== 'started' && t.state !== 'indexing') + .filter((t) => t.state !== TRANSFORM_STATE.STARTED && t.state !== TRANSFORM_STATE.INDEXING) .map((t) => ({ transform_id: t.id, description: transformsDict.get(t.id)?.description, @@ -109,6 +112,80 @@ export function transformHealthServiceProvider( node_name: t.node?.name, })); }, + /** + * Returns report about transforms that contain error messages + * @param transformIds + */ + async getErrorMessagesReport( + transformIds: string[] + ): Promise { + interface TransformErrorsBucket { + key: string; + doc_count: number; + error_messages: estypes.AggregationsTopHitsAggregate; + } + + const response = await esClient.search< + unknown, + Record<'by_transform', estypes.AggregationsMultiBucketAggregateBase> + >({ + index: ML_DF_NOTIFICATION_INDEX_PATTERN, + size: 0, + query: { + bool: { + filter: [ + { + term: { + level: 'error', + }, + }, + { + terms: { + transform_id: transformIds, + }, + }, + ], + }, + }, + aggs: { + by_transform: { + terms: { + field: 'transform_id', + size: transformIds.length, + }, + aggs: { + error_messages: { + top_hits: { + size: 10, + _source: { + includes: ['message', 'level', 'timestamp', 'node_name'], + }, + }, + }, + }, + }, + }, + }); + + // If transform contains errors, it's in a failed state + const transformsStats = ( + await esClient.transform.getTransformStats({ + transform_id: transformIds.join(','), + }) + ).transforms; + const failedTransforms = new Set( + transformsStats.filter((t) => t.state === TRANSFORM_STATE.FAILED).map((t) => t.id) + ); + + return (response.aggregations?.by_transform.buckets as TransformErrorsBucket[]) + .map(({ key, error_messages: errorMessages }) => { + return { + transform_id: key, + error_messages: errorMessages.hits.hits.map((v) => v._source), + } as ErrorMessagesTransformResponse; + }) + .filter((v) => failedTransforms.has(v.transform_id)); + }, /** * Returns results of the transform health checks * @param params @@ -137,7 +214,30 @@ export function transformHealthServiceProvider( 'xpack.transform.alertTypes.transformHealth.notStartedMessage', { defaultMessage: - '{count, plural, one {Transform} other {Transforms}} {transformsString} {count, plural, one {is} other {are}} not started.', + '{count, plural, one {Transform} other {Transform}} {transformsString} {count, plural, one {is} other {are}} not started.', + values: { count, transformsString }, + } + ), + }, + }); + } + } + + if (testsConfig.errorMessages.enabled) { + const response = await this.getErrorMessagesReport(transformIds); + if (response.length > 0) { + const count = response.length; + const transformsString = response.map((t) => t.transform_id).join(', '); + + result.push({ + name: TRANSFORM_HEALTH_CHECK_NAMES.errorMessages.name, + context: { + results: response, + message: i18n.translate( + 'xpack.transform.alertTypes.transformHealth.errorMessagesMessage', + { + defaultMessage: + '{count, plural, one {Transform} other {Transforms}} {transformsString} {count, plural, one {contains} other {contain}} error messages.', values: { count, transformsString }, } ), diff --git a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts index a6d732b1208b3..4be207a21ac29 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts @@ -14,7 +14,7 @@ import { addBasePath } from '../index'; import { wrapError, wrapEsError } from './error_utils'; -const ML_DF_NOTIFICATION_INDEX_PATTERN = '.transform-notifications-read'; +export const ML_DF_NOTIFICATION_INDEX_PATTERN = '.transform-notifications-read'; const SIZE = 500; interface BoolQuery { From 490cd51abcc5e49e636516308ac6b2e284cf3394 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Tue, 29 Mar 2022 14:40:41 -0400 Subject: [PATCH 014/108] Fix Windows Expand-Archive command for agent install (#128785) --- .../agent_enrollment_flyout/standalone_instructions.tsx | 2 +- .../public/components/enrollment_instructions/manual/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx index 9dd787d74a6e2..4fabb36b99a45 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -66,7 +66,7 @@ tar xzvf elastic-agent-${kibanaVersion}-darwin-x86_64.tar.gz sudo ./elastic-agent install`; const STANDALONE_RUN_INSTRUCTIONS_WINDOWS = `wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip -Expand-Archive .\elastic-agent-${kibanaVersion}-windows-x86_64.zip +Expand-Archive .\\elastic-agent-${kibanaVersion}-windows-x86_64.zip .\\elastic-agent.exe install`; const linuxDebCommand = `curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-amd64.deb diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index 39ad23a859900..4279e46cbcd66 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -40,7 +40,7 @@ cd elastic-agent-${kibanaVersion}-darwin-x86_64 sudo ./elastic-agent install ${enrollArgs}`; const windowsCommand = `wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip -Expand-Archive .\elastic-agent-${kibanaVersion}-windows-x86_64.zip +Expand-Archive .\\elastic-agent-${kibanaVersion}-windows-x86_64.zip .\\elastic-agent.exe install ${enrollArgs}`; const linuxDebCommand = `curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-amd64.deb From 038f091311f9b469dd2333bb40b8069954faaef4 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 29 Mar 2022 14:47:23 -0400 Subject: [PATCH 015/108] [Uptime] allow an administrator to enable and disable monitor management (#128223) * uptime - allow an administrator to enable monitor management Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: shahzad31 --- .../uptime/common/constants/rest_api.ts | 1 + .../runtime_types/monitor_management/state.ts | 14 +- x-pack/plugins/uptime/e2e/journeys/index.ts | 1 + .../journeys/monitor_management.journey.ts | 30 +- .../monitor_management_enablement.journey.ts | 76 +++++ .../e2e/journeys/monitor_name.journey.ts | 14 +- .../read_only_user/monitor_management.ts | 2 +- .../e2e/page_objects/monitor_management.tsx | 71 +++- .../uptime/e2e/tasks/uptime_monitor.ndjson | 3 +- .../common/header/action_menu_content.tsx | 4 +- .../monitor_management/add_monitor_btn.tsx | 198 ++++++++++- .../monitor_management/content/index.ts | 36 ++ .../hooks/use_enablement.ts | 45 +++ .../hooks/use_inline_errors.test.tsx | 5 +- .../hooks/use_inline_errors_count.test.tsx | 5 +- .../hooks/use_locations.test.tsx | 3 + .../monitor_management/loader/loader.tsx | 1 + .../monitor_list/enablement_empty_state.tsx | 133 ++++++++ .../monitor_list/invalid_monitors.tsx | 5 +- .../monitor_list/monitor_list.test.tsx | 3 + .../monitor_list/monitor_list.tsx | 2 +- .../monitor_list/monitor_list_container.tsx | 119 +++++++ .../monitor_list/columns/test_now_col.tsx | 2 +- .../public/lib/__mocks__/uptime_store.mock.ts | 3 + .../pages/monitor_management/add_monitor.tsx | 2 +- .../pages/monitor_management/edit_monitor.tsx | 4 +- .../monitor_management/monitor_management.tsx | 205 +++++++----- .../service_allowed_wrapper.test.tsx | 4 +- .../service_allowed_wrapper.tsx | 6 +- .../use_monitor_management_breadcrumbs.tsx | 2 +- .../state/actions/monitor_management.ts | 16 + .../public/state/api/monitor_management.ts | 19 ++ .../state/effects/monitor_management.ts | 34 +- .../state/reducers/monitor_management.ts | 132 +++++++- .../uptime/server/lib/requests/index.ts | 10 + .../lib/saved_objects/service_api_key.ts | 9 + .../synthetics_service/get_api_key.test.ts | 35 +- .../lib/synthetics_service/get_api_key.ts | 120 +++++-- .../synthetics_service/synthetics_service.ts | 77 +++-- .../plugins/uptime/server/rest_api/index.ts | 16 +- .../synthetics_service/add_monitor.ts | 2 +- .../synthetics_service/delete_monitor.ts | 2 +- .../synthetics_service/edit_monitor.ts | 2 +- .../rest_api/synthetics_service/enablement.ts | 81 +++++ .../synthetics_service/run_once_monitor.ts | 2 +- .../telemetry/monitor_upgrade_sender.ts | 6 +- .../apis/uptime/rest/get_monitor.ts | 4 +- .../api_integration/apis/uptime/rest/index.ts | 1 + .../apis/uptime/rest/synthetics_enablement.ts | 316 ++++++++++++++++++ 49 files changed, 1633 insertions(+), 250 deletions(-) create mode 100644 x-pack/plugins/uptime/e2e/journeys/monitor_management_enablement.journey.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/content/index.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/hooks/use_enablement.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/enablement_empty_state.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx create mode 100644 x-pack/plugins/uptime/server/rest_api/synthetics_service/enablement.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts diff --git a/x-pack/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts index c163718e0fc13..fba2c2b750a4a 100644 --- a/x-pack/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/plugins/uptime/common/constants/rest_api.ts @@ -40,6 +40,7 @@ export enum API_URLS { INDEX_TEMPLATES = '/internal/uptime/service/index_templates', SERVICE_LOCATIONS = '/internal/uptime/service/locations', SYNTHETICS_MONITORS = '/internal/uptime/service/monitors', + SYNTHETICS_ENABLEMENT = '/internal/uptime/service/enablement', RUN_ONCE_MONITOR = '/internal/uptime/service/monitors/run_once', TRIGGER_MONITOR = '/internal/uptime/service/monitors/trigger', SERVICE_ALLOWED = '/internal/uptime/service/allowed', diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/state.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/state.ts index 8119fbca4b9e7..c95ec2f50a529 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/state.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/state.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; -export const FetchMonitorManagementListQueryArgsType = t.partial({ +export const FetchMonitorManagementListQueryArgsCodec = t.partial({ page: t.number, perPage: t.number, sortField: t.string, @@ -17,5 +17,15 @@ export const FetchMonitorManagementListQueryArgsType = t.partial({ }); export type FetchMonitorManagementListQueryArgs = t.TypeOf< - typeof FetchMonitorManagementListQueryArgsType + typeof FetchMonitorManagementListQueryArgsCodec +>; + +export const MonitorManagementEnablementResultCodec = t.type({ + isEnabled: t.boolean, + canEnable: t.boolean, + areApiKeysEnabled: t.boolean, +}); + +export type MonitorManagementEnablementResult = t.TypeOf< + typeof MonitorManagementEnablementResultCodec >; diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index a01502eb84f58..8430dfec98642 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -13,4 +13,5 @@ export * from './read_only_user'; export * from './monitor_details.journey'; export * from './monitor_name.journey'; export * from './monitor_management.journey'; +export * from './monitor_management_enablement.journey'; export * from './monitor_details'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts index 309cc5eb0ec6d..7dfc7e4e6ab66 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts @@ -96,18 +96,14 @@ const createMonitorJourney = ({ async ({ page, params }: { page: Page; params: any }) => { const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl }); const isRemote = process.env.SYNTHETICS_REMOTE_ENABLED; - const deleteMonitor = async () => { - await uptime.navigateToMonitorManagement(); - const isSuccessful = await uptime.deleteMonitor(); - expect(isSuccessful).toBeTruthy(); - }; before(async () => { await uptime.waitForLoadingToFinish(); }); after(async () => { - await deleteMonitor(); + await uptime.navigateToMonitorManagement(); + await uptime.enableMonitorManagement(false); }); step('Go to monitor-management', async () => { @@ -123,13 +119,14 @@ const createMonitorJourney = ({ }); step(`create ${monitorType} monitor`, async () => { + await uptime.enableMonitorManagement(); await uptime.clickAddMonitor(); await uptime.createMonitor({ monitorConfig, monitorType }); const isSuccessful = await uptime.confirmAndSave(); expect(isSuccessful).toBeTruthy(); }); - step(`view ${monitorType} details in monitor management UI`, async () => { + step(`view ${monitorType} details in Monitor Management UI`, async () => { await uptime.navigateToMonitorManagement(); const hasFailure = await uptime.findMonitorConfiguration(monitorDetails); expect(hasFailure).toBeFalsy(); @@ -141,6 +138,12 @@ const createMonitorJourney = ({ await page.waitForSelector(`text=${monitorName}`, { timeout: 160 * 1000 }); }); } + + step('delete monitor', async () => { + await uptime.navigateToMonitorManagement(); + const isSuccessful = await uptime.deleteMonitors(); + expect(isSuccessful).toBeTruthy(); + }); } ); }; @@ -167,6 +170,10 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page; await uptime.waitForLoadingToFinish(); }); + after(async () => { + await uptime.enableMonitorManagement(false); + }); + step('Go to monitor-management', async () => { await uptime.navigateToMonitorManagement(); }); @@ -177,13 +184,14 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page; step('Check breadcrumb', async () => { const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent(); - expect(lastBreadcrumb).toEqual('Monitor management'); + expect(lastBreadcrumb).toEqual('Monitor Management'); }); step('check breadcrumbs', async () => { + await uptime.enableMonitorManagement(); await uptime.clickAddMonitor(); const breadcrumbs = await page.$$('[data-test-subj="breadcrumb"]'); - expect(await breadcrumbs[1].textContent()).toEqual('Monitor management'); + expect(await breadcrumbs[1].textContent()).toEqual('Monitor Management'); const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent(); expect(lastBreadcrumb).toEqual('Add monitor'); }); @@ -204,14 +212,14 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page; // breadcrumb is available before edit page is loaded, make sure its edit view await page.waitForSelector(byTestId('monitorManagementMonitorName'), { timeout: 60 * 1000 }); const breadcrumbs = await page.$$('[data-test-subj=breadcrumb]'); - expect(await breadcrumbs[1].textContent()).toEqual('Monitor management'); + expect(await breadcrumbs[1].textContent()).toEqual('Monitor Management'); const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent(); expect(lastBreadcrumb).toEqual('Edit monitor'); }); step('delete monitor', async () => { await uptime.navigateToMonitorManagement(); - const isSuccessful = await uptime.deleteMonitor(); + const isSuccessful = await uptime.deleteMonitors(); expect(isSuccessful).toBeTruthy(); }); }); diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_management_enablement.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_management_enablement.journey.ts new file mode 100644 index 0000000000000..14232336799d4 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_management_enablement.journey.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before, after, Page } from '@elastic/synthetics'; +import { monitorManagementPageProvider } from '../page_objects/monitor_management'; + +journey( + 'Monitor Management-enablement-superuser', + async ({ page, params }: { page: Page; params: any }) => { + const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + before(async () => { + await uptime.waitForLoadingToFinish(); + }); + + after(async () => { + await uptime.enableMonitorManagement(false); + }); + + step('Go to monitor-management', async () => { + await uptime.navigateToMonitorManagement(); + }); + + step('login to Kibana', async () => { + await uptime.loginToKibana(); + const invalid = await page.locator( + `text=Username or password is incorrect. Please try again.` + ); + expect(await invalid.isVisible()).toBeFalsy(); + }); + + step('check add monitor button', async () => { + expect(await uptime.checkIsEnabled()).toBe(false); + }); + + step('enable Monitor Management', async () => { + await uptime.enableMonitorManagement(); + expect(await uptime.checkIsEnabled()).toBe(true); + }); + } +); + +journey( + 'MonitorManagement-enablement-obs-admin', + async ({ page, params }: { page: Page; params: any }) => { + const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + before(async () => { + await uptime.waitForLoadingToFinish(); + }); + + step('Go to monitor-management', async () => { + await uptime.navigateToMonitorManagement(); + }); + + step('login to Kibana', async () => { + await uptime.loginToKibana('obs_admin_user', 'changeme'); + const invalid = await page.locator( + `text=Username or password is incorrect. Please try again.` + ); + expect(await invalid.isVisible()).toBeFalsy(); + }); + + step('check add monitor button', async () => { + expect(await uptime.checkIsEnabled()).toBe(false); + }); + + step('check that enabled toggle does not appear', async () => { + expect(await page.$(`[data-test-subj=syntheticsEnableSwitch]`)).toBeFalsy(); + }); + } +); diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts index 456d219adef05..624538244e6d1 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts @@ -12,7 +12,7 @@ * 2.0. */ -import { journey, step, expect, after, before, Page } from '@elastic/synthetics'; +import { journey, step, expect, before, Page } from '@elastic/synthetics'; import { monitorManagementPageProvider } from '../page_objects/monitor_management'; import { byTestId } from './utils'; @@ -23,11 +23,6 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => await uptime.waitForLoadingToFinish(); }); - after(async () => { - await uptime.navigateToMonitorManagement(); - await uptime.deleteMonitor(); - }); - step('Go to monitor-management', async () => { await uptime.navigateToMonitorManagement(); }); @@ -39,6 +34,7 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => }); step(`shows error if name already exists`, async () => { + await uptime.enableMonitorManagement(); await uptime.clickAddMonitor(); await uptime.createBasicMonitorDetails({ name: 'Test monitor', @@ -61,4 +57,10 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => expect(await page.isEnabled(byTestId('monitorTestNowRunBtn'))).toBeTruthy(); }); + + step('delete monitor', async () => { + await uptime.navigateToMonitorManagement(); + await uptime.deleteMonitors(); + await uptime.enableMonitorManagement(false); + }); }); diff --git a/x-pack/plugins/uptime/e2e/journeys/read_only_user/monitor_management.ts b/x-pack/plugins/uptime/e2e/journeys/read_only_user/monitor_management.ts index 89d394e945cee..da2958c3775dd 100644 --- a/x-pack/plugins/uptime/e2e/journeys/read_only_user/monitor_management.ts +++ b/x-pack/plugins/uptime/e2e/journeys/read_only_user/monitor_management.ts @@ -27,7 +27,7 @@ journey( }); step('Adding monitor is disabled', async () => { - expect(await page.isEnabled(byTestId('addMonitorBtn'))).toBeFalsy(); + expect(await page.isEnabled(byTestId('syntheticsAddMonitorBtn'))).toBeFalsy(); }); } ); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx index b56cd8a361684..eb13c3678f47e 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { Page } from '@elastic/synthetics'; import { DataStream } from '../../common/runtime_types/monitor_management'; import { getQuerystring } from '../journeys/utils'; @@ -39,6 +38,60 @@ export function monitorManagementPageProvider({ await page.goto(monitorManagement, { waitUntil: 'networkidle', }); + await this.waitForMonitorManagementLoadingToFinish(); + }, + + async waitForMonitorManagementLoadingToFinish() { + while (true) { + if ((await page.$(this.byTestId('uptimeLoader'))) === null) break; + await page.waitForTimeout(5 * 1000); + } + }, + + async enableMonitorManagement(shouldEnable: boolean = true) { + const isEnabled = await this.checkIsEnabled(); + if (isEnabled === shouldEnable) { + return; + } + const [toggle, button] = await Promise.all([ + page.$(this.byTestId('syntheticsEnableSwitch')), + page.$(this.byTestId('syntheticsEnableButton')), + ]); + + if (toggle === null && button === null) { + return null; + } + if (toggle) { + if (isEnabled !== shouldEnable) { + await toggle.click(); + } + } else { + await button?.click(); + } + if (shouldEnable) { + await this.findByText('Monitor Management enabled successfully.'); + } else { + await this.findByText('Monitor Management disabled successfully.'); + } + }, + + async getEnableToggle() { + return await this.findByTestSubj('syntheticsEnableSwitch'); + }, + + async getEnableButton() { + return await this.findByTestSubj('syntheticsEnableSwitch'); + }, + + async getAddMonitorButton() { + return await this.findByTestSubj('syntheticsAddMonitorBtn'); + }, + + async checkIsEnabled() { + await page.waitForTimeout(5 * 1000); + const addMonitorBtn = await this.getAddMonitorButton(); + const isDisabled = await addMonitorBtn.isDisabled(); + return !isDisabled; }, async navigateToAddMonitor() { @@ -57,10 +110,18 @@ export function monitorManagementPageProvider({ await page.click('text=Add monitor'); }, - async deleteMonitor() { - await this.clickByTestSubj('monitorManagementDeleteMonitor'); - await this.clickByTestSubj('confirmModalConfirmButton'); - return await this.findByTestSubj('uptimeDeleteMonitorSuccess'); + async deleteMonitors() { + let isSuccessful: boolean = false; + await page.waitForSelector('[data-test-subj="monitorManagementDeleteMonitor"]'); + while (true) { + if ((await page.$(this.byTestId('monitorManagementDeleteMonitor'))) === null) break; + await page.click(this.byTestId('monitorManagementDeleteMonitor'), { delay: 800 }); + await page.waitForSelector('[data-test-subj="confirmModalTitleText"]'); + await this.clickByTestSubj('confirmModalConfirmButton'); + isSuccessful = Boolean(await this.findByTestSubj('uptimeDeleteMonitorSuccess')); + await page.waitForTimeout(5 * 1000); + } + return isSuccessful; }, async editMonitor() { diff --git a/x-pack/plugins/uptime/e2e/tasks/uptime_monitor.ndjson b/x-pack/plugins/uptime/e2e/tasks/uptime_monitor.ndjson index 308be283a86b8..bb8acca240094 100644 --- a/x-pack/plugins/uptime/e2e/tasks/uptime_monitor.ndjson +++ b/x-pack/plugins/uptime/e2e/tasks/uptime_monitor.ndjson @@ -1,3 +1,2 @@ {"attributes":{"__ui":{"is_tls_enabled":false,"is_zip_url_tls_enabled":false},"check.request.method":"GET","check.response.status":[],"enabled":true,"locations":[{"geo":{"lat":41.25,"lon":-95.86},"id":"us_central","label":"US Central","url":"https://us-central.synthetics.elastic.dev"}],"max_redirects":"0","name":"Test Monitor","proxy_url":"","response.include_body":"on_error","response.include_headers":true,"schedule":{"number":"3","unit":"m"},"service.name":"","tags":[],"timeout":"16","type":"http","urls":"https://www.google.com", "secrets": "{}"},"coreMigrationVersion":"8.1.0","id":"832b9980-7fba-11ec-b360-25a79ce3f496","references":[],"sort":[1643319958480,20371],"type":"synthetics-monitor","updated_at":"2022-01-27T21:45:58.480Z","version":"WzExOTg3ODYsMl0="} -{"attributes":{"__ui":{"is_tls_enabled":false,"is_zip_url_tls_enabled":false},"check.request.method":"GET","check.response.status":[],"enabled":true,"locations":[{"geo":{"lat":41.25,"lon":-95.86},"id":"us_central","label":"US Central","url":"https://us-central.synthetics.elastic.dev"}],"max_redirects":"0","name":"Test Monito","proxy_url":"","response.include_body":"on_error","response.include_headers":true,"schedule":{"number":"3","unit":"m"},"service.name":"","tags":[],"timeout":"16","type":"http","urls":"https://www.google.com", "secrets": "{}"},"coreMigrationVersion":"8.1.0","id":"b28380d0-7fba-11ec-b360-25a79ce3f496","references":[],"sort":[1643320037906,20374],"type":"synthetics-monitor","updated_at":"2022-01-27T21:47:17.906Z","version":"WzExOTg4MDAsMl0="} -{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":2,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":2,"missingRefCount":0,"missingReferences":[]} diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index f83c71ada73a9..54d53ec6d4b1e 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -78,7 +78,7 @@ export function ActionMenuContent(): React.ReactElement { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/add_monitor_btn.tsx b/x-pack/plugins/uptime/public/components/monitor_management/add_monitor_btn.tsx index ddf0f009aeefe..f7ff56cd2f095 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/add_monitor_btn.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/add_monitor_btn.tsx @@ -5,39 +5,203 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiToolTip, EuiSwitch } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; +import { kibanaService } from '../../state/kibana_service'; import { MONITOR_ADD_ROUTE } from '../../../common/constants'; +import { useEnablement } from './hooks/use_enablement'; import { useSyntheticsServiceAllowed } from './hooks/use_service_allowed'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; export const AddMonitorBtn = () => { const history = useHistory(); + const [isEnabling, setIsEnabling] = useState(false); + const [isDisabling, setIsDisabling] = useState(false); + const { + error, + loading: enablementLoading, + enablement, + disableSynthetics, + enableSynthetics, + totalMonitors, + } = useEnablement(); + const { isEnabled, canEnable, areApiKeysEnabled } = enablement || {}; - const { isAllowed, loading } = useSyntheticsServiceAllowed(); + useEffect(() => { + if (isEnabling && isEnabled) { + setIsEnabling(false); + kibanaService.toasts.addSuccess({ + title: SYNTHETICS_ENABLE_SUCCESS, + toastLifeTimeMs: 3000, + }); + } else if (isDisabling && !isEnabled) { + setIsDisabling(false); + kibanaService.toasts.addSuccess({ + title: SYNTHETICS_DISABLE_SUCCESS, + toastLifeTimeMs: 3000, + }); + } else if (isEnabling && error) { + setIsEnabling(false); + kibanaService.toasts.addDanger({ + title: SYNTHETICS_ENABLE_FAILURE, + toastLifeTimeMs: 3000, + }); + } else if (isDisabling && error) { + kibanaService.toasts.addDanger({ + title: SYNTHETICS_DISABLE_FAILURE, + toastLifeTimeMs: 3000, + }); + } + }, [isEnabled, isEnabling, isDisabling, error]); + + const handleSwitch = () => { + if (isEnabled) { + setIsDisabling(true); + disableSynthetics(); + } else { + setIsEnabling(true); + enableSynthetics(); + } + }; + + const getShowSwitch = () => { + if (isEnabled) { + return canEnable; + } else if (!isEnabled) { + return canEnable && (totalMonitors || 0) > 0; + } + }; + + const getSwitchToolTipContent = () => { + if (!isEnabled) { + return SYNTHETICS_ENABLE_TOOL_TIP_MESSAGE; + } else if (isEnabled) { + return SYNTHETICS_DISABLE_TOOL_TIP_MESSAGE; + } else if (!areApiKeysEnabled) { + return API_KEYS_DISABLED_TOOL_TIP_MESSAGE; + } else { + return ''; + } + }; + + const { isAllowed, loading: allowedLoading } = useSyntheticsServiceAllowed(); + + const loading = allowedLoading || enablementLoading; const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save; return ( - - - {ADD_MONITOR_LABEL} - - + + + {getShowSwitch() && !loading && ( + + handleSwitch()} + data-test-subj="syntheticsEnableSwitch" + /> + + )} + {getShowSwitch() && loading && ( + {}} + /> + )} + + + + + {ADD_MONITOR_LABEL} + + + + ); }; const ADD_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.addMonitorLabel', { defaultMessage: 'Add monitor', }); + +const SYNTHETICS_ENABLE_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsEnableLabel', + { + defaultMessage: 'Enable', + } +); + +const SYNTHETICS_ENABLE_FAILURE = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsEnabledFailure', + { + defaultMessage: 'Monitor Management was not able to be enabled. Please contact support.', + } +); + +const SYNTHETICS_DISABLE_FAILURE = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsDisabledFailure', + { + defaultMessage: 'Monitor Management was not able to be disabled. Please contact support.', + } +); + +const SYNTHETICS_ENABLE_SUCCESS = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsEnableSuccess', + { + defaultMessage: 'Monitor Management enabled successfully.', + } +); + +const SYNTHETICS_DISABLE_SUCCESS = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsDisabledSuccess', + { + defaultMessage: 'Monitor Management disabled successfully.', + } +); + +const SYNTHETICS_DISABLED_MESSAGE = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsDisabled', + { + defaultMessage: + 'Monitor Management is currently disabled. Please contact an administrator to enable Monitor Management.', + } +); + +const SYNTHETICS_ENABLE_TOOL_TIP_MESSAGE = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsEnableToolTip', + { + defaultMessage: + 'Enable Monitor Management to create lightweight and real-browser monitors from locations around the world.', + } +); + +const SYNTHETICS_DISABLE_TOOL_TIP_MESSAGE = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsDisableToolTip', + { + defaultMessage: + 'Disabling Monitor Management with immediately stop the execution of monitors in all test locations and prevent the creation of new monitors.', + } +); + +const API_KEYS_DISABLED_TOOL_TIP_MESSAGE = i18n.translate( + 'xpack.uptime.monitorManagement.apiKeysDisabledToolTip', + { + defaultMessage: + 'API Keys are disabled for this cluster. Monitor Management requires the use of API keys to write back to your Elasticsearch cluster. To enable API keys, please contact an administrator.', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/content/index.ts b/x-pack/plugins/uptime/public/components/monitor_management/content/index.ts new file mode 100644 index 0000000000000..369506a12b3d9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/content/index.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 SYNTHETICS_ENABLE_FAILURE = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsEnabledFailure', + { + defaultMessage: 'Monitor Management was not able to be enabled. Please contact support.', + } +); + +export const SYNTHETICS_DISABLE_FAILURE = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsDisabledFailure', + { + defaultMessage: 'Monitor Management was not able to be disabled. Please contact support.', + } +); + +export const SYNTHETICS_ENABLE_SUCCESS = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsEnableSuccess', + { + defaultMessage: 'Monitor Management enabled successfully.', + } +); + +export const SYNTHETICS_DISABLE_SUCCESS = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsDisabledSuccess', + { + defaultMessage: 'Monitor Management disabled successfully.', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_enablement.ts b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_enablement.ts new file mode 100644 index 0000000000000..f4c17a767c4e3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_enablement.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 { useEffect, useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { monitorManagementListSelector } from '../../../state/selectors'; +import { + getSyntheticsEnablement, + disableSynthetics, + enableSynthetics, +} from '../../../state/actions'; + +export function useEnablement() { + const dispatch = useDispatch(); + + const { + loading: { enablement: loading }, + error: { enablement: error }, + enablement, + list: { total }, + } = useSelector(monitorManagementListSelector); + + useEffect(() => { + if (!enablement) { + dispatch(getSyntheticsEnablement()); + } + }, [dispatch, enablement]); + + return { + enablement: { + areApiKeysEnabled: enablement?.areApiKeysEnabled, + canEnable: enablement?.canEnable, + isEnabled: enablement?.isEnabled, + }, + error, + loading, + totalMonitors: total, + enableSynthetics: useCallback(() => dispatch(enableSynthetics()), [dispatch]), + disableSynthetics: useCallback(() => dispatch(disableSynthetics()), [dispatch]), + }; +} diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.test.tsx index 4eabc1fa1eb64..49c069bc3757f 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.test.tsx @@ -68,9 +68,10 @@ describe('useInlineErrors', function () { [ 'heartbeat-8*,heartbeat-7*,synthetics-*', { - error: { monitorList: null, serviceLocations: null }, + error: { monitorList: null, serviceLocations: null, enablement: null }, + enablement: null, list: { monitors: [], page: 1, perPage: 10, total: null }, - loading: { monitorList: false, serviceLocations: false }, + loading: { monitorList: false, serviceLocations: false, enablement: false }, locations: [], syntheticsService: { loading: false, diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.test.tsx index 66961fe66b0f7..8a9115b1490ad 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.test.tsx @@ -67,9 +67,10 @@ describe('useInlineErrorsCount', function () { [ 'heartbeat-8*,heartbeat-7*,synthetics-*', { - error: { monitorList: null, serviceLocations: null }, + error: { monitorList: null, serviceLocations: null, enablement: null }, list: { monitors: [], page: 1, perPage: 10, total: null }, - loading: { monitorList: false, serviceLocations: false }, + enablement: null, + loading: { monitorList: false, serviceLocations: false, enablement: false }, locations: [], syntheticsService: { loading: false, diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.test.tsx index 8c58a4a28ea8c..8d272baa88e95 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.test.tsx @@ -42,13 +42,16 @@ describe('useExpViewTimeRange', function () { monitors: [], }, locations: [], + enablement: null, error: { serviceLocations: error, monitorList: null, + enablement: null, }, loading: { monitorList: false, serviceLocations: loading, + enablement: false, }, syntheticsService: { loading: false, diff --git a/x-pack/plugins/uptime/public/components/monitor_management/loader/loader.tsx b/x-pack/plugins/uptime/public/components/monitor_management/loader/loader.tsx index 8b3b64a3f91c8..fbaf5c1d536cf 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/loader/loader.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/loader/loader.tsx @@ -44,6 +44,7 @@ export const Loader = ({ color="subdued" icon={} title={

{loadingTitle}

} + data-test-subj="uptimeLoader" /> ) : null} diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/enablement_empty_state.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/enablement_empty_state.tsx new file mode 100644 index 0000000000000..c1ed29e999569 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/enablement_empty_state.tsx @@ -0,0 +1,133 @@ +/* + * 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, { useState, useEffect, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui'; +import { useEnablement } from '..//hooks/use_enablement'; +import { kibanaService } from '../../../state/kibana_service'; +import { SYNTHETICS_ENABLE_SUCCESS, SYNTHETICS_DISABLE_SUCCESS } from '../content'; + +export const EnablementEmptyState = ({ focusButton }: { focusButton: boolean }) => { + const { error, enablement, enableSynthetics, loading } = useEnablement(); + const [isEnabling, setIsEnabling] = useState(false); + const { isEnabled, canEnable } = enablement; + const buttonRef = useRef(null); + + useEffect(() => { + if (isEnabling && isEnabled) { + setIsEnabling(false); + kibanaService.toasts.addSuccess({ + title: SYNTHETICS_ENABLE_SUCCESS, + toastLifeTimeMs: 3000, + }); + } else if (isEnabling && error) { + setIsEnabling(false); + kibanaService.toasts.addSuccess({ + title: SYNTHETICS_DISABLE_SUCCESS, + toastLifeTimeMs: 3000, + }); + } + }, [isEnabled, isEnabling, error]); + + const handleEnableSynthetics = () => { + enableSynthetics(); + setIsEnabling(true); + }; + + useEffect(() => { + if (focusButton) { + buttonRef.current?.focus(); + } + }, [focusButton]); + + return !isEnabled && !loading ? ( + + {canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_LABEL : MONITOR_MANAGEMENT_DISABLED_LABEL} + + } + body={ +

+ {canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE : MONITOR_MANAGEMENT_DISABLED_MESSAGE} +

+ } + actions={ + canEnable ? ( + + {MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL} + + ) : null + } + footer={ + <> + +

{LEARN_MORE_LABEL}

+
+ + {DOCS_LABEL} + + + } + /> + ) : null; +}; + +const MONITOR_MANAGEMENT_ENABLEMENT_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.emptyState.enablement.enabled.title', + { + defaultMessage: 'Enable Monitor Management', + } +); + +const MONITOR_MANAGEMENT_DISABLED_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.emptyState.enablement.disabled.title', + { + defaultMessage: 'Monitor Management is disabled', + } +); + +const MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE = i18n.translate( + 'xpack.uptime.monitorManagement.emptyState.enablement', + { + defaultMessage: + 'Enable Monitor Management to run lightweight checks and real-browser monitors from hosted testing locations around the world. Enabling Monitor Management will generate an API key to allow the Synthetics Service to write back to your Elasticsearch cluster.', + } +); + +const MONITOR_MANAGEMENT_DISABLED_MESSAGE = i18n.translate( + 'xpack.uptime.monitorManagement.emptyState.enablement.disabledDescription', + { + defaultMessage: + 'Monitor Management is currently disabled. Monitor Management allows you to run lightweight checks and real-browser monitors from hosted testing locations around the world. To enable Monitor Management, please contact an administrator.', + } +); + +const MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.emptyState.enablement.title', + { + defaultMessage: 'Enable', + } +); + +const DOCS_LABEL = i18n.translate('xpack.uptime.monitorManagement.emptyState.enablement.doc', { + defaultMessage: 'Read the docs', +}); + +const LEARN_MORE_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.emptyState.enablement.learnMore', + { + defaultMessage: 'Want to learn more?', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/invalid_monitors.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/invalid_monitors.tsx index 11f7317c785c4..87a6cf2dc1d2b 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/invalid_monitors.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/invalid_monitors.tsx @@ -49,8 +49,9 @@ export const InvalidMonitors = ({ perPage: pageState.pageSize, total: invalidTotal ?? 0, }, - error: { monitorList: null, serviceLocations: null }, - loading: { monitorList: summariesLoading, serviceLocations: false }, + enablement: null, + error: { monitorList: null, serviceLocations: null, enablement: null }, + loading: { monitorList: summariesLoading, serviceLocations: false, enablement: false }, locations: monitorList.locations, syntheticsService: monitorList.syntheticsService, throttling: DEFAULT_THROTTLING, diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx index 5543904b6a3c4..ba06cf4ad30f7 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx @@ -50,13 +50,16 @@ describe('', () => { monitors, }, locations: [], + enablement: null, error: { serviceLocations: null, monitorList: null, + enablement: null, }, loading: { monitorList: true, serviceLocations: false, + enablement: false, }, syntheticsService: { loading: false, diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx index 559e84d2dd827..2138c452f8053 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx @@ -207,7 +207,7 @@ export const MonitorManagementList = ({ { + const [pageState, dispatchPageAction] = useReducer( + monitorManagementPageReducer, + { + pageIndex: 1, // saved objects page index is base 1 + pageSize: 10, + sortOrder: 'asc', + sortField: ConfigKey.NAME, + } + ); + + const onPageStateChange = useCallback( + (state) => { + dispatchPageAction({ type: 'update', payload: state }); + }, + [dispatchPageAction] + ); + + const onUpdate = useCallback(() => { + dispatchPageAction({ type: 'refresh' }); + }, [dispatchPageAction]); + + useTrackPageview({ app: 'uptime', path: 'manage-monitors' }); + useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 }); + + const dispatch = useDispatch(); + const monitorList = useSelector(monitorManagementListSelector); + + const { pageIndex, pageSize, sortField, sortOrder } = pageState as MonitorManagementListPageState; + + const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>(); + const { errorSummaries, loading, count } = useInlineErrors({ + onlyInvalidMonitors: viewType === 'invalid', + sortField: pageState.sortField, + sortOrder: pageState.sortOrder, + }); + + useEffect(() => { + if (viewType === 'all') { + dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortField, sortOrder })); + } + }, [dispatch, pageState, pageIndex, pageSize, sortField, sortOrder, viewType]); + + const { data: monitorSavedObjects, loading: objectsLoading } = useInvalidMonitors(errorSummaries); + + return ( + <> + + {viewType === 'all' ? ( + + ) : ( + + )} + + ); +}; + +type MonitorManagementPageAction = + | { + type: 'update'; + payload: MonitorManagementListPageState; + } + | { type: 'refresh' }; + +const monitorManagementPageReducer: Reducer< + MonitorManagementListPageState, + MonitorManagementPageAction +> = (state: MonitorManagementListPageState, action: MonitorManagementPageAction) => { + switch (action.type) { + case 'update': + return { + ...state, + ...action.payload, + }; + case 'refresh': + return { ...state }; + default: + throw new Error(`Action "${(action as MonitorManagementPageAction)?.type}" not recognizable`); + } +}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/test_now_col.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/test_now_col.tsx index 68845067f1275..3248f65963d77 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/test_now_col.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/test_now_col.tsx @@ -58,7 +58,7 @@ export const TEST_NOW_ARIA_LABEL = i18n.translate('xpack.uptime.monitorList.test export const TEST_NOW_AVAILABLE_LABEL = i18n.translate( 'xpack.uptime.monitorList.testNow.available', { - defaultMessage: 'Test now is only available for monitors added via Monitor management.', + defaultMessage: 'Test now is only available for monitors added via Monitor Management.', } ); diff --git a/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts index 378345116d176..298f3d17575f1 100644 --- a/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts +++ b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts @@ -74,11 +74,14 @@ export const mockState: AppState = { loading: { monitorList: false, serviceLocations: false, + enablement: false, }, error: { monitorList: null, serviceLocations: null, + enablement: null, }, + enablement: null, syntheticsService: { loading: false, }, diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx index b3b0f0d611c8c..1cb3a023adbc1 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx @@ -52,7 +52,7 @@ const LOADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.addMonitorL const ERROR_HEADING_LABEL = i18n.translate( 'xpack.uptime.monitorManagement.addMonitorLoadingError', { - defaultMessage: 'Error loading monitor management', + defaultMessage: 'Error loading Monitor Management', } ); diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/edit_monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/edit_monitor.tsx index 7ebfbb2297d0a..3d36d507ee917 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/edit_monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/edit_monitor.tsx @@ -49,8 +49,8 @@ const LOADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.editMonitor defaultMessage: 'Loading monitor', }); -const ERROR_HEADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.editMonitorError', { - defaultMessage: 'Error loading monitor management', +const ERROR_HEADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.manageMonitorError', { + defaultMessage: 'Error loading Monitor Management', }); const SERVICE_LOCATIONS_ERROR_LABEL = i18n.translate( diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx index d826db82517fc..3e0e9b955f31f 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx @@ -5,116 +5,151 @@ * 2.0. */ -import React, { useEffect, useReducer, useCallback, Reducer } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { useDispatch, useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; +import { EuiCallOut, EuiButton, EuiSpacer, EuiLink } from '@elastic/eui'; import { useTrackPageview } from '../../../../observability/public'; import { ConfigKey } from '../../../common/runtime_types'; import { getMonitors } from '../../state/actions'; import { monitorManagementListSelector } from '../../state/selectors'; -import { MonitorManagementListPageState } from '../../components/monitor_management/monitor_list/monitor_list'; import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs'; -import { useInlineErrors } from '../../components/monitor_management/hooks/use_inline_errors'; -import { MonitorListTabs } from '../../components/monitor_management/monitor_list/list_tabs'; -import { AllMonitors } from '../../components/monitor_management/monitor_list/all_monitors'; -import { InvalidMonitors } from '../../components/monitor_management/monitor_list/invalid_monitors'; -import { useInvalidMonitors } from '../../components/monitor_management/hooks/use_invalid_monitors'; +import { MonitorListContainer } from '../../components/monitor_management/monitor_list/monitor_list_container'; +import { EnablementEmptyState } from '../../components/monitor_management/monitor_list/enablement_empty_state'; +import { useEnablement } from '../../components/monitor_management/hooks/use_enablement'; +import { Loader } from '../../components/monitor_management/loader/loader'; export const MonitorManagementPage: React.FC = () => { - const [pageState, dispatchPageAction] = useReducer( - monitorManagementPageReducer, - { - pageIndex: 1, // saved objects page index is base 1 - pageSize: 10, - sortOrder: 'asc', - sortField: ConfigKey.NAME, - } - ); - - const onPageStateChange = useCallback( - (state) => { - dispatchPageAction({ type: 'update', payload: state }); - }, - [dispatchPageAction] - ); - - const onUpdate = useCallback(() => { - dispatchPageAction({ type: 'refresh' }); - }, [dispatchPageAction]); - useTrackPageview({ app: 'uptime', path: 'manage-monitors' }); useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 }); useMonitorManagementBreadcrumbs(); const dispatch = useDispatch(); - const monitorList = useSelector(monitorManagementListSelector); + const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false); - const { pageIndex, pageSize, sortField, sortOrder } = pageState as MonitorManagementListPageState; + const { + error: enablementError, + enablement, + loading: enablementLoading, + enableSynthetics, + } = useEnablement(); + const { list: monitorList } = useSelector(monitorManagementListSelector); + const { isEnabled } = enablement; - const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>(); - const { errorSummaries, loading, count } = useInlineErrors({ - onlyInvalidMonitors: viewType === 'invalid', - sortField: pageState.sortField, - sortOrder: pageState.sortOrder, - }); + const isEnabledRef = useRef(isEnabled); useEffect(() => { - if (viewType === 'all') { - dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortField, sortOrder })); + if (monitorList.total === null) { + dispatch( + getMonitors({ + page: 1, // saved objects page index is base 1 + perPage: 10, + sortOrder: 'asc', + sortField: ConfigKey.NAME, + }) + ); } - }, [dispatch, pageState, pageIndex, pageSize, sortField, sortOrder, viewType]); + }, [dispatch, monitorList.total]); - const { data: monitorSavedObjects, loading: objectsLoading } = useInvalidMonitors(errorSummaries); + useEffect(() => { + if (!isEnabled && isEnabledRef.current === true) { + /* shift focus to enable button when enable toggle disappears. Prevent + * focus loss on the page */ + setShouldFocusEnablementButton(true); + } + isEnabledRef.current = Boolean(isEnabled); + }, [isEnabled]); return ( <> - - {viewType === 'all' ? ( - - ) : ( - + + {!isEnabled && monitorList.total && monitorList.total > 0 ? ( + <> + +

{CALLOUT_MANAGEMENT_DESCRIPTION}

+ {enablement.canEnable ? ( + { + enableSynthetics(); + }} + > + {SYNTHETICS_ENABLE_LABEL} + + ) : ( +

+ {CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} + + {LEARN_MORE_LABEL} + +

+ )} +
+ + + ) : null} + {isEnabled || (!isEnabled && monitorList.total) ? : null} +
+ {isEnabled !== undefined && monitorList.total === 0 && ( + )} ); }; -type MonitorManagementPageAction = - | { - type: 'update'; - payload: MonitorManagementListPageState; - } - | { type: 'refresh' }; +const LOADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.manageMonitorLoadingLabel', { + defaultMessage: 'Loading Monitor Management', +}); -const monitorManagementPageReducer: Reducer< - MonitorManagementListPageState, - MonitorManagementPageAction -> = (state: MonitorManagementListPageState, action: MonitorManagementPageAction) => { - switch (action.type) { - case 'update': - return { - ...state, - ...action.payload, - }; - case 'refresh': - return { ...state }; - default: - throw new Error(`Action "${(action as MonitorManagementPageAction)?.type}" not recognizable`); +const LEARN_MORE_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.manageMonitorLoadingLabel.callout.learnMore', + { + defaultMessage: 'Learn more.', } -}; +); + +const CALLOUT_MANAGEMENT_DISABLED = i18n.translate( + 'xpack.uptime.monitorManagement.callout.disabled', + { + defaultMessage: 'Monitor Management is disabled', + } +); + +const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( + 'xpack.uptime.monitorManagement.callout.disabled.adminContact', + { + defaultMessage: 'Please contact your administrator to enable Monitor Management.', + } +); + +const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( + 'xpack.uptime.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.', + } +); + +const ERROR_HEADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.editMonitorError', { + defaultMessage: 'Error loading Monitor Management', +}); + +const ERROR_HEADING_BODY = i18n.translate( + 'xpack.uptime.monitorManagement.editMonitorError.description', + { + defaultMessage: 'Monitor Management settings could not be loaded. Please contact Support.', + } +); + +const SYNTHETICS_ENABLE_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.syntheticsEnableLabel.management', + { + defaultMessage: 'Enable Monitor Management', + } +); diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.test.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.test.tsx index a8aac213186d3..77e64f70d48e3 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.test.tsx @@ -31,7 +31,7 @@ describe('ServiceAllowedWrapper', () => { ); - expect(await findByText('Loading monitor management')).toBeInTheDocument(); + expect(await findByText('Loading Monitor Management')).toBeInTheDocument(); }); it('renders when enabled state is false', async () => { @@ -45,7 +45,7 @@ describe('ServiceAllowedWrapper', () => { ); - expect(await findByText('Monitor management')).toBeInTheDocument(); + expect(await findByText('Monitor Management')).toBeInTheDocument(); }); it('renders when enabled state is true', async () => { diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.tsx index 3092b8f5f1c3b..8f6cd7d3f0eb5 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.tsx @@ -45,13 +45,13 @@ const REQUEST_ACCESS_LABEL = i18n.translate('xpack.uptime.monitorManagement.requ }); const MONITOR_MANAGEMENT_LABEL = i18n.translate('xpack.uptime.monitorManagement.label', { - defaultMessage: 'Monitor management', + defaultMessage: 'Monitor Management', }); const LOADING_MONITOR_MANAGEMENT_LABEL = i18n.translate( 'xpack.uptime.monitorManagement.loading.label', { - defaultMessage: 'Loading monitor management', + defaultMessage: 'Loading Monitor Management', } ); @@ -59,7 +59,7 @@ const PUBLIC_BETA_DESCRIPTION = i18n.translate( 'xpack.uptime.monitorManagement.publicBetaDescription', { defaultMessage: - 'Monitor management is available only for selected public beta users. With public\n' + + 'Monitor Management is available only for selected public beta users. With public\n' + 'beta access, you will be able to add HTTP, TCP, ICMP and Browser checks which will\n' + "run on Elastic's managed synthetics service nodes.", } diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx index 834752c996153..d013af54c38f6 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx @@ -48,7 +48,7 @@ export const useMonitorManagementBreadcrumbs = ({ export const MONITOR_MANAGEMENT_CRUMB = i18n.translate( 'xpack.uptime.monitorManagement.monitorManagementCrumb', { - defaultMessage: 'Monitor management', + defaultMessage: 'Monitor Management', } ); diff --git a/x-pack/plugins/uptime/public/state/actions/monitor_management.ts b/x-pack/plugins/uptime/public/state/actions/monitor_management.ts index 68ca48b5cf22d..278f8fe9a4b99 100644 --- a/x-pack/plugins/uptime/public/state/actions/monitor_management.ts +++ b/x-pack/plugins/uptime/public/state/actions/monitor_management.ts @@ -30,6 +30,22 @@ export const getServiceLocationsSuccess = createAction<{ }>('GET_SERVICE_LOCATIONS_LIST_SUCCESS'); export const getServiceLocationsFailure = createAction('GET_SERVICE_LOCATIONS_LIST_FAILURE'); +export const getSyntheticsEnablement = createAction('GET_SYNTHETICS_ENABLEMENT'); +export const getSyntheticsEnablementSuccess = createAction( + 'GET_SYNTHETICS_ENABLEMENT_SUCCESS' +); +export const getSyntheticsEnablementFailure = createAction( + 'GET_SYNTHETICS_ENABLEMENT_FAILURE' +); + +export const disableSynthetics = createAction('DISABLE_SYNTHETICS'); +export const disableSyntheticsSuccess = createAction('DISABLE_SYNTEHTICS_SUCCESS'); +export const disableSyntheticsFailure = createAction('DISABLE_SYNTHETICS_FAILURE'); + +export const enableSynthetics = createAction('ENABLE_SYNTHETICS'); +export const enableSyntheticsSuccess = createAction('ENABLE_SYNTEHTICS_SUCCESS'); +export const enableSyntheticsFailure = createAction('ENABLE_SYNTHETICS_FAILURE'); + export const getSyntheticsServiceAllowed = createAsyncAction( 'GET_SYNTHETICS_SERVICE_ALLOWED' ); diff --git a/x-pack/plugins/uptime/public/state/api/monitor_management.ts b/x-pack/plugins/uptime/public/state/api/monitor_management.ts index f9e08e3020936..58cc0217c298c 100644 --- a/x-pack/plugins/uptime/public/state/api/monitor_management.ts +++ b/x-pack/plugins/uptime/public/state/api/monitor_management.ts @@ -10,6 +10,8 @@ import { FetchMonitorManagementListQueryArgs, MonitorManagementListResultCodec, MonitorManagementListResult, + MonitorManagementEnablementResultCodec, + MonitorManagementEnablementResult, ServiceLocations, SyntheticsMonitor, EncryptedSyntheticsMonitor, @@ -91,6 +93,23 @@ export const testNowMonitor = async (configId: string): Promise => { + return await apiService.get( + 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); +}; + export const fetchServiceAllowed = async (): Promise => { return await apiService.get(API_URLS.SERVICE_ALLOWED); }; diff --git a/x-pack/plugins/uptime/public/state/effects/monitor_management.ts b/x-pack/plugins/uptime/public/state/effects/monitor_management.ts index 5839d5d9ca30f..e09f275a8817d 100644 --- a/x-pack/plugins/uptime/public/state/effects/monitor_management.ts +++ b/x-pack/plugins/uptime/public/state/effects/monitor_management.ts @@ -13,9 +13,25 @@ import { getServiceLocations, getServiceLocationsSuccess, getServiceLocationsFailure, + getSyntheticsEnablement, + getSyntheticsEnablementSuccess, + getSyntheticsEnablementFailure, + disableSynthetics, + disableSyntheticsSuccess, + disableSyntheticsFailure, + enableSynthetics, + enableSyntheticsSuccess, + enableSyntheticsFailure, getSyntheticsServiceAllowed, } from '../actions'; -import { fetchMonitorManagementList, fetchServiceAllowed, fetchServiceLocations } from '../api'; +import { + fetchMonitorManagementList, + fetchServiceLocations, + fetchServiceAllowed, + fetchGetSyntheticsEnablement, + fetchDisableSynthetics, + fetchEnableSynthetics, +} from '../api'; import { fetchEffectFactory } from './fetch_effect'; export function* fetchMonitorManagementEffect() { @@ -31,6 +47,22 @@ export function* fetchMonitorManagementEffect() { getServiceLocationsFailure ) ); + yield takeLatest( + getSyntheticsEnablement, + fetchEffectFactory( + fetchGetSyntheticsEnablement, + getSyntheticsEnablementSuccess, + getSyntheticsEnablementFailure + ) + ); + yield takeLatest( + disableSynthetics, + fetchEffectFactory(fetchDisableSynthetics, disableSyntheticsSuccess, disableSyntheticsFailure) + ); + yield takeLatest( + enableSynthetics, + fetchEffectFactory(fetchEnableSynthetics, enableSyntheticsSuccess, enableSyntheticsFailure) + ); } export function* fetchSyntheticsServiceAllowedEffect() { diff --git a/x-pack/plugins/uptime/public/state/reducers/monitor_management.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_management.ts index 58f7079067652..419c43db20ccf 100644 --- a/x-pack/plugins/uptime/public/state/reducers/monitor_management.ts +++ b/x-pack/plugins/uptime/public/state/reducers/monitor_management.ts @@ -14,23 +14,32 @@ import { getServiceLocations, getServiceLocationsSuccess, getServiceLocationsFailure, + getSyntheticsEnablement, + getSyntheticsEnablementSuccess, + getSyntheticsEnablementFailure, + disableSynthetics, + disableSyntheticsSuccess, + disableSyntheticsFailure, + enableSynthetics, + enableSyntheticsSuccess, + enableSyntheticsFailure, getSyntheticsServiceAllowed, } from '../actions'; - -import { SyntheticsServiceAllowed } from '../../../common/types'; - import { + MonitorManagementEnablementResult, MonitorManagementListResult, ServiceLocations, ThrottlingOptions, DEFAULT_THROTTLING, } from '../../../common/runtime_types'; +import { SyntheticsServiceAllowed } from '../../../common/types'; export interface MonitorManagementList { - error: Record<'monitorList' | 'serviceLocations', Error | null>; - loading: Record<'monitorList' | 'serviceLocations', boolean>; + error: Record<'monitorList' | 'serviceLocations' | 'enablement', Error | null>; + loading: Record<'monitorList' | 'serviceLocations' | 'enablement', boolean>; list: MonitorManagementListResult; locations: ServiceLocations; + enablement: MonitorManagementEnablementResult | null; syntheticsService: { isAllowed?: boolean; loading: boolean }; throttling: ThrottlingOptions; } @@ -43,13 +52,16 @@ export const initialState: MonitorManagementList = { monitors: [], }, locations: [], + enablement: null, loading: { monitorList: false, serviceLocations: false, + enablement: false, }, error: { monitorList: null, serviceLocations: null, + enablement: null, }, syntheticsService: { loading: false, @@ -141,6 +153,116 @@ export const monitorManagementListReducer = createReducer(initialState, (builder }, }) ) + .addCase(getSyntheticsEnablement, (state: WritableDraft) => ({ + ...state, + loading: { + ...state.loading, + enablement: true, + }, + })) + .addCase( + getSyntheticsEnablementSuccess, + (state: WritableDraft, action: PayloadAction) => ({ + ...state, + loading: { + ...state.loading, + enablement: false, + }, + error: { + ...state.error, + enablement: null, + }, + enablement: action.payload, + }) + ) + .addCase( + getSyntheticsEnablementFailure, + (state: WritableDraft, action: PayloadAction) => ({ + ...state, + loading: { + ...state.loading, + enablement: false, + }, + error: { + ...state.error, + enablement: action.payload, + }, + }) + ) + .addCase(disableSynthetics, (state: WritableDraft) => ({ + ...state, + loading: { + ...state.loading, + enablement: true, + }, + })) + .addCase(disableSyntheticsSuccess, (state: WritableDraft) => ({ + ...state, + loading: { + ...state.loading, + enablement: false, + }, + error: { + ...state.error, + enablement: null, + }, + enablement: { + canEnable: state.enablement?.canEnable || false, + areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false, + isEnabled: false, + }, + })) + .addCase( + disableSyntheticsFailure, + (state: WritableDraft, action: PayloadAction) => ({ + ...state, + loading: { + ...state.loading, + enablement: false, + }, + error: { + ...state.error, + enablement: action.payload, + }, + }) + ) + .addCase(enableSynthetics, (state: WritableDraft) => ({ + ...state, + loading: { + ...state.loading, + enablement: true, + }, + })) + .addCase(enableSyntheticsSuccess, (state: WritableDraft) => ({ + ...state, + loading: { + ...state.loading, + enablement: false, + }, + error: { + ...state.error, + enablement: null, + }, + enablement: { + canEnable: state.enablement?.canEnable || false, + areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false, + isEnabled: true, + }, + })) + .addCase( + enableSyntheticsFailure, + (state: WritableDraft, action: PayloadAction) => ({ + ...state, + loading: { + ...state.loading, + enablement: false, + }, + error: { + ...state.error, + enablement: action.payload, + }, + }) + ) .addCase( String(getSyntheticsServiceAllowed.get), (state: WritableDraft) => ({ diff --git a/x-pack/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts index 2c2633095c8e2..979a914fdfb62 100644 --- a/x-pack/plugins/uptime/server/lib/requests/index.ts +++ b/x-pack/plugins/uptime/server/lib/requests/index.ts @@ -26,6 +26,12 @@ import { getJourneyFailedSteps } from './get_journey_failed_steps'; import { getLastSuccessfulCheck } from './get_last_successful_check'; import { getJourneyScreenshotBlocks } from './get_journey_screenshot_blocks'; import { getSyntheticsMonitor } from './get_monitor'; +import { + getSyntheticsEnablement, + deleteServiceApiKey, + generateAndSaveServiceAPIKey, + getAPIKeyForSyntheticsService, +} from '../synthetics_service/get_api_key'; export const requests = { getCerts, @@ -49,6 +55,10 @@ export const requests = { getJourneyScreenshotBlocks, getJourneyDetails, getNetworkEvents, + getSyntheticsEnablement, + getAPIKeyForSyntheticsService, + deleteServiceApiKey, + generateAndSaveServiceAPIKey, }; export type UptimeRequests = typeof requests; diff --git a/x-pack/plugins/uptime/server/lib/saved_objects/service_api_key.ts b/x-pack/plugins/uptime/server/lib/saved_objects/service_api_key.ts index db63008c35017..43fee5ecd6dad 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects/service_api_key.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects/service_api_key.ts @@ -63,6 +63,7 @@ export const getSyntheticsServiceAPIKey = async (client: EncryptedSavedObjectsCl throw getErr; } }; + export const setSyntheticsServiceApiKey = async ( client: SavedObjectsClientContract, apiKey: SyntheticsServiceApiKey @@ -72,3 +73,11 @@ export const setSyntheticsServiceApiKey = async ( overwrite: true, }); }; + +export const deleteSyntheticsServiceApiKey = async (client: SavedObjectsClientContract) => { + try { + return await client.delete(syntheticsServiceApiKey.name, syntheticsApiKeyID); + } catch (e) { + throw e; + } +}; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.test.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.test.ts index f9ba0ce545bad..7ad3d0b1e2e67 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.test.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.test.ts @@ -12,6 +12,7 @@ import { coreMock } from '../../../../../../src/core/server/mocks'; import { syntheticsServiceApiKey } from '../saved_objects/service_api_key'; import { KibanaRequest } from 'kibana/server'; import { UptimeServerSetup } from '../adapters'; +import { getUptimeESMockClient } from '../requests/helper'; describe('getAPIKeyTest', function () { const core = coreMock.createStart(); @@ -23,6 +24,7 @@ describe('getAPIKeyTest', function () { security, encryptedSavedObjects, savedObjectsClient: core.savedObjects.getScopedClient(request), + uptimeEsClient: getUptimeESMockClient().uptimeEsClient, } as unknown as UptimeServerSetup; security.authc.apiKeys.areAPIKeysEnabled = jest.fn().mockReturnValue(true); @@ -33,38 +35,6 @@ describe('getAPIKeyTest', function () { encoded: '@#$%^&', }); - it('should generate an api key and return it', async () => { - const apiKey = await getAPIKeyForSyntheticsService({ - request, - server, - }); - - expect(security.authc.apiKeys.areAPIKeysEnabled).toHaveBeenCalledTimes(1); - expect(security.authc.apiKeys.create).toHaveBeenCalledTimes(1); - expect(security.authc.apiKeys.create).toHaveBeenCalledWith( - {}, - { - name: 'synthetics-api-key', - role_descriptors: { - synthetics_writer: { - cluster: ['monitor', 'read_ilm', 'read_pipeline'], - index: [ - { - names: ['synthetics-*'], - privileges: ['view_index_metadata', 'create_doc', 'auto_configure'], - }, - ], - }, - }, - metadata: { - description: - 'Created for synthetics service to be passed to the heartbeat to communicate with ES', - }, - } - ); - expect(apiKey).toEqual({ apiKey: 'qwerty', id: 'test', name: 'service-api-key' }); - }); - it('should return existing api key', async () => { const getObject = jest .fn() @@ -74,7 +44,6 @@ describe('getAPIKeyTest', function () { getDecryptedAsInternalUser: getObject, }); const apiKey = await getAPIKeyForSyntheticsService({ - request, server, }); diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts index cd90828f93ccf..766f26a0aeca7 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts @@ -4,25 +4,42 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { + SecurityClusterPrivilege, + SecurityIndexPrivilege, +} from '@elastic/elasticsearch/lib/api/types'; import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server'; + import { SecurityPluginStart } from '../../../../security/server'; import { getSyntheticsServiceAPIKey, + deleteSyntheticsServiceApiKey, setSyntheticsServiceApiKey, syntheticsServiceApiKey, } from '../saved_objects/service_api_key'; import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetics_service_api_key'; import { UptimeServerSetup } from '../adapters'; +export const serviceApiKeyPrivileges = { + cluster: ['monitor', 'read_ilm', 'read_pipeline'] as SecurityClusterPrivilege[], + index: [ + { + names: ['synthetics-*'], + privileges: [ + 'view_index_metadata', + 'create_doc', + 'auto_configure', + ] as SecurityIndexPrivilege[], + }, + ], +}; + export const getAPIKeyForSyntheticsService = async ({ - request, server, }: { server: UptimeServerSetup; - request?: KibanaRequest; }): Promise => { - const { security, encryptedSavedObjects, authSavedObjectsClient } = server; + const { encryptedSavedObjects } = server; const encryptedClient = encryptedSavedObjects.getClient({ includedHiddenTypes: [syntheticsServiceApiKey.name], @@ -36,19 +53,15 @@ export const getAPIKeyForSyntheticsService = async ({ } catch (err) { // TODO: figure out how to handle decryption errors } - - return await generateAndSaveAPIKey({ - request, - security, - authSavedObjectsClient, - }); }; -export const generateAndSaveAPIKey = async ({ +export const generateAndSaveServiceAPIKey = async ({ + server, security, request, authSavedObjectsClient, }: { + server: UptimeServerSetup; request?: KibanaRequest; security: SecurityPluginStart; // authSavedObject is needed for write operations @@ -64,18 +77,15 @@ export const generateAndSaveAPIKey = async ({ throw new Error('User authorization is needed for api key generation'); } + const { canEnable } = await getSyntheticsEnablement({ request, server }); + if (!canEnable) { + throw new SyntheticsForbiddenError(); + } + const apiKeyResult = await security.authc.apiKeys?.create(request, { name: 'synthetics-api-key', role_descriptors: { - synthetics_writer: { - cluster: ['monitor', 'read_ilm', 'read_pipeline'], - index: [ - { - names: ['synthetics-*'], - privileges: ['view_index_metadata', 'create_doc', 'auto_configure'], - }, - ], - }, + synthetics_writer: serviceApiKeyPrivileges, }, metadata: { description: @@ -93,3 +103,73 @@ export const generateAndSaveAPIKey = async ({ return apiKeyObject; } }; + +export const deleteServiceApiKey = async ({ + request, + server, + savedObjectsClient, +}: { + server: UptimeServerSetup; + request?: KibanaRequest; + savedObjectsClient: SavedObjectsClientContract; +}) => { + await deleteSyntheticsServiceApiKey(savedObjectsClient); +}; + +export const getSyntheticsEnablement = async ({ + request, + server: { uptimeEsClient, security, encryptedSavedObjects }, +}: { + server: UptimeServerSetup; + request?: KibanaRequest; +}) => { + const encryptedClient = encryptedSavedObjects.getClient({ + includedHiddenTypes: [syntheticsServiceApiKey.name], + }); + + const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([ + getSyntheticsServiceAPIKey(encryptedClient), + uptimeEsClient.baseESClient.security.hasPrivileges({ + body: { + cluster: [ + 'manage_security', + 'manage_api_key', + 'manage_own_api_key', + ...serviceApiKeyPrivileges.cluster, + ], + index: serviceApiKeyPrivileges.index, + }, + }), + security.authc.apiKeys.areAPIKeysEnabled(), + ]); + + const { cluster } = hasPrivileges; + const { + manage_security: manageSecurity, + manage_api_key: manageApiKey, + manage_own_api_key: manageOwnApiKey, + monitor, + read_ilm: readILM, + read_pipeline: readPipeline, + } = cluster || {}; + + const canManageApiKeys = manageSecurity || manageApiKey || manageOwnApiKey; + const hasClusterPermissions = readILM && readPipeline && monitor; + const hasIndexPermissions = !Object.values(hasPrivileges.index?.['synthetics-*'] || []).includes( + false + ); + + return { + canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions, + isEnabled: Boolean(apiKey), + areApiKeysEnabled, + }; +}; + +export class SyntheticsForbiddenError extends Error { + constructor() { + super(); + this.message = 'Forbidden'; + this.name = 'SyntheticsForbiddenError'; + } +} diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts index 4a86725d137ac..21d5fa6760983 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts @@ -191,32 +191,25 @@ export class SyntheticsService { } } - async getOutput(request?: KibanaRequest) { - if (!this.apiKey) { - try { - this.apiKey = await getAPIKeyForSyntheticsService({ server: this.server, request }); - } catch (err) { - this.logger.error(err); - throw err; - } - } - - if (!this.apiKey) { - const error = new APIKeyMissingError(); - this.logger.error(error); - throw error; + async getApiKey() { + try { + this.apiKey = await getAPIKeyForSyntheticsService({ server: this.server }); + } catch (err) { + this.logger.error(err); + throw err; } - this.logger.debug('Found api key and esHosts for service.'); + return this.apiKey; + } + async getOutput(apiKey: SyntheticsServiceApiKey) { return { hosts: this.esHosts, - api_key: `${this.apiKey.id}:${this.apiKey.apiKey}`, + api_key: `${apiKey?.id}:${apiKey?.apiKey}`, }; } async pushConfigs( - request?: KibanaRequest, configs?: Array< SyntheticsMonitorWithId & { fields_under_root?: boolean; @@ -229,9 +222,16 @@ export class SyntheticsService { this.logger.debug('No monitor found which can be pushed to service.'); return; } + + this.apiKey = await this.getApiKey(); + + if (!this.apiKey) { + return null; + } + const data = { monitors, - output: await this.getOutput(request), + output: await this.getOutput(this.apiKey), }; this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`); @@ -245,7 +245,6 @@ export class SyntheticsService { } async runOnceConfigs( - request?: KibanaRequest, configs?: Array< SyntheticsMonitorWithId & { fields_under_root?: boolean; @@ -257,9 +256,15 @@ export class SyntheticsService { if (monitors.length === 0) { return; } + this.apiKey = await this.getApiKey(); + + if (!this.apiKey) { + return null; + } + const data = { monitors, - output: await this.getOutput(request), + output: await this.getOutput(this.apiKey), }; try { @@ -283,9 +288,16 @@ export class SyntheticsService { if (monitors.length === 0) { return; } + + this.apiKey = await this.getApiKey(); + + if (!this.apiKey) { + return null; + } + const data = { monitors, - output: await this.getOutput(request), + output: await this.getOutput(this.apiKey), }; try { @@ -296,14 +308,25 @@ export class SyntheticsService { } } - async deleteConfigs(request: KibanaRequest, configs: SyntheticsMonitorWithId[]) { + async deleteConfigs(configs: SyntheticsMonitorWithId[]) { + this.apiKey = await this.getApiKey(); + + if (!this.apiKey) { + return null; + } + const data = { monitors: this.formatConfigs(configs), - output: await this.getOutput(request), + output: await this.getOutput(this.apiKey), }; return await this.apiClient.delete(data); } + async deleteAllConfigs() { + const configs = await this.getMonitorConfigs(); + return await this.deleteConfigs(configs); + } + async getMonitorConfigs() { const savedObjectsClient = this.server.savedObjectsClient; const encryptedClient = this.server.encryptedSavedObjects.getClient(); @@ -362,14 +385,6 @@ export class SyntheticsService { } } -class APIKeyMissingError extends Error { - constructor() { - super(); - this.message = 'API key is needed for synthetics service.'; - this.name = 'APIKeyMissingError'; - } -} - class IndexTemplateInstallationError extends Error { constructor() { super(); diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index 8b0775b6ed31a..b95e929416713 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -38,6 +38,11 @@ import { editSyntheticsMonitorRoute } from './synthetics_service/edit_monitor'; import { deleteSyntheticsMonitorRoute } from './synthetics_service/delete_monitor'; import { runOnceSyntheticsMonitorRoute } from './synthetics_service/run_once_monitor'; import { testNowMonitorRoute } from './synthetics_service/test_now_monitor'; +import { + getSyntheticsEnablementRoute, + disableSyntheticsRoute, + enableSyntheticsRoute, +} from './synthetics_service/enablement'; import { getServiceAllowedRoute } from './synthetics_service/get_service_allowed'; export * from './types'; @@ -45,6 +50,8 @@ export { createRouteWithAuth } from './create_route_with_auth'; export { uptimeRouteWrapper } from './uptime_route_wrapper'; export const restApiRoutes: UMRestApiRouteFactory[] = [ + addSyntheticsMonitorRoute, + getSyntheticsEnablementRoute, createGetPingsRoute, createGetIndexStatusRoute, createGetDynamicSettingsRoute, @@ -63,13 +70,14 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createJourneyFailedStepsRoute, createLastSuccessfulCheckRoute, createJourneyScreenshotBlocksRoute, - installIndexTemplatesRoute, + deleteSyntheticsMonitorRoute, + disableSyntheticsRoute, + editSyntheticsMonitorRoute, + enableSyntheticsRoute, getServiceLocationsRoute, getSyntheticsMonitorRoute, getAllSyntheticsMonitorRoute, - addSyntheticsMonitorRoute, - editSyntheticsMonitorRoute, - deleteSyntheticsMonitorRoute, + installIndexTemplatesRoute, runOnceSyntheticsMonitorRoute, testNowMonitorRoute, getServiceAllowedRoute, diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/add_monitor.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/add_monitor.ts index d5beeb9967b18..19bc5050ddfcc 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/add_monitor.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/add_monitor.ts @@ -45,7 +45,7 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ const { syntheticsService } = server; - const errors = await syntheticsService.pushConfigs(request, [ + const errors = await syntheticsService.pushConfigs([ { ...monitor, id: newMonitor.id, diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/delete_monitor.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/delete_monitor.ts index 6e94e1a802897..17f34b0d82ec0 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/delete_monitor.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/delete_monitor.ts @@ -59,7 +59,7 @@ export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ const normalizedMonitor = normalizeSecrets(monitor); await savedObjectsClient.delete(syntheticsMonitorType, monitorId); - const errors = await syntheticsService.deleteConfigs(request, [ + const errors = await syntheticsService.deleteConfigs([ { ...normalizedMonitor.attributes, id: monitorId }, ]); diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts index f590c45166fd5..9ad9084b72ba5 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts @@ -91,7 +91,7 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ monitor.type === 'browser' ? { ...monitorWithRevision, urls: '' } : monitorWithRevision ); - const errors = await syntheticsService.pushConfigs(request, [ + const errors = await syntheticsService.pushConfigs([ { ...editedMonitor, id: editMonitor.id, diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/enablement.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/enablement.ts new file mode 100644 index 0000000000000..5fa7a8b24a495 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/enablement.ts @@ -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 { UMRestApiRouteFactory } from '../types'; +import { API_URLS } from '../../../common/constants'; +import { SyntheticsForbiddenError } from '../../lib/synthetics_service/get_api_key'; + +export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({ + method: 'GET', + path: API_URLS.SYNTHETICS_ENABLEMENT, + validate: {}, + handler: async ({ request, response, server }): Promise => { + try { + return response.ok({ + body: await libs.requests.getSyntheticsEnablement({ + request, + server, + }), + }); + } catch (e) { + server.logger.error(e); + throw e; + } + }, +}); + +export const disableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({ + method: 'DELETE', + path: API_URLS.SYNTHETICS_ENABLEMENT, + validate: {}, + handler: async ({ response, request, server, savedObjectsClient }): Promise => { + const { syntheticsService, security } = server; + try { + const { canEnable } = await libs.requests.getSyntheticsEnablement({ request, server }); + if (!canEnable) { + return response.forbidden(); + } + await syntheticsService.deleteAllConfigs(); + const apiKey = await libs.requests.getAPIKeyForSyntheticsService({ + server, + }); + await libs.requests.deleteServiceApiKey({ + request, + server, + savedObjectsClient, + }); + await security.authc.apiKeys?.invalidate(request, { ids: [apiKey?.id || ''] }); + return response.ok({}); + } catch (e) { + server.logger.error(e); + throw e; + } + }, +}); + +export const enableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({ + method: 'POST', + path: API_URLS.SYNTHETICS_ENABLEMENT, + validate: {}, + handler: async ({ request, response, server }): Promise => { + const { authSavedObjectsClient, logger, security } = server; + try { + await libs.requests.generateAndSaveServiceAPIKey({ + request, + authSavedObjectsClient, + security, + server, + }); + return response.ok({}); + } catch (e) { + logger.error(e); + if (e instanceof SyntheticsForbiddenError) { + return response.forbidden(); + } + throw e; + } + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/run_once_monitor.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/run_once_monitor.ts index 409990a12fcf0..e4e3e1f1bd518 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/run_once_monitor.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/run_once_monitor.ts @@ -32,7 +32,7 @@ export const runOnceSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ const { syntheticsService } = server; - const errors = await syntheticsService.runOnceConfigs(request, [ + const errors = await syntheticsService.runOnceConfigs([ { ...monitor, id: monitorId, diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/telemetry/monitor_upgrade_sender.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/telemetry/monitor_upgrade_sender.ts index 2fe8d9eba761c..29f28e5ab36a6 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/telemetry/monitor_upgrade_sender.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/telemetry/monitor_upgrade_sender.ts @@ -53,7 +53,7 @@ export function formatTelemetryEvent({ lastUpdatedAt?: string; durationSinceLastUpdated?: number; deletedAt?: string; - errors?: ServiceLocationErrors; + errors?: ServiceLocationErrors | null; }) { const { attributes } = monitor; @@ -91,7 +91,7 @@ export function formatTelemetryUpdateEvent( currentMonitor: SavedObjectsUpdateResponse, previousMonitor: SavedObject, kibanaVersion: string, - errors?: ServiceLocationErrors + errors?: ServiceLocationErrors | null ) { let durationSinceLastUpdated: number = 0; if (currentMonitor.updated_at && previousMonitor.updated_at) { @@ -113,7 +113,7 @@ export function formatTelemetryDeleteEvent( previousMonitor: SavedObject, kibanaVersion: string, deletedAt: string, - errors?: ServiceLocationErrors + errors?: ServiceLocationErrors | null ) { let durationSinceLastUpdated: number = 0; if (deletedAt && previousMonitor.updated_at) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts index b24a83769caa9..0fd67b8cb3663 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts @@ -30,7 +30,9 @@ export default function ({ getService }: FtrProviderContext) { return res.body as SimpleSavedObject; }; - before(() => { + before(async () => { + await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + _monitors = [ getFixtureJson('icmp_monitor'), getFixtureJson('tcp_monitor'), diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index f674879552d6a..61b1666b3e559 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -77,6 +77,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./add_monitor')); loadTestFile(require.resolve('./edit_monitor')); loadTestFile(require.resolve('./delete_monitor')); + loadTestFile(require.resolve('./synthetics_enablement')); }); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts b/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts new file mode 100644 index 0000000000000..e9eff005afa3c --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts @@ -0,0 +1,316 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; +import { serviceApiKeyPrivileges } from '../../../../../plugins/uptime/server/lib/synthetics_service/get_api_key'; + +export default function ({ getService }: FtrProviderContext) { + describe('/internal/uptime/service/enablement', () => { + const supertestWithAuth = getService('supertest'); + const supertest = getService('supertestWithoutAuth'); + const security = getService('security'); + + before(async () => { + await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); + }); + + describe('[GET] - /internal/uptime/service/enablement', () => { + ['manage_security', 'manage_api_key', 'manage_own_api_key'].forEach((privilege) => { + it(`returns response for an admin with priviledge ${privilege}`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin-${privilege}`; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { + cluster: [privilege, ...serviceApiKeyPrivileges.cluster], + indices: serviceApiKeyPrivileges.index, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const apiResponse = await supertest + .get(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canEnable: true, + isEnabled: false, + }); + } finally { + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + }); + + it('returns response for an uptime all user without admin privileges', async () => { + const username = 'uptime'; + const roleName = 'uptime_user'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: {}, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const apiResponse = await supertest + .get(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canEnable: false, + isEnabled: false, + }); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + }); + + describe('[POST] - /internal/uptime/service/enablement', () => { + it('with an admin', 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: ['manage_security', ...serviceApiKeyPrivileges.cluster], + indices: serviceApiKeyPrivileges.index, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await supertest + .post(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + const apiResponse = await supertest + .get(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canEnable: true, + isEnabled: true, + }); + } finally { + await supertest + .delete(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it('with an uptime user', async () => { + const username = 'uptime'; + const roleName = `uptime_user`; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: {}, + }); + + await security.user.create(username, { + password, + roles: [roleName], + 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) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canEnable: false, + isEnabled: false, + }); + } finally { + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + }); + + describe('[DELETE] - /internal/uptime/service/enablement', () => { + it('with an admin', 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: ['manage_security', ...serviceApiKeyPrivileges.cluster], + indices: serviceApiKeyPrivileges.index, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await supertest + .post(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + await supertest + .delete(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + const apiResponse = await supertest + .get(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canEnable: true, + isEnabled: false, + }); + } finally { + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it('with an uptime user', async () => { + const username = 'uptime'; + const roleName = `uptime_user`; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: {}, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await supertestWithAuth + .post(API_URLS.SYNTHETICS_ENABLEMENT) + .set('kbn-xsrf', 'true') + .expect(200); + await supertest + .delete(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(403); + const apiResponse = await supertest + .get(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canEnable: false, + isEnabled: true, + }); + } finally { + await supertestWithAuth + .delete(API_URLS.SYNTHETICS_ENABLEMENT) + .set('kbn-xsrf', 'true') + .expect(200); + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + }); + }); +} From dbaf6305358ad3a6770fb833b17430a776ad4c58 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 29 Mar 2022 13:55:32 -0500 Subject: [PATCH 016/108] [ML] Combines annotations into one block if multiple of them overlap (#128782) * Add logic for stacking/merging overlapping annotations * Add logic for stacking/merging overlapping annotations * Add stacking to explorer charts * Fix annotation without block * Make formatting consistent between two views --- .../swimlane_annotation_container.tsx | 157 +++++++++---- .../timeseries_chart/timeseries_chart.js | 207 ++++++++++++------ 2 files changed, 250 insertions(+), 114 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_annotation_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_annotation_container.tsx index 511bb772360c1..c8ab280d465e0 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_annotation_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_annotation_container.tsx @@ -9,8 +9,8 @@ import React, { FC, useEffect } from 'react'; import d3 from 'd3'; import { scaleTime } from 'd3-scale'; import { i18n } from '@kbn/i18n'; -import { formatHumanReadableDateTimeSeconds } from '../../../common/util/date_utils'; -import { AnnotationsTable } from '../../../common/types/annotations'; +import moment from 'moment'; +import type { Annotation, AnnotationsTable } from '../../../common/types/annotations'; import { ChartTooltipService } from '../components/chart_tooltip'; import { useCurrentEuiTheme } from '../components/color_range_legend'; @@ -83,17 +83,54 @@ export const SwimlaneAnnotationContainer: FC = .style('fill', 'none') .style('stroke-width', 1); + // Merging overlapping annotations into bigger blocks + let mergedAnnotations: Array<{ start: number; end: number; annotations: Annotation[] }> = []; + const sortedAnnotationsData = [...annotationsData].sort((a, b) => a.timestamp - b.timestamp); + + if (sortedAnnotationsData.length > 0) { + let lastEndTime = + sortedAnnotationsData[0].end_timestamp ?? sortedAnnotationsData[0].timestamp; + + mergedAnnotations = [ + { + start: sortedAnnotationsData[0].timestamp, + end: lastEndTime, + annotations: [sortedAnnotationsData[0]], + }, + ]; + + for (let i = 1; i < sortedAnnotationsData.length; i++) { + if (sortedAnnotationsData[i].timestamp < lastEndTime) { + const itemToMerge = mergedAnnotations.pop(); + if (itemToMerge) { + const newMergedItem = { + ...itemToMerge, + end: lastEndTime, + annotations: [...itemToMerge.annotations, sortedAnnotationsData[i]], + }; + mergedAnnotations.push(newMergedItem); + } + } else { + lastEndTime = + sortedAnnotationsData[i].end_timestamp ?? sortedAnnotationsData[i].timestamp; + + mergedAnnotations.push({ + start: sortedAnnotationsData[i].timestamp, + end: lastEndTime, + annotations: [sortedAnnotationsData[i]], + }); + } + } + } + // Add annotation marker - annotationsData.forEach((d) => { + mergedAnnotations.forEach((d) => { const annotationWidth = Math.max( - d.end_timestamp - ? xScale(Math.min(d.end_timestamp, domain.max)) - - Math.max(xScale(d.timestamp), startingXPos) - : 0, + d.end ? xScale(Math.min(d.end, domain.max)) - Math.max(xScale(d.start), startingXPos) : 0, ANNOTATION_MIN_WIDTH ); - const xPos = d.timestamp >= domain.min ? xScale(d.timestamp) : startingXPos; + const xPos = d.start >= domain.min ? xScale(d.start) : startingXPos; svg .append('rect') .classed('mlAnnotationRect', true) @@ -103,42 +140,74 @@ export const SwimlaneAnnotationContainer: FC = .attr('height', ANNOTATION_CONTAINER_HEIGHT) .attr('width', annotationWidth) .on('mouseover', function () { - const startingTime = formatHumanReadableDateTimeSeconds(d.timestamp); - const endingTime = - d.end_timestamp !== undefined - ? formatHumanReadableDateTimeSeconds(d.end_timestamp) - : undefined; - - const timeLabel = endingTime ? `${startingTime} - ${endingTime}` : startingTime; - - const tooltipData = [ - { - label: `${d.annotation}`, - seriesIdentifier: { - key: 'anomaly_timeline', - specId: d._id ?? `${d.annotation}-${d.timestamp}-label`, - }, - valueAccessor: 'label', - }, - { - label: `${timeLabel}`, - seriesIdentifier: { - key: 'anomaly_timeline', - specId: d._id ?? `${d.annotation}-${d.timestamp}-ts`, - }, - valueAccessor: 'time', - }, - ]; - if (d.partition_field_name !== undefined && d.partition_field_value !== undefined) { - tooltipData.push({ - label: `${d.partition_field_name}: ${d.partition_field_value}`, - seriesIdentifier: { - key: 'anomaly_timeline', - specId: d._id - ? `${d._id}-partition` - : `${d.partition_field_name}-${d.partition_field_value}-label`, - }, - valueAccessor: 'partition', + const tooltipData: Array<{ + label: string; + seriesIdentifier: { key: string; specId: string } | { key: string; specId: string }; + valueAccessor: string; + skipHeader?: boolean; + value?: string; + }> = []; + if (Array.isArray(d.annotations)) { + const hasMergedAnnotations = d.annotations.length > 1; + if (hasMergedAnnotations) { + // @ts-ignore skipping header so it doesn't have other params + tooltipData.push({ skipHeader: true }); + } + d.annotations.forEach((item) => { + let timespan = moment(item.timestamp).format('MMMM Do YYYY, HH:mm'); + + if (typeof item.end_timestamp !== 'undefined') { + timespan += ` - ${moment(item.end_timestamp).format( + hasMergedAnnotations ? 'HH:mm' : 'MMMM Do YYYY, HH:mm' + )}`; + } + + if (hasMergedAnnotations) { + tooltipData.push({ + label: timespan, + value: `${item.annotation}`, + seriesIdentifier: { + key: 'anomaly_timeline', + specId: item._id ?? `${item.annotation}-${item.timestamp}-label`, + }, + valueAccessor: 'annotation', + }); + } else { + tooltipData.push( + { + label: `${item.annotation}`, + seriesIdentifier: { + key: 'anomaly_timeline', + specId: item._id ?? `${item.annotation}-${item.timestamp}-label`, + }, + valueAccessor: 'label', + }, + { + label: `${timespan}`, + seriesIdentifier: { + key: 'anomaly_timeline', + specId: item._id ?? `${item.annotation}-${item.timestamp}-ts`, + }, + valueAccessor: 'time', + } + ); + } + + if ( + item.partition_field_name !== undefined && + item.partition_field_value !== undefined + ) { + tooltipData.push({ + label: `${item.partition_field_name}: ${item.partition_field_value}`, + seriesIdentifier: { + key: 'anomaly_timeline', + specId: item._id + ? `${item._id}-partition` + : `${item.partition_field_name}-${item.partition_field_value}-label`, + }, + valueAccessor: 'partition', + }); + } }); } // @ts-ignore we don't need all the fields for tooltip to show diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 31cdfa5df0576..9a95cf787c70d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -975,6 +975,48 @@ class TimeseriesChartIntl extends Component { this.props; const data = contextChartData; + const focusAnnotationData = Array.isArray(annotationData) + ? [...annotationData].sort((a, b) => a.timestamp - b.timestamp) + : []; + + // Since there might be lots of annotations which is hard to view + // we should merge overlapping annotations into bigger annotation "blocks" + let mergedAnnotations = []; + if (focusAnnotationData.length > 0) { + mergedAnnotations = [ + { + start: focusAnnotationData[0].timestamp, + end: focusAnnotationData[0].end_timestamp, + annotations: [focusAnnotationData[0]], + }, + ]; + let lastEndTime = focusAnnotationData[0].end_timestamp; + + // Since annotations/intervals are already sorted from earliest to latest + // we can keep checking if next annotation starts before the last merged end_timestamp + for (let i = 1; i < focusAnnotationData.length; i++) { + if (focusAnnotationData[i].timestamp < lastEndTime) { + // If it overlaps with last annotation block, update block with latest end_timestamp + const itemToMerge = mergedAnnotations.pop(); + const newMergedItem = { + ...itemToMerge, + end: lastEndTime, + // and add to list of annotations for that block + annotations: [...itemToMerge.annotations, focusAnnotationData[i]], + }; + mergedAnnotations.push(newMergedItem); + } else { + // If annotation does not overlap with previous block, add it as a new block + mergedAnnotations.push({ + start: focusAnnotationData[i].timestamp, + end: focusAnnotationData[i].end_timestamp, + annotations: [focusAnnotationData[i]], + }); + } + lastEndTime = focusAnnotationData[i].end_timestamp; + } + } + const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); const hideFocusChartTooltip = this.props.tooltipService.hide.bind(this.props.tooltipService); @@ -1101,7 +1143,7 @@ class TimeseriesChartIntl extends Component { const ctxAnnotations = cxtGroup .select('.mlContextAnnotations') .selectAll('g.mlContextAnnotation') - .data(annotationData, (d) => d._id || ''); + .data(mergedAnnotations, (d) => `${d.start}-${d.end}` || ''); ctxAnnotations.enter().append('g').classed('mlContextAnnotation', true); @@ -1113,14 +1155,14 @@ class TimeseriesChartIntl extends Component { .enter() .append('rect') .on('mouseover', function (d) { - showFocusChartTooltip(d, this); + showFocusChartTooltip(d.annotations.length === 1 ? d.annotations[0] : d, this); }) .on('mouseout', () => hideFocusChartTooltip()) .classed('mlContextAnnotationRect', true); ctxAnnotationRects - .attr('x', (d) => { - const date = moment(d.timestamp); + .attr('x', (item) => { + const date = moment(item.start); let xPos = this.contextXScale(date); if (xPos - ANNOTATION_SYMBOL_HEIGHT <= contextXRangeStart) { @@ -1135,11 +1177,11 @@ class TimeseriesChartIntl extends Component { .attr('y', cxtChartHeight + swlHeight + 2) .attr('height', ANNOTATION_SYMBOL_HEIGHT) .attr('width', (d) => { - const start = Math.max(this.contextXScale(moment(d.timestamp)) + 1, contextXRangeStart); + const start = Math.max(this.contextXScale(moment(d.start)) + 1, contextXRangeStart); const end = Math.min( contextXRangeEnd, - typeof d.end_timestamp !== 'undefined' - ? this.contextXScale(moment(d.end_timestamp)) - 1 + typeof d.end !== 'undefined' + ? this.contextXScale(moment(d.end)) - 1 : start + ANNOTATION_MIN_WIDTH ); const width = Math.max(ANNOTATION_MIN_WIDTH, end - start); @@ -1514,16 +1556,18 @@ class TimeseriesChartIntl extends Component { valueAccessor: 'typical', }); } else { - tooltipData.push({ - label: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel', { - defaultMessage: 'value', - }), - value: formatValue(marker.value, marker.function, fieldFormat), - seriesIdentifier: { - key: seriesKey, - }, - valueAccessor: 'value', - }); + if (marker.value !== undefined) { + tooltipData.push({ + label: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel', { + defaultMessage: 'value', + }), + value: formatValue(marker.value, marker.function, fieldFormat), + seriesIdentifier: { + key: seriesKey, + }, + valueAccessor: 'value', + }); + } if (marker.byFieldName !== undefined && marker.numberOfCauses !== undefined) { const numberOfCauses = marker.numberOfCauses; // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. @@ -1549,45 +1593,47 @@ class TimeseriesChartIntl extends Component { } } } else { - tooltipData.push({ - label: i18n.translate( - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.actualLabel', - { - defaultMessage: 'actual', - } - ), - value: formatValue(marker.actual, marker.function, fieldFormat), - seriesIdentifier: { - key: seriesKey, - }, - valueAccessor: 'actual', - }); - tooltipData.push({ - label: i18n.translate( - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel', - { - defaultMessage: 'upper bounds', - } - ), - value: formatValue(marker.upper, marker.function, fieldFormat), - seriesIdentifier: { - key: seriesKey, - }, - valueAccessor: 'upper_bounds', - }); - tooltipData.push({ - label: i18n.translate( - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel', - { - defaultMessage: 'lower bounds', - } - ), - value: formatValue(marker.lower, marker.function, fieldFormat), - seriesIdentifier: { - key: seriesKey, - }, - valueAccessor: 'lower_bounds', - }); + if (!marker.annotations) { + tooltipData.push({ + label: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.actualLabel', + { + defaultMessage: 'actual', + } + ), + value: formatValue(marker.actual, marker.function, fieldFormat), + seriesIdentifier: { + key: seriesKey, + }, + valueAccessor: 'actual', + }); + tooltipData.push({ + label: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel', + { + defaultMessage: 'upper bounds', + } + ), + value: formatValue(marker.upper, marker.function, fieldFormat), + seriesIdentifier: { + key: seriesKey, + }, + valueAccessor: 'upper_bounds', + }); + tooltipData.push({ + label: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel', + { + defaultMessage: 'lower bounds', + } + ), + value: formatValue(marker.lower, marker.function, fieldFormat), + seriesIdentifier: { + key: seriesKey, + }, + valueAccessor: 'lower_bounds', + }); + } } } else { // TODO - need better formatting for small decimals. @@ -1606,22 +1652,24 @@ class TimeseriesChartIntl extends Component { valueAccessor: 'prediction', }); } else { - tooltipData.push({ - label: i18n.translate( - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.valueLabel', - { - defaultMessage: 'value', - } - ), - value: formatValue(marker.value, marker.function, fieldFormat), - seriesIdentifier: { - key: seriesKey, - }, - valueAccessor: 'value', - }); + if (marker.value !== undefined) { + tooltipData.push({ + label: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.valueLabel', + { + defaultMessage: 'value', + } + ), + value: formatValue(marker.value, marker.function, fieldFormat), + seriesIdentifier: { + key: seriesKey, + }, + valueAccessor: 'value', + }); + } } - if (modelPlotEnabled === true) { + if (!marker.annotations && modelPlotEnabled === true) { tooltipData.push({ label: i18n.translate( 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.upperBoundsLabel', @@ -1692,6 +1740,25 @@ class TimeseriesChartIntl extends Component { }); } + if (marker.annotations?.length > 1) { + marker.annotations.forEach((annotation) => { + let timespan = moment(annotation.timestamp).format('MMMM Do YYYY, HH:mm'); + + if (typeof annotation.end_timestamp !== 'undefined') { + timespan += ` - ${moment(annotation.end_timestamp).format('HH:mm')}`; + } + tooltipData.push({ + label: timespan, + value: `${annotation.annotation}`, + seriesIdentifier: { + key: 'anomaly_timeline', + specId: annotation._id ?? `${annotation.annotation}-${annotation.timestamp}-label`, + }, + valueAccessor: 'annotation', + }); + }); + } + let xOffset = LINE_CHART_ANOMALY_RADIUS * 2; // When the annotation area is hovered From 11bba0a04b2b8ef362825702d6a414d5823f32fc Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Tue, 29 Mar 2022 15:09:55 -0400 Subject: [PATCH 017/108] [Security Solution] Consider exceptions when loading threshold alert timelines (#128495) * Add exceptions to threshold timeline * Tests and error handling * Fix unit tests * Add alias for exceptions filter * Fix tests * Type fixes Co-authored-by: Marshall Main --- .../src/technical_field_names.ts | 3 + .../src/use_exception_list_items/index.ts | 2 +- .../src/build_exception_filter/index.ts | 6 +- .../build_exceptions_filter.test.ts | 5 + .../detection_engine/get_query_filter.ts | 1 + .../components/alerts_table/actions.test.tsx | 486 +++++++++++------- .../components/alerts_table/actions.tsx | 51 +- .../components/alerts_table/helpers.ts | 6 +- .../investigate_in_timeline_action.test.tsx | 5 + .../use_investigate_in_timeline.test.tsx | 5 + .../use_investigate_in_timeline.tsx | 69 ++- .../components/alerts_table/types.ts | 4 + .../take_action_dropdown/index.test.tsx | 6 + .../signals/build_events_query.ts | 1 + .../server/lib/machine_learning/index.ts | 1 + 15 files changed, 458 insertions(+), 193 deletions(-) diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index e49b47c712780..f76043c2a6afc 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -47,6 +47,7 @@ const ALERT_RULE_CREATED_AT = `${ALERT_RULE_NAMESPACE}.created_at` as const; const ALERT_RULE_CREATED_BY = `${ALERT_RULE_NAMESPACE}.created_by` as const; const ALERT_RULE_DESCRIPTION = `${ALERT_RULE_NAMESPACE}.description` as const; const ALERT_RULE_ENABLED = `${ALERT_RULE_NAMESPACE}.enabled` as const; +const ALERT_RULE_EXCEPTIONS_LIST = `${ALERT_RULE_NAMESPACE}.exceptions_list` as const; const ALERT_RULE_EXECUTION_UUID = `${ALERT_RULE_NAMESPACE}.execution.uuid` as const; const ALERT_RULE_FROM = `${ALERT_RULE_NAMESPACE}.from` as const; const ALERT_RULE_INTERVAL = `${ALERT_RULE_NAMESPACE}.interval` as const; @@ -104,6 +105,7 @@ const fields = { ALERT_RULE_CREATED_BY, ALERT_RULE_DESCRIPTION, ALERT_RULE_ENABLED, + ALERT_RULE_EXCEPTIONS_LIST, ALERT_RULE_EXECUTION_UUID, ALERT_RULE_FROM, ALERT_RULE_INTERVAL, @@ -158,6 +160,7 @@ export { ALERT_RULE_CREATED_BY, ALERT_RULE_DESCRIPTION, ALERT_RULE_ENABLED, + ALERT_RULE_EXCEPTIONS_LIST, ALERT_RULE_EXECUTION_UUID, ALERT_RULE_FROM, ALERT_RULE_INTERVAL, diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts index 4962ecee58016..623e1e76a7f53 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts @@ -27,7 +27,7 @@ export type ReturnExceptionListAndItems = [ ]; /** - * Hook for using to get an ExceptionList and it's ExceptionListItems + * Hook for using to get an ExceptionList and its ExceptionListItems * * @param http Kibana http service * @param lists array of ExceptionListIdentifiers for all lists to fetch diff --git a/packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts b/packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts index dc00314ece266..966cf4281ad75 100644 --- a/packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts @@ -141,10 +141,12 @@ export const buildExceptionFilter = ({ lists, excludeExceptions, chunkSize, + alias = null, }: { lists: Array; excludeExceptions: boolean; chunkSize: number; + alias: string | null; }): Filter | undefined => { // Remove exception items with large value lists. These are evaluated // elsewhere for the moment being. @@ -154,7 +156,7 @@ export const buildExceptionFilter = ({ const exceptionFilter: Filter = { meta: { - alias: null, + alias, disabled: false, negate: excludeExceptions, }, @@ -195,7 +197,7 @@ export const buildExceptionFilter = ({ return { meta: { - alias: null, + alias, disabled: false, negate: excludeExceptions, }, diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts index feee231f232b0..31d5715512d7b 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts @@ -45,6 +45,7 @@ describe('build_exceptions_filter', () => { describe('buildExceptionFilter', () => { test('it should return undefined if no exception items', () => { const booleanFilter = buildExceptionFilter({ + alias: null, chunkSize: 1, excludeExceptions: false, lists: [], @@ -54,6 +55,7 @@ describe('build_exceptions_filter', () => { test('it should build a filter given an exception list', () => { const booleanFilter = buildExceptionFilter({ + alias: null, chunkSize: 1, excludeExceptions: false, lists: [getExceptionListItemSchemaMock()], @@ -109,6 +111,7 @@ describe('build_exceptions_filter', () => { entries: [{ field: 'user.name', operator: 'included', type: 'match', value: 'name' }], }; const exceptionFilter = buildExceptionFilter({ + alias: null, chunkSize: 2, excludeExceptions: true, lists: [exceptionItem1, exceptionItem2], @@ -187,6 +190,7 @@ describe('build_exceptions_filter', () => { entries: [{ field: 'file.path', operator: 'included', type: 'match', value: '/safe/path' }], }; const exceptionFilter = buildExceptionFilter({ + alias: null, chunkSize: 2, excludeExceptions: true, lists: [exceptionItem1, exceptionItem2, exceptionItem3], @@ -284,6 +288,7 @@ describe('build_exceptions_filter', () => { ]; const booleanFilter = buildExceptionFilter({ + alias: null, chunkSize: 1, excludeExceptions: true, lists: exceptions, diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index e033dfb5b0177..326dc88a1ebd6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -43,6 +43,7 @@ export const getQueryFilter = ( lists, excludeExceptions, chunkSize: 1024, + alias: null, }); const initialQuery = { query, language }; const allFilters = getAllFilters(filters as Filter[], exceptionFilter); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index b1226e5b59190..4f8882ee823b3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -8,6 +8,8 @@ import sinon from 'sinon'; import moment from 'moment'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + import { sendAlertToTimelineAction, determineToAndFrom } from './actions'; import { defaultTimelineProps, @@ -34,6 +36,18 @@ import { DEFAULT_FROM_MOMENT, DEFAULT_TO_MOMENT, } from '../../../common/utils/default_date_settings'; +import { + COMMENTS, + DATE_NOW, + DESCRIPTION, + ENTRIES, + ITEM_TYPE, + META, + NAME, + NAMESPACE_TYPE, + TIE_BREAKER, + USER, +} from '../../../../../lists/common/constants.mock'; jest.mock('../../../timelines/containers/api', () => ({ getTimelineTemplate: jest.fn(), @@ -41,6 +55,30 @@ jest.mock('../../../timelines/containers/api', () => ({ jest.mock('../../../common/lib/kibana'); +export const getExceptionListItemSchemaMock = ( + overrides?: Partial +): ExceptionListItemSchema => ({ + _version: undefined, + comments: COMMENTS, + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + entries: ENTRIES, + id: '1', + item_id: 'endpoint_list_item', + list_id: 'endpoint_list_id', + meta: META, + name: NAME, + namespace_type: NAMESPACE_TYPE, + os_types: [], + tags: ['user added string for a tag', 'malware'], + tie_breaker_id: TIE_BREAKER, + type: ITEM_TYPE, + updated_at: DATE_NOW, + updated_by: USER, + ...(overrides || {}), +}); + describe('alert actions', () => { const anchor = '2020-03-01T17:59:46.349Z'; const unix = moment(anchor).valueOf(); @@ -49,9 +87,51 @@ describe('alert actions', () => { let searchStrategyClient: jest.Mocked; let clock: sinon.SinonFakeTimers; let mockKibanaServices: jest.Mock; + let mockGetExceptions: jest.Mock; let fetchMock: jest.Mock; let toastMock: jest.Mock; + const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({ + ...mockAADEcsDataWithAlert, + kibana: { + alert: { + ...mockAADEcsDataWithAlert.kibana?.alert, + rule: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule, + parameters: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, + threshold: { + field: ['destination.ip'], + value: 1, + }, + }, + name: ['mock threshold rule'], + saved_id: [], + type: ['threshold'], + uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], + timeline_id: undefined, + timeline_title: undefined, + }, + threshold_result: { + count: 99, + from: '2021-01-10T21:11:45.839Z', + cardinality: [ + { + field: 'source.ip', + value: 1, + }, + ], + terms: [ + { + field: 'destination.ip', + value: 1, + }, + ], + }, + }, + }, + }); + beforeEach(() => { // jest carries state between mocked implementations when using // spyOn. So now we're doing all three of these. @@ -59,6 +139,7 @@ describe('alert actions', () => { jest.resetAllMocks(); jest.restoreAllMocks(); jest.clearAllMocks(); + mockGetExceptions = jest.fn().mockResolvedValue([]); createTimeline = jest.fn() as jest.Mocked; updateTimelineIsLoading = jest.fn() as jest.Mocked; @@ -98,8 +179,10 @@ describe('alert actions', () => { ecsData: mockEcsDataWithAlert, updateTimelineIsLoading, searchStrategyClient, + getExceptions: mockGetExceptions, }); + expect(mockGetExceptions).not.toHaveBeenCalled(); expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1); expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: TimelineId.active, @@ -113,6 +196,7 @@ describe('alert actions', () => { ecsData: mockEcsDataWithAlert, updateTimelineIsLoading, searchStrategyClient, + getExceptions: mockGetExceptions, }); const expected = { from: '2018-11-05T18:58:25.937Z', @@ -248,6 +332,7 @@ describe('alert actions', () => { ruleNote: '# this is some markdown documentation', }; + expect(mockGetExceptions).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledWith(expected); }); @@ -269,9 +354,11 @@ describe('alert actions', () => { ecsData: mockEcsDataWithAlert, updateTimelineIsLoading, searchStrategyClient, + getExceptions: mockGetExceptions, }); const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0]; + expect(mockGetExceptions).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery'); }); @@ -286,6 +373,7 @@ describe('alert actions', () => { ecsData: mockEcsDataWithAlert, updateTimelineIsLoading, searchStrategyClient, + getExceptions: mockGetExceptions, }); const defaultTimelinePropsWithoutNote = { ...defaultTimelineProps }; @@ -299,6 +387,7 @@ describe('alert actions', () => { id: TimelineId.active, isLoading: false, }); + expect(mockGetExceptions).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith({ ...defaultTimelinePropsWithoutNote, @@ -331,9 +420,11 @@ describe('alert actions', () => { ecsData: ecsDataMock, updateTimelineIsLoading, searchStrategyClient, + getExceptions: mockGetExceptions, }); expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(mockGetExceptions).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); }); @@ -356,9 +447,11 @@ describe('alert actions', () => { ecsData: ecsDataMock, updateTimelineIsLoading, searchStrategyClient, + getExceptions: mockGetExceptions, }); expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(mockGetExceptions).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); }); @@ -385,9 +478,11 @@ describe('alert actions', () => { ecsData: ecsDataMock, updateTimelineIsLoading, searchStrategyClient, + getExceptions: mockGetExceptions, }); expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(mockGetExceptions).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith({ ...defaultTimelineProps, @@ -426,215 +521,260 @@ describe('alert actions', () => { ecsData: ecsDataMock, updateTimelineIsLoading, searchStrategyClient, + getExceptions: mockGetExceptions, }); expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(mockGetExceptions).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); }); }); - }); - describe('determineToAndFrom', () => { - const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({ - ...mockAADEcsDataWithAlert, - kibana: { - alert: { - ...mockAADEcsDataWithAlert.kibana?.alert, - rule: { - ...mockAADEcsDataWithAlert.kibana?.alert?.rule, - parameters: { - ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, - threshold: { - field: ['destination.ip'], - value: 1, - }, - }, - name: ['mock threshold rule'], - saved_id: [], - type: ['threshold'], - uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], - timeline_id: undefined, - timeline_title: undefined, - }, - threshold_result: { - count: 99, - from: '2021-01-10T21:11:45.839Z', - cardinality: [ - { - field: 'source.ip', - value: 1, - }, - ], - terms: [ + describe('Threshold', () => { + beforeEach(() => { + fetchMock.mockResolvedValue({ + hits: { + hits: [ { - field: 'destination.ip', - value: 1, + _id: ecsDataMockWithNoTemplateTimeline[0]._id, + _index: 'mock', + _source: ecsDataMockWithNoTemplateTimeline[0], }, ], }, - }, - }, - }); - beforeEach(() => { - fetchMock.mockResolvedValue({ - hits: { - hits: [ - { - _id: ecsDataMockWithNoTemplateTimeline[0]._id, - _index: 'mock', - _source: ecsDataMockWithNoTemplateTimeline[0], - }, - ], - }, + }); }); - }); - test('it uses ecs.Data.timestamp if one is provided', () => { - const ecsDataMock: Ecs = { - ...mockEcsDataWithAlert, - timestamp: '2020-03-20T17:59:46.349Z', - }; - const result = determineToAndFrom({ ecs: ecsDataMock }); - - expect(result.from).toEqual('2020-03-20T17:54:46.349Z'); - expect(result.to).toEqual('2020-03-20T17:59:46.349Z'); - }); - test('it uses current time timestamp if ecsData.timestamp is not provided', () => { - const { timestamp, ...ecsDataMock } = mockEcsDataWithAlert; - const result = determineToAndFrom({ ecs: ecsDataMock }); - - expect(result.from).toEqual('2020-03-01T17:54:46.349Z'); - expect(result.to).toEqual('2020-03-01T17:59:46.349Z'); - }); + test('Exceptions are included', async () => { + mockGetExceptions.mockResolvedValue([getExceptionListItemSchemaMock()]); + await sendAlertToTimelineAction({ + createTimeline, + ecsData: ecsDataMockWithNoTemplateTimeline, + updateTimelineIsLoading, + searchStrategyClient, + getExceptions: mockGetExceptions, + }); - test('it uses original_time and threshold_result.from for threshold alerts', async () => { - const expectedFrom = '2021-01-10T21:11:45.839Z'; - const expectedTo = '2021-01-10T21:12:45.839Z'; + const expectedFrom = '2021-01-10T21:11:45.839Z'; + const expectedTo = '2021-01-10T21:12:45.839Z'; - await sendAlertToTimelineAction({ - createTimeline, - ecsData: ecsDataMockWithNoTemplateTimeline, - updateTimelineIsLoading, - searchStrategyClient, - }); - expect(createTimeline).toHaveBeenCalledTimes(1); - expect(createTimeline).toHaveBeenCalledWith({ - ...defaultTimelineProps, - timeline: { - ...defaultTimelineProps.timeline, - dataProviders: [ - { - and: [], - enabled: true, - excluded: false, - id: 'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-destination-ip-1', - kqlQuery: '', - name: 'destination.ip', - queryMatch: { field: 'destination.ip', operator: ':', value: 1 }, + expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(mockGetExceptions).toHaveBeenCalled(); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith({ + ...defaultTimelineProps, + timeline: { + ...defaultTimelineProps.timeline, + dataProviders: [ + { + and: [], + enabled: true, + excluded: false, + id: 'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-destination-ip-1', + kqlQuery: '', + name: 'destination.ip', + queryMatch: { field: 'destination.ip', operator: ':', value: 1 }, + }, + ], + dateRange: { + start: expectedFrom, + end: expectedTo, }, - ], - dateRange: { - start: expectedFrom, - end: expectedTo, - }, - description: '_id: 1', - kqlQuery: { - filterQuery: { - kuery: { - expression: ['user.id:1'], - kind: ['kuery'], + description: '_id: 1', + filters: [ + { + meta: { + alias: 'Exceptions', + disabled: false, + negate: true, + }, + query: { + bool: { + should: [ + { + bool: { + filter: [ + { + nested: { + path: 'some.parentField', + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'some.parentField.nested.field': 'some value', + }, + }, + ], + }, + }, + score_mode: 'none', + }, + }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'some.not.nested.field': 'some value', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + ], + kqlQuery: { + filterQuery: { + kuery: { + expression: ['user.id:1'], + kind: ['kuery'], + }, + serializedQuery: ['user.id:1'], }, - serializedQuery: ['user.id:1'], }, + resolveTimelineConfig: undefined, }, - resolveTimelineConfig: undefined, - }, - from: expectedFrom, - to: expectedTo, + from: expectedFrom, + to: expectedTo, + }); }); }); - }); - describe('show toasts when data is malformed', () => { - const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({ - ...mockAADEcsDataWithAlert, - kibana: { - alert: { - ...mockAADEcsDataWithAlert.kibana?.alert, - rule: { - ...mockAADEcsDataWithAlert.kibana?.alert?.rule, - parameters: { - ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, - threshold: { - field: ['destination.ip'], - value: 1, - }, - }, - name: ['mock threshold rule'], - saved_id: [], - type: ['threshold'], - uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], - timeline_id: undefined, - timeline_title: undefined, - }, - threshold_result: { - count: 99, - from: '2021-01-10T21:11:45.839Z', - cardinality: [ + describe('determineToAndFrom', () => { + beforeEach(() => { + fetchMock.mockResolvedValue({ + hits: { + hits: [ { - field: 'source.ip', - value: 1, + _id: ecsDataMockWithNoTemplateTimeline[0]._id, + _index: 'mock', + _source: ecsDataMockWithNoTemplateTimeline[0], }, ], - terms: [ + }, + }); + }); + + test('it uses ecs.Data.timestamp if one is provided', () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithAlert, + timestamp: '2020-03-20T17:59:46.349Z', + }; + const result = determineToAndFrom({ ecs: ecsDataMock }); + + expect(result.from).toEqual('2020-03-20T17:54:46.349Z'); + expect(result.to).toEqual('2020-03-20T17:59:46.349Z'); + }); + + test('it uses current time timestamp if ecsData.timestamp is not provided', () => { + const { timestamp, ...ecsDataMock } = mockEcsDataWithAlert; + const result = determineToAndFrom({ ecs: ecsDataMock }); + + expect(result.from).toEqual('2020-03-01T17:54:46.349Z'); + expect(result.to).toEqual('2020-03-01T17:59:46.349Z'); + }); + + test('it uses original_time and threshold_result.from for threshold alerts', async () => { + const expectedFrom = '2021-01-10T21:11:45.839Z'; + const expectedTo = '2021-01-10T21:12:45.839Z'; + + await sendAlertToTimelineAction({ + createTimeline, + ecsData: ecsDataMockWithNoTemplateTimeline, + updateTimelineIsLoading, + searchStrategyClient, + getExceptions: mockGetExceptions, + }); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith({ + ...defaultTimelineProps, + timeline: { + ...defaultTimelineProps.timeline, + dataProviders: [ { - field: 'destination.ip', - value: 1, + and: [], + enabled: true, + excluded: false, + id: 'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-destination-ip-1', + kqlQuery: '', + name: 'destination.ip', + queryMatch: { field: 'destination.ip', operator: ':', value: 1 }, }, ], + dateRange: { + start: expectedFrom, + end: expectedTo, + }, + description: '_id: 1', + kqlQuery: { + filterQuery: { + kuery: { + expression: ['user.id:1'], + kind: ['kuery'], + }, + serializedQuery: ['user.id:1'], + }, + }, + resolveTimelineConfig: undefined, }, - }, - }, - }); - beforeEach(() => { - fetchMock.mockResolvedValue({ - hits: 'not correctly formed doc', + from: expectedFrom, + to: expectedTo, + }); }); }); - test('renders a toast and calls create timeline with basic defaults', async () => { - const expectedFrom = DEFAULT_FROM_MOMENT.toISOString(); - const expectedTo = DEFAULT_TO_MOMENT.toISOString(); - const timelineProps = { - ...defaultTimelineProps, - timeline: { - ...defaultTimelineProps.timeline, - dataProviders: [], - dateRange: { - start: expectedFrom, - end: expectedTo, - }, - description: '', - kqlQuery: { - filterQuery: null, + + describe('show toasts when data is malformed', () => { + beforeEach(() => { + fetchMock.mockResolvedValue({ + hits: 'not correctly formed doc', + }); + }); + + test('renders a toast and calls create timeline with basic defaults', async () => { + const expectedFrom = DEFAULT_FROM_MOMENT.toISOString(); + const expectedTo = DEFAULT_TO_MOMENT.toISOString(); + const timelineProps = { + ...defaultTimelineProps, + timeline: { + ...defaultTimelineProps.timeline, + dataProviders: [], + dateRange: { + start: expectedFrom, + end: expectedTo, + }, + description: '', + kqlQuery: { + filterQuery: null, + }, + resolveTimelineConfig: undefined, }, - resolveTimelineConfig: undefined, - }, - from: expectedFrom, - to: expectedTo, - }; + from: expectedFrom, + to: expectedTo, + }; - delete timelineProps.ruleNote; + delete timelineProps.ruleNote; - await sendAlertToTimelineAction({ - createTimeline, - ecsData: ecsDataMockWithNoTemplateTimeline, - updateTimelineIsLoading, - searchStrategyClient, + await sendAlertToTimelineAction({ + createTimeline, + ecsData: ecsDataMockWithNoTemplateTimeline, + updateTimelineIsLoading, + searchStrategyClient, + getExceptions: mockGetExceptions, + }); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(timelineProps); + expect(toastMock).toHaveBeenCalled(); }); - expect(createTimeline).toHaveBeenCalledTimes(1); - expect(createTimeline).toHaveBeenCalledWith(timelineProps); - expect(toastMock).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 46e439d38f81e..ac47032acc539 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -11,9 +11,11 @@ import { getOr, isEmpty } from 'lodash/fp'; import moment from 'moment'; import dateMath from '@elastic/datemath'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { FilterStateStore, Filter } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; + import { ALERT_RULE_FROM, ALERT_RULE_TYPE, @@ -21,7 +23,9 @@ import { ALERT_RULE_PARAMETERS, } from '@kbn/rule-data-utils'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { buildExceptionFilter } from '@kbn/securitysolution-list-utils'; + import { ALERT_ORIGINAL_TIME, ALERT_GROUP_ID, @@ -265,14 +269,14 @@ export const getThresholdAggregationData = (ecsData: Ecs | Ecs[]): ThresholdAggr ); }; -export const isEqlRuleWithGroupId = (ecsData: Ecs): boolean => { +export const isEqlAlertWithGroupId = (ecsData: Ecs): boolean => { const ruleType = getField(ecsData, ALERT_RULE_TYPE); const groupId = getField(ecsData, ALERT_GROUP_ID); const isEql = ruleType === 'eql' || (Array.isArray(ruleType) && ruleType[0] === 'eql'); return isEql && groupId?.length > 0; }; -export const isThresholdRule = (ecsData: Ecs): boolean => { +export const isThresholdAlert = (ecsData: Ecs): boolean => { const ruleType = getField(ecsData, ALERT_RULE_TYPE); return ( ruleType === 'threshold' || @@ -396,7 +400,8 @@ const createThresholdTimeline = async ( ecsData: Ecs, createTimeline: ({ from, timeline, to }: CreateTimelineProps) => void, noteContent: string, - templateValues: { filters?: Filter[]; query?: string; dataProviders?: DataProvider[] } + templateValues: { filters?: Filter[]; query?: string; dataProviders?: DataProvider[] }, + getExceptions: (ecs: Ecs) => Promise ) => { try { const alertResponse = await KibanaServices.get().http.fetch< @@ -417,6 +422,7 @@ const createThresholdTimeline = async ( }, ]; }, []) ?? []; + const alertDoc = formattedAlertData[0]; const params = getField(alertDoc, ALERT_RULE_PARAMETERS); const filters = getFiltersFromRule(params.filters ?? alertDoc.signal?.rule?.filters) ?? []; @@ -425,13 +431,23 @@ const createThresholdTimeline = async ( const indexNames = params.index ?? alertDoc.signal?.rule?.index ?? []; const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(alertDoc); + const exceptions = await getExceptions(ecsData); + const exceptionsFilter = + buildExceptionFilter({ + lists: exceptions, + excludeExceptions: true, + chunkSize: 10000, + alias: 'Exceptions', + }) ?? []; + const allFilters = (templateValues.filters ?? filters).concat(exceptionsFilter); + return createTimeline({ from: thresholdFrom, notes: null, timeline: { ...timelineDefaults, description: `_id: ${alertDoc._id}`, - filters: templateValues.filters ?? filters, + filters: allFilters, dataProviders: templateValues.dataProviders ?? dataProviders, id: TimelineId.active, indexNames, @@ -495,6 +511,7 @@ export const sendAlertToTimelineAction = async ({ ecsData: ecs, updateTimelineIsLoading, searchStrategyClient, + getExceptions, }: SendAlertToTimelineActionProps) => { /* FUTURE DEVELOPER * We are making an assumption here that if you have an array of ecs data they are all coming from the same rule @@ -554,12 +571,18 @@ export const sendAlertToTimelineAction = async ({ timeline.timelineType ); // threshold with template - if (isThresholdRule(ecsData)) { - return createThresholdTimeline(ecsData, createTimeline, noteContent, { - filters, - query, - dataProviders, - }); + if (isThresholdAlert(ecsData)) { + return createThresholdTimeline( + ecsData, + createTimeline, + noteContent, + { + filters, + query, + dataProviders, + }, + getExceptions + ); } else { return createTimeline({ from, @@ -612,11 +635,11 @@ export const sendAlertToTimelineAction = async ({ to, }); } - } else if (isThresholdRule(ecsData)) { - return createThresholdTimeline(ecsData, createTimeline, noteContent, {}); + } else if (isThresholdAlert(ecsData)) { + return createThresholdTimeline(ecsData, createTimeline, noteContent, {}, getExceptions); } else { let { dataProviders, filters } = buildTimelineDataProviderOrFilter(alertIds ?? [], ecsData._id); - if (isEqlRuleWithGroupId(ecsData)) { + if (isEqlAlertWithGroupId(ecsData)) { const tempEql = buildEqlDataProviderOrFilter(alertIds ?? [], ecs); dataProviders = tempEql.dataProviders; filters = tempEql.filters; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts index de1a061ab7dac..37edd3ecab3e5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts @@ -6,14 +6,16 @@ */ import { isEmpty } from 'lodash/fp'; + import { Filter, FilterStateStore, KueryNode, fromKueryExpression } from '@kbn/es-query'; + +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { TimelineType } from '../../../../common/types/timeline'; import { DataProvider, DataProviderType, DataProvidersAnd, } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { TimelineType } from '../../../../common/types/timeline'; interface FindValueToChangeInQuery { field: string; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx index 24433e2f2ca99..9564347e88ccd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx @@ -13,6 +13,7 @@ import * as actions from '../actions'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import type { SendAlertToTimelineActionProps } from '../types'; import { InvestigateInTimelineAction } from './investigate_in_timeline_action'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; const ecsRowData: Ecs = { _id: '1', @@ -29,6 +30,7 @@ const ecsRowData: Ecs = { }; jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../actions'); const props = { @@ -54,6 +56,9 @@ describe('use investigate in timeline hook', () => { }, }, }); + (useAppToasts as jest.Mock).mockReturnValue({ + addError: jest.fn(), + }); }); afterEach(() => { jest.resetAllMocks(); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx index fc413a6f4f814..7dea1581e9f0f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx @@ -13,6 +13,7 @@ import { useInvestigateInTimeline } from './use_investigate_in_timeline'; import * as actions from '../actions'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import type { SendAlertToTimelineActionProps } from '../types'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; const ecsRowData: Ecs = { _id: '1', @@ -29,6 +30,7 @@ const ecsRowData: Ecs = { }; jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../actions'); const props = { @@ -53,6 +55,9 @@ describe('use investigate in timeline hook', () => { }, }, }); + (useAppToasts as jest.Mock).mockReturnValue({ + addError: jest.fn(), + }); }); afterEach(() => { jest.resetAllMocks(); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index 301395eb5b963..58163029667e6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -8,8 +8,17 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { EuiContextMenuItem } from '@elastic/eui'; -import { useKibana } from '../../../../common/lib/kibana'; +import { i18n } from '@kbn/i18n'; +import { ALERT_RULE_EXCEPTIONS_LIST } from '@kbn/rule-data-utils'; +import { + ExceptionListIdentifiers, + ExceptionListItemSchema, + ReadExceptionListSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { useApi } from '@kbn/securitysolution-list-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { Ecs } from '../../../../../common/ecs'; import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; @@ -19,6 +28,8 @@ import { useCreateTimeline } from '../../../../timelines/components/timeline/pro import { CreateTimelineProps } from '../types'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../translations'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { getField } from '../../../../helpers'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; interface UseInvestigateInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; @@ -29,11 +40,65 @@ export const useInvestigateInTimeline = ({ ecsRowData, onInvestigateInTimelineAlertClick, }: UseInvestigateInTimelineActionProps) => { + const { addError } = useAppToasts(); const { data: { search: searchStrategyClient, query }, } = useKibana().services; const dispatch = useDispatch(); + const { services } = useKibana(); + const { getExceptionListsItems } = useApi(services.http); + + const getExceptions = useCallback( + async (ecsData: Ecs): Promise => { + const exceptionsLists: ReadExceptionListSchema[] = ( + getField(ecsData, ALERT_RULE_EXCEPTIONS_LIST) ?? [] + ) + .map((list: string) => JSON.parse(list)) + .filter((list: ExceptionListIdentifiers) => list.type === 'detection'); + + const allExceptions: ExceptionListItemSchema[] = []; + + if (exceptionsLists.length > 0) { + for (const list of exceptionsLists) { + if (list.id && list.list_id && list.namespace_type) { + await getExceptionListsItems({ + lists: [ + { + id: list.id, + listId: list.list_id, + type: 'detection', + namespaceType: list.namespace_type, + }, + ], + filterOptions: [], + pagination: { + page: 0, + perPage: 10000, + total: 10000, + }, + showDetectionsListsOnly: true, + showEndpointListsOnly: false, + onSuccess: ({ exceptions }) => { + allExceptions.push(...exceptions); + }, + onError: (err: string[]) => { + addError(err, { + title: i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.fetchExceptionsFailure', + { defaultMessage: 'Error fetching exceptions.' } + ), + }); + }, + }); + } + } + } + return allExceptions; + }, + [addError, getExceptionListsItems] + ); + const filterManagerBackup = useMemo(() => query.filterManager, [query.filterManager]); const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); const { filterManager: activeFilterManager } = useDeepEqualSelector((state) => @@ -86,6 +151,7 @@ export const useInvestigateInTimeline = ({ ecsData: ecsRowData, searchStrategyClient, updateTimelineIsLoading, + getExceptions, }); } }, [ @@ -94,6 +160,7 @@ export const useInvestigateInTimeline = ({ onInvestigateInTimelineAlertClick, searchStrategyClient, updateTimelineIsLoading, + getExceptions, ]); const investigateInTimelineActionItems = useMemo( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index 1ca0fc9b7ca23..6773a4fddaeaf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + import type { ISearchStart } from '../../../../../../../src/plugins/data/public'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Ecs } from '../../../../common/ecs'; @@ -55,6 +57,7 @@ export interface SendAlertToTimelineActionProps { ecsData: Ecs | Ecs[]; updateTimelineIsLoading: UpdateTimelineLoading; searchStrategyClient: ISearchStart; + getExceptions: GetExceptions; } export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void; @@ -68,6 +71,7 @@ export interface CreateTimelineProps { } export type CreateTimeline = ({ from, timeline, to }: CreateTimelineProps) => void; +export type GetExceptions = (ecsData: Ecs) => Promise; export interface ThresholdAggregationData { thresholdFrom: string; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index 8aa8986d3e563..bbc10fcb2dfd4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -35,6 +35,12 @@ jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges', () = })); jest.mock('../../../cases/components/use_insert_timeline'); +jest.mock('../../../common/hooks/use_app_toasts', () => ({ + useAppToasts: jest.fn().mockReturnValue({ + addError: jest.fn(), + }), +})); + jest.mock('../../../common/hooks/use_experimental_features', () => ({ useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), })); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts index 1a664261215c2..346abaea66dd6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts @@ -193,6 +193,7 @@ export const buildEqlSearchRequest = ( lists: exceptionLists, excludeExceptions: true, chunkSize: 1024, + alias: null, }); const rangeFilter = buildTimeRangeFilter({ to, from, timestampOverride }); diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts index 27a3d376ece92..5beba022d8614 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts @@ -57,6 +57,7 @@ export const getAnomalies = async ( lists: params.exceptionItems, excludeExceptions: true, chunkSize: 1024, + alias: null, })?.query, }, }, From 1e33587b68b8852474b2a63c8d78845cf7d62a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 29 Mar 2022 15:16:32 -0400 Subject: [PATCH 018/108] [APM] fix cypress (#128411) * Fix synthtrace, some broken tests * fixing comparison test * fixing error count e2e * fixing error details test * Fix APM deep links * Add default environment to /apm/services request in home.spec.ts * fixing service overview filter test * Fix accessibility test in transactions overview page * testing CI * removing time arg * removing unused import * Fix e2e tests for infrastructure feature flag * fixing and skipping tests * fixing test * skipping flaky test Co-authored-by: Dario Gieselaar Co-authored-by: gbamparop Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/elastic-apm-synthtrace/src/index.ts | 1 + .../power_user/feature_flag/comparison.ts | 8 +++ .../power_user/feature_flag/infrastructure.ts | 12 ++--- .../power_user/rules/error_count.spec.ts | 12 +---- .../errors/error_details.spec.ts | 4 +- .../integration/read_only_user/home.spec.ts | 7 +-- .../service_inventory.spec.ts | 5 +- .../service_overview/header_filters.spec.ts | 7 +-- .../service_overview/service_overview.spec.ts | 51 +++++++------------ .../service_overview/time_comparison.spec.ts | 3 +- .../transactions_overview.spec.ts | 7 +-- .../apm/ftr_e2e/cypress/plugins/index.ts | 6 +-- .../apm/ftr_e2e/cypress/support/commands.ts | 18 ++++--- x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts | 2 +- x-pack/plugins/apm/ftr_e2e/synthtrace.ts | 2 +- x-pack/plugins/apm/public/plugin.ts | 34 ++++--------- x-pack/plugins/apm/scripts/test/e2e.js | 26 +--------- 17 files changed, 77 insertions(+), 128 deletions(-) diff --git a/packages/elastic-apm-synthtrace/src/index.ts b/packages/elastic-apm-synthtrace/src/index.ts index 0138a6525baf5..9c44f1902789c 100644 --- a/packages/elastic-apm-synthtrace/src/index.ts +++ b/packages/elastic-apm-synthtrace/src/index.ts @@ -15,3 +15,4 @@ export { createLogger, LogLevel } from './lib/utils/create_logger'; export type { Fields } from './lib/entity'; export type { ApmException, ApmSynthtraceEsClient } from './lib/apm'; export type { SpanIterable } from './lib/span_iterable'; +export { SpanArrayIterable } from './lib/span_iterable'; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.ts index 1bc4ad0a478b6..d5a28b6d85bb4 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.ts @@ -62,6 +62,14 @@ describe('Comparison feature flag', () => { }); describe('when comparison feature is disabled', () => { + // Reverts to default state, which is comparison enabled + after(() => { + cy.visit(settingsPath); + cy.get(comparisonToggle).click(); + cy.contains('Save changes').should('not.be.disabled'); + cy.contains('Save changes').click(); + }); + it('shows the flag as disabled in kibana advanced settings', () => { cy.visit(settingsPath); cy.get(comparisonToggle).click(); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/infrastructure.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/infrastructure.ts index 0b142e41ab607..1fade825bc4cb 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/infrastructure.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/infrastructure.ts @@ -41,12 +41,12 @@ describe('Infrastracture feature flag', () => { cy.get(infraToggle) .should('have.attr', 'aria-checked') - .and('equal', 'true'); + .and('equal', 'false'); }); - it('shows infrastructure tab in service overview page', () => { + it('hides infrastructure tab in service overview page', () => { cy.visit(serviceOverviewPath); - cy.contains('a[role="tab"]', 'Infrastructure').click(); + cy.contains('a[role="tab"]', 'Infrastructure').should('not.exist'); }); }); @@ -59,12 +59,12 @@ describe('Infrastracture feature flag', () => { cy.get(infraToggle) .should('have.attr', 'aria-checked') - .and('equal', 'false'); + .and('equal', 'true'); }); - it('hides infrastructure tab in service overview page', () => { + it('shows infrastructure tab in service overview page', () => { cy.visit(serviceOverviewPath); - cy.contains('a[role="tab"]', 'Infrastructure').should('not.exist'); + cy.contains('a[role="tab"]', 'Infrastructure').click(); }); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts index 89e203860179f..e53b84ca76496 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts @@ -49,8 +49,7 @@ describe('Rules', () => { // Create a rule in APM cy.visit('/app/apm/services'); cy.contains('Alerts and rules').click(); - cy.contains('Error count').click(); - cy.contains('Create threshold rule').click(); + cy.contains('Create error count rule').click(); // Check for the existence of this element to make sure the form // has loaded. @@ -69,10 +68,6 @@ describe('Rules', () => { before(() => { cy.loginAsPowerUser(); deleteAllRules(); - cy.intercept( - 'GET', - '/api/alerting/rules/_find?page=1&per_page=10&default_search_operator=AND&sort_field=name&sort_order=asc' - ).as('list rules API call'); }); after(() => { @@ -83,11 +78,6 @@ describe('Rules', () => { // Go to stack management cy.visit('/app/management/insightsAndAlerting/triggersActions/rules'); - // Wait for this call to finish so the create rule button does not disappear. - // The timeout is set high because at this point we're also waiting for the - // full page load. - cy.wait('@list rules API call', { timeout: 30000 }); - // Create a rule cy.contains('button', 'Create rule').click(); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts index beaf1837c834c..c131cb2dd36d7 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts @@ -14,7 +14,7 @@ const start = '2021-10-10T00:00:00.000Z'; const end = '2021-10-10T00:15:00.000Z'; const errorDetailsPageHref = url.format({ pathname: - '/app/apm/services/opbeans-java/errors/0000000000000000000000000Error%201', + '/app/apm/services/opbeans-java/errors/0000000000000000000000000Error%200', query: { rangeFrom: start, rangeTo: end, @@ -89,7 +89,7 @@ describe('Error details', () => { describe('when clicking on View x occurences in discover', () => { it('should redirects the user to discover', () => { cy.visit(errorDetailsPageHref); - cy.contains('span', 'Discover').click(); + cy.contains('View 1 occurrence in Discover.').click(); cy.url().should('include', 'app/discover'); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index 1e09ec6dbf7c1..f0f306a538331 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -28,7 +28,8 @@ const apisToIntercept = [ }, ]; -describe('Home page', () => { +// flaky test +describe.skip('Home page', () => { before(async () => { await synthtrace.index( opbeans({ @@ -46,12 +47,12 @@ describe('Home page', () => { cy.loginAsReadOnlyUser(); }); - it('Redirects to service page with rangeFrom and rangeTo added to the URL', () => { + it('Redirects to service page with environment, rangeFrom and rangeTo added to the URL', () => { cy.visit('/app/apm'); cy.url().should( 'include', - 'app/apm/services?rangeFrom=now-15m&rangeTo=now' + 'app/apm/services?environment=ENVIRONMENT_ALL&rangeFrom=now-15m&rangeTo=now' ); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts index 40afece0ce908..ecee9c3c4f63e 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import moment from 'moment'; import url from 'url'; import { synthtrace } from '../../../../synthtrace'; import { opbeans } from '../../../fixtures/synthtrace/opbeans'; @@ -104,8 +105,8 @@ describe('When navigating to the service inventory', () => { cy.wait(aliasNames); cy.selectAbsoluteTimeRange( - 'Oct 10, 2021 @ 01:00:00.000', - 'Oct 10, 2021 @ 01:30:00.000' + moment(timeRange.rangeFrom).subtract(5, 'm').toISOString(), + moment(timeRange.rangeTo).subtract(5, 'm').toISOString() ); cy.contains('Update').click(); cy.wait(aliasNames); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts index 49d7104f44a88..9caf8ede5e527 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts @@ -46,11 +46,6 @@ const apisToIntercept = [ '/internal/apm/services/opbeans-node/service_overview_instances/main_statistics?*', name: 'instancesMainStatisticsRequest', }, - { - endpoint: - '/internal/apm/services/opbeans-node/errors/groups/main_statistics?*', - name: 'errorGroupsMainStatisticsRequest', - }, { endpoint: '/internal/apm/services/opbeans-node/transaction/charts/breakdown?*', @@ -144,7 +139,7 @@ describe('Service overview - header filters', () => { .find('li') .first() .click(); - cy.get('[data-test-subj="suggestionContainer"]').realPress('{enter}'); + cy.get('[data-test-subj="headerFilterKuerybar"]').type('{enter}'); cy.url().should('include', '&kuery=transaction.name'); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts index 4dd66f6dd9311..0935d23d02696 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts @@ -5,6 +5,7 @@ * 2.0. */ +import moment from 'moment'; import url from 'url'; import { synthtrace } from '../../../../synthtrace'; import { opbeans } from '../../../fixtures/synthtrace/opbeans'; @@ -23,12 +24,12 @@ const apiRequestsToIntercept = [ { endpoint: '/internal/apm/services/opbeans-node/transactions/groups/main_statistics?*', - aliasName: 'transactionsGroupsMainStadisticsRequest', + aliasName: 'transactionsGroupsMainStatisticsRequest', }, { endpoint: '/internal/apm/services/opbeans-node/errors/groups/main_statistics?*', - aliasName: 'errorsGroupsMainStadisticsRequest', + aliasName: 'errorsGroupsMainStatisticsRequest', }, { endpoint: @@ -59,18 +60,18 @@ const apiRequestsToInterceptWithComparison = [ { endpoint: '/internal/apm/services/opbeans-node/transactions/groups/detailed_statistics?*', - aliasName: 'transactionsGroupsDetailedStadisticsRequest', + aliasName: 'transactionsGroupsDetailedStatisticsRequest', }, { endpoint: '/internal/apm/services/opbeans-node/service_overview_instances/main_statistics?*', - aliasName: 'instancesMainStadisticsRequest', + aliasName: 'instancesMainStatisticsRequest', }, { endpoint: '/internal/apm/services/opbeans-node/service_overview_instances/detailed_statistics?*', - aliasName: 'instancesDetailedStadisticsRequest', + aliasName: 'instancesDetailedStatisticsRequest', }, ]; @@ -84,7 +85,8 @@ const aliasNamesWithComparison = apiRequestsToInterceptWithComparison.map( const aliasNames = [...aliasNamesNoComparison, ...aliasNamesWithComparison]; -describe('Service Overview', () => { +// flaky test +describe.skip('Service Overview', () => { before(async () => { await synthtrace.index( opbeans({ @@ -104,37 +106,16 @@ describe('Service Overview', () => { cy.visit(baseUrl); }); - it('has no detectable a11y violations on load', () => { + it('renders all components on the page', () => { cy.contains('opbeans-node'); // set skipFailures to true to not fail the test when there are accessibility failures checkA11y({ skipFailures: true }); - }); - - it('transaction latency chart', () => { cy.get('[data-test-subj="latencyChart"]'); - }); - - it('throughput chart', () => { cy.get('[data-test-subj="throughput"]'); - }); - - it('transactions group table', () => { cy.get('[data-test-subj="transactionsGroupTable"]'); - }); - - it('error table', () => { cy.get('[data-test-subj="serviceOverviewErrorsTable"]'); - }); - - it('dependencies table', () => { cy.get('[data-test-subj="dependenciesTable"]'); - }); - - it('instances latency distribution chart', () => { cy.get('[data-test-subj="instancesLatencyDistribution"]'); - }); - - it('instances table', () => { cy.get('[data-test-subj="serviceOverviewInstancesTable"]'); }); }); @@ -241,16 +222,18 @@ describe('Service Overview', () => { it('when selecting a different time range and clicking the update button', () => { cy.wait(aliasNames, { requestTimeout: 10000 }); - cy.selectAbsoluteTimeRange( - 'Oct 10, 2021 @ 01:00:00.000', - 'Oct 10, 2021 @ 01:30:00.000' - ); + const timeStart = moment(start).subtract(5, 'm').toISOString(); + const timeEnd = moment(end).subtract(5, 'm').toISOString(); + + cy.selectAbsoluteTimeRange(timeStart, timeEnd); + cy.contains('Update').click(); cy.expectAPIsToHaveBeenCalledWith({ apisIntercepted: aliasNames, - value: - 'start=2021-10-10T00%3A00%3A00.000Z&end=2021-10-10T00%3A30%3A00.000Z', + value: `start=${encodeURIComponent( + new Date(timeStart).toISOString() + )}&end=${encodeURIComponent(new Date(timeEnd).toISOString())}`, }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts index 955b429a567a7..f844969850b84 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts @@ -50,7 +50,8 @@ const apisToIntercept = [ }, ]; -describe('Service overview: Time Comparison', () => { +// Skipping tests since it's flaky. +describe.skip('Service overview: Time Comparison', () => { before(async () => { await synthtrace.index( opbeans({ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts index fb8468f42474e..c5676dfb9c532 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts @@ -38,9 +38,10 @@ describe('Transactions Overview', () => { it('has no detectable a11y violations on load', () => { cy.visit(serviceTransactionsHref); - cy.contains('aria-selected="true"', 'Transactions').should( - 'have.class', - 'euiTab-isSelected' + cy.get('a:contains(Transactions)').should( + 'have.attr', + 'aria-selected', + 'true' ); // set skipFailures to true to not fail the test when there are accessibility failures checkA11y({ skipFailures: true }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts b/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts index 6b6aff63976d5..093ededbcc247 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts @@ -8,7 +8,7 @@ import { apm, createLogger, LogLevel, - SpanIterable, + SpanArrayIterable, } from '@elastic/apm-synthtrace'; import { createEsClientForTesting } from '@kbn/test'; @@ -46,8 +46,8 @@ const plugin: Cypress.PluginConfig = (on, config) => { ); on('task', { - 'synthtrace:index': async (events: SpanIterable) => { - await synthtraceEsClient.index(events); + 'synthtrace:index': async (events: Array>) => { + await synthtraceEsClient.index(new SpanArrayIterable(events)); return null; }, 'synthtrace:clean': async () => { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 89d8fa620c183..3d8d86145cdac 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -7,7 +7,9 @@ import 'cypress-real-events/support'; import { Interception } from 'cypress/types/net-stubbing'; import 'cypress-axe'; -import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/test'; +import moment from 'moment'; +// Commenting this out since it's breaking the tests. It was caused by https://github.com/elastic/kibana/commit/bef90a58663b6c4b668a7fe0ce45a002fb68c474#diff-8a4659c6955a712376fe5ca0d81636164d1b783a63fe9d1a23da4850bd0dfce3R10 +// import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/test'; Cypress.Commands.add('loginAsReadOnlyUser', () => { cy.loginAs({ username: 'apm_read_user', password: 'changeme' }); @@ -47,16 +49,18 @@ Cypress.Commands.add('changeTimeRange', (value: string) => { Cypress.Commands.add( 'selectAbsoluteTimeRange', (start: string, end: string) => { + const format = 'MMM D, YYYY @ HH:mm:ss.SSS'; + cy.get('[data-test-subj="superDatePickerstartDatePopoverButton"]').click(); cy.get('[data-test-subj="superDatePickerAbsoluteDateInput"]') .eq(0) .clear() - .type(start, { force: true }); + .type(moment(start).format(format), { force: true }); cy.get('[data-test-subj="superDatePickerendDatePopoverButton"]').click(); cy.get('[data-test-subj="superDatePickerAbsoluteDateInput"]') .eq(1) .clear() - .type(end, { force: true }); + .type(moment(end).format(format), { force: true }); } ); @@ -84,11 +88,13 @@ Cypress.Commands.add( // A11y configuration const axeConfig = { - ...AXE_CONFIG, + // See comment on line 11 + // ...AXE_CONFIG, }; const axeOptions = { - ...AXE_OPTIONS, - runOnly: [...AXE_OPTIONS.runOnly, 'best-practice'], + // See comment on line 11 + // ...AXE_OPTIONS, + // runOnly: [...AXE_OPTIONS.runOnly, 'best-practice'], }; export const checkA11y = ({ skipFailures }: { skipFailures: boolean }) => { diff --git a/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts b/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts index 768ad9b3f79f6..34d6da688de82 100644 --- a/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts +++ b/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts @@ -23,7 +23,7 @@ async function testRunner({ getService }: FtrProviderContext) { const result = await cypressStart(getService, cypress.run); if (result && (result.status === 'failed' || result.totalFailed > 0)) { - throw new Error(`APM Cypress tests failed`); + process.exit(1); } } diff --git a/x-pack/plugins/apm/ftr_e2e/synthtrace.ts b/x-pack/plugins/apm/ftr_e2e/synthtrace.ts index 2409dded17780..775951dfe85c7 100644 --- a/x-pack/plugins/apm/ftr_e2e/synthtrace.ts +++ b/x-pack/plugins/apm/ftr_e2e/synthtrace.ts @@ -9,7 +9,7 @@ import { SpanIterable } from '@elastic/apm-synthtrace'; export const synthtrace = { index: (events: SpanIterable) => new Promise((resolve) => { - cy.task('synthtrace:index', events).then(resolve); + cy.task('synthtrace:index', events.toArray()).then(resolve); }), clean: () => new Promise((resolve) => { diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 1968f35791f40..83aad1b3b4fe6 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -263,30 +263,16 @@ export class ApmPlugin implements Plugin { icon: 'plugins/apm/public/icon.svg', category: DEFAULT_APP_CATEGORIES.observability, deepLinks: [ - { - id: 'services', - title: servicesTitle, - // path: serviceGroupsEnabled ? '/service-groups' : '/services', - deepLinks: serviceGroupsEnabled - ? [ - { - id: 'service-groups-list', - title: 'Service groups', - path: '/service-groups', - }, - { - id: 'service-groups-services', - title: servicesTitle, - path: '/services', - }, - { - id: 'service-groups-service-map', - title: serviceMapTitle, - path: '/service-map', - }, - ] - : [], - }, + ...(serviceGroupsEnabled + ? [ + { + id: 'service-groups-list', + title: 'Service groups', + path: '/service-groups', + }, + ] + : []), + { id: 'services', title: servicesTitle, path: '/services' }, { id: 'traces', title: tracesTitle, path: '/traces' }, { id: 'service-map', title: serviceMapTitle, path: '/service-map' }, { id: 'backends', title: dependenciesTitle, path: '/backends' }, diff --git a/x-pack/plugins/apm/scripts/test/e2e.js b/x-pack/plugins/apm/scripts/test/e2e.js index 8f3461af238bf..148d5011b1ecb 100644 --- a/x-pack/plugins/apm/scripts/test/e2e.js +++ b/x-pack/plugins/apm/scripts/test/e2e.js @@ -7,7 +7,6 @@ /* eslint-disable no-console */ -const { times } = require('lodash'); const path = require('path'); const yargs = require('yargs'); const childProcess = require('child_process'); @@ -46,11 +45,6 @@ const { argv } = yargs(process.argv.slice(2)) type: 'boolean', description: 'stop tests after the first failure', }) - .option('times', { - default: 1, - type: 'number', - description: 'Repeat the test n number of times', - }) .help(); const { server, runner, open, grep, bail, kibanaInstallDir } = argv; @@ -70,22 +64,4 @@ const bailArg = bail ? `--bail` : ''; const cmd = `node ../../../../scripts/${ftrScript} --config ${config} ${grepArg} ${bailArg} --kibana-install-dir '${kibanaInstallDir}'`; console.log(`Running "${cmd}"`); - -if (argv.times > 1) { - console.log(`The command will be executed ${argv.times} times`); -} - -const runCounter = { succeeded: 0, failed: 0, remaining: argv.times }; -times(argv.times, () => { - try { - childProcess.execSync(cmd, { cwd: e2eDir, stdio: 'inherit' }); - runCounter.succeeded++; - } catch (e) { - runCounter.failed++; - } - runCounter.remaining--; - - if (argv.times > 1) { - console.log(runCounter); - } -}); +childProcess.execSync(cmd, { cwd: e2eDir, stdio: 'inherit' }); From 92d65484a5deff0a8da8b4661fc72ae97f574537 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 29 Mar 2022 13:19:58 -0600 Subject: [PATCH 019/108] [plugin-discovery] move logic to a package (#128684) --- .github/CODEOWNERS | 1 + package.json | 2 + packages/BUILD.bazel | 2 + packages/kbn-dev-utils/BUILD.bazel | 3 +- packages/kbn-dev-utils/src/index.ts | 1 - .../src/plugin_list/discover_plugins.ts | 2 +- .../src/api_docs/find_plugins.ts | 2 +- .../src/optimizer/kibana_platform_plugins.ts | 2 +- packages/kbn-plugin-discovery/BUILD.bazel | 124 ++++++++++++++++++ packages/kbn-plugin-discovery/README.md | 3 + packages/kbn-plugin-discovery/jest.config.js | 13 ++ packages/kbn-plugin-discovery/package.json | 7 + .../src}/index.ts | 1 + .../src}/parse_kibana_platform_plugin.ts | 6 +- .../src/plugin_search_paths.ts | 26 ++++ ...simple_kibana_platform_plugin_discovery.ts | 7 +- packages/kbn-plugin-discovery/tsconfig.json | 18 +++ .../src/load_kibana_platform_plugin.ts | 3 +- .../src/lib/bazel_cli_config.ts | 1 + src/dev/plugin_discovery/find_plugins.ts | 2 +- src/dev/plugin_discovery/get_plugin_deps.ts | 2 +- ...n_find_plugins_ready_migrate_to_ts_refs.ts | 3 +- yarn.lock | 8 ++ 23 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 packages/kbn-plugin-discovery/BUILD.bazel create mode 100644 packages/kbn-plugin-discovery/README.md create mode 100644 packages/kbn-plugin-discovery/jest.config.js create mode 100644 packages/kbn-plugin-discovery/package.json rename packages/{kbn-dev-utils/src/plugins => kbn-plugin-discovery/src}/index.ts (92%) rename packages/{kbn-dev-utils/src/plugins => kbn-plugin-discovery/src}/parse_kibana_platform_plugin.ts (94%) create mode 100644 packages/kbn-plugin-discovery/src/plugin_search_paths.ts rename packages/{kbn-dev-utils/src/plugins => kbn-plugin-discovery/src}/simple_kibana_platform_plugin_discovery.ts (86%) create mode 100644 packages/kbn-plugin-discovery/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c89fe8be4a654..c07121d3bc07f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -232,6 +232,7 @@ /packages/kbn-utils/ @elastic/kibana-operations /packages/kbn-cli-dev-mode/ @elastic/kibana-operations /packages/kbn-generate/ @elastic/kibana-operations +/packages/kbn-plugin-discovery/ @elastic/kibana-operations /src/cli/keystore/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /.github/workflows/ @elastic/kibana-operations diff --git a/package.json b/package.json index 35edc14f01325..8b434d291013e 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,7 @@ "@kbn/logging-mocks": "link:bazel-bin/packages/kbn-logging-mocks", "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco", + "@kbn/plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery", "@kbn/react-field": "link:bazel-bin/packages/kbn-react-field", "@kbn/rule-data-utils": "link:bazel-bin/packages/kbn-rule-data-utils", "@kbn/securitysolution-autocomplete": "link:bazel-bin/packages/kbn-securitysolution-autocomplete", @@ -199,6 +200,7 @@ "@turf/helpers": "6.0.1", "@turf/length": "^6.0.2", "@types/jsonwebtoken": "^8.5.6", + "@types/kbn__plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery/npm_module_types", "@types/kbn__shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components/npm_module_types", "@types/kbn__shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types", "@types/kbn__shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index e880df5f55782..f2ca181877883 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -46,6 +46,7 @@ filegroup( "//packages/kbn-mapbox-gl:build", "//packages/kbn-monaco:build", "//packages/kbn-optimizer:build", + "//packages/kbn-plugin-discovery:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-plugin-helpers:build", "//packages/kbn-react-field:build", @@ -123,6 +124,7 @@ filegroup( "//packages/kbn-mapbox-gl:build_types", "//packages/kbn-monaco:build_types", "//packages/kbn-optimizer:build_types", + "//packages/kbn-plugin-discovery:build_types", "//packages/kbn-plugin-generator:build_types", "//packages/kbn-plugin-helpers:build_types", "//packages/kbn-react-field:build_types", diff --git a/packages/kbn-dev-utils/BUILD.bazel b/packages/kbn-dev-utils/BUILD.bazel index 5f31a8ba07481..7b60e46d030a3 100644 --- a/packages/kbn-dev-utils/BUILD.bazel +++ b/packages/kbn-dev-utils/BUILD.bazel @@ -46,6 +46,7 @@ NPM_MODULE_EXTRA_FILES = [ RUNTIME_DEPS = [ "//packages/kbn-std", "//packages/kbn-utils", + "//packages/kbn-plugin-discovery", "@npm//@babel/core", "@npm//axios", "@npm//chalk", @@ -54,7 +55,6 @@ RUNTIME_DEPS = [ "@npm//execa", "@npm//exit-hook", "@npm//getopts", - "@npm//globby", "@npm//jest-diff", "@npm//load-json-file", "@npm//markdown-it", @@ -72,6 +72,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-std:npm_module_types", "//packages/kbn-utils:npm_module_types", + "//packages/kbn-plugin-discovery:npm_module_types", "@npm//@babel/parser", "@npm//@babel/types", "@npm//@types/babel__core", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index db96db46bde71..cb75cbdf62782 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -27,7 +27,6 @@ export * from './axios'; export * from './stdio'; export * from './ci_stats_reporter'; export * from './plugin_list'; -export * from './plugins'; export * from './streams'; export * from './babel'; export * from './extract'; diff --git a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts index 966d27e6dc620..5f0b623b29b18 100644 --- a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts +++ b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts @@ -12,8 +12,8 @@ import Fs from 'fs'; import MarkdownIt from 'markdown-it'; import cheerio from 'cheerio'; import { REPO_ROOT } from '@kbn/utils'; +import { simpleKibanaPlatformPluginDiscovery } from '@kbn/plugin-discovery'; -import { simpleKibanaPlatformPluginDiscovery } from '../plugins'; import { extractAsciidocInfo } from './extract_asciidoc_info'; export interface Plugin { diff --git a/packages/kbn-docs-utils/src/api_docs/find_plugins.ts b/packages/kbn-docs-utils/src/api_docs/find_plugins.ts index 774452a6f1f9f..ebe329029302c 100644 --- a/packages/kbn-docs-utils/src/api_docs/find_plugins.ts +++ b/packages/kbn-docs-utils/src/api_docs/find_plugins.ts @@ -12,7 +12,7 @@ import globby from 'globby'; import loadJsonFile from 'load-json-file'; import { getPluginSearchPaths } from '@kbn/config'; -import { simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; +import { simpleKibanaPlatformPluginDiscovery } from '@kbn/plugin-discovery'; import { REPO_ROOT } from '@kbn/utils'; import { ApiScope, PluginOrPackage } from './types'; diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts index ec7b08a009b32..833d4cd8b8110 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; +import { simpleKibanaPlatformPluginDiscovery } from '@kbn/plugin-discovery'; export interface KibanaPlatformPlugin { readonly directory: string; diff --git a/packages/kbn-plugin-discovery/BUILD.bazel b/packages/kbn-plugin-discovery/BUILD.bazel new file mode 100644 index 0000000000000..b2e5ecde9c426 --- /dev/null +++ b/packages/kbn-plugin-discovery/BUILD.bazel @@ -0,0 +1,124 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "kbn-plugin-discovery" +PKG_REQUIRE_NAME = "@kbn/plugin-discovery" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//globby", + "@npm//load-json-file", + "@npm//normalize-path", + "@npm//tslib", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/normalize-path", + "@npm//globby", + "@npm//load-json-file", + "@npm//normalize-path", + "@npm//tslib", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-plugin-discovery/README.md b/packages/kbn-plugin-discovery/README.md new file mode 100644 index 0000000000000..7b433b0fdec72 --- /dev/null +++ b/packages/kbn-plugin-discovery/README.md @@ -0,0 +1,3 @@ +# @kbn/plugin-discovery + +Logic used to find plugins in the repository. diff --git a/packages/kbn-plugin-discovery/jest.config.js b/packages/kbn-plugin-discovery/jest.config.js new file mode 100644 index 0000000000000..37d7a4f2b63a2 --- /dev/null +++ b/packages/kbn-plugin-discovery/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/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-plugin-discovery'], +}; diff --git a/packages/kbn-plugin-discovery/package.json b/packages/kbn-plugin-discovery/package.json new file mode 100644 index 0000000000000..7758cd5773215 --- /dev/null +++ b/packages/kbn-plugin-discovery/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/plugin-discovery", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/kbn-dev-utils/src/plugins/index.ts b/packages/kbn-plugin-discovery/src/index.ts similarity index 92% rename from packages/kbn-dev-utils/src/plugins/index.ts rename to packages/kbn-plugin-discovery/src/index.ts index e9ca66a892ebf..a382b45515e53 100644 --- a/packages/kbn-dev-utils/src/plugins/index.ts +++ b/packages/kbn-plugin-discovery/src/index.ts @@ -7,4 +7,5 @@ */ export * from './parse_kibana_platform_plugin'; +export * from './plugin_search_paths'; export * from './simple_kibana_platform_plugin_discovery'; diff --git a/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts b/packages/kbn-plugin-discovery/src/parse_kibana_platform_plugin.ts similarity index 94% rename from packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts rename to packages/kbn-plugin-discovery/src/parse_kibana_platform_plugin.ts index c34192a8396e8..d7cab724cde6b 100644 --- a/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts +++ b/packages/kbn-plugin-discovery/src/parse_kibana_platform_plugin.ts @@ -12,7 +12,7 @@ import loadJsonFile from 'load-json-file'; export interface KibanaPlatformPlugin { readonly directory: string; readonly manifestPath: string; - readonly manifest: Manifest; + readonly manifest: KibanaPlatformPluginManifest; } function isValidDepsDeclaration(input: unknown, type: string): string[] { @@ -23,7 +23,7 @@ function isValidDepsDeclaration(input: unknown, type: string): string[] { throw new TypeError(`The "${type}" in plugin manifest should be an array of strings.`); } -interface Manifest { +export interface KibanaPlatformPluginManifest { id: string; ui: boolean; server: boolean; @@ -50,7 +50,7 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP throw new TypeError('expected new platform manifest path to be absolute'); } - const manifest: Partial = loadJsonFile.sync(manifestPath); + const manifest: Partial = loadJsonFile.sync(manifestPath); if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); } diff --git a/packages/kbn-plugin-discovery/src/plugin_search_paths.ts b/packages/kbn-plugin-discovery/src/plugin_search_paths.ts new file mode 100644 index 0000000000000..fe6f0a44b044e --- /dev/null +++ b/packages/kbn-plugin-discovery/src/plugin_search_paths.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 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 { resolve } from 'path'; + +export interface SearchOptions { + rootDir: string; + oss: boolean; + examples: boolean; +} + +export function getPluginSearchPaths({ rootDir, oss, examples }: SearchOptions) { + return [ + resolve(rootDir, 'src', 'plugins'), + ...(oss ? [] : [resolve(rootDir, 'x-pack', 'plugins')]), + resolve(rootDir, 'plugins'), + ...(examples ? [resolve(rootDir, 'examples')] : []), + ...(examples && !oss ? [resolve(rootDir, 'x-pack', 'examples')] : []), + resolve(rootDir, '..', 'kibana-extra'), + ]; +} diff --git a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-plugin-discovery/src/simple_kibana_platform_plugin_discovery.ts similarity index 86% rename from packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts rename to packages/kbn-plugin-discovery/src/simple_kibana_platform_plugin_discovery.ts index 2381faefbff29..ebf476a668851 100644 --- a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts +++ b/packages/kbn-plugin-discovery/src/simple_kibana_platform_plugin_discovery.ts @@ -11,12 +11,15 @@ import Path from 'path'; import globby from 'globby'; import normalize from 'normalize-path'; -import { parseKibanaPlatformPlugin } from './parse_kibana_platform_plugin'; +import { parseKibanaPlatformPlugin, KibanaPlatformPlugin } from './parse_kibana_platform_plugin'; /** * Helper to find the new platform plugins. */ -export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], pluginPaths: string[]) { +export function simpleKibanaPlatformPluginDiscovery( + scanDirs: string[], + pluginPaths: string[] +): KibanaPlatformPlugin[] { const patterns = Array.from( new Set([ // find kibana.json files up to 5 levels within the scan dir diff --git a/packages/kbn-plugin-discovery/tsconfig.json b/packages/kbn-plugin-discovery/tsconfig.json new file mode 100644 index 0000000000000..789c6b3111115 --- /dev/null +++ b/packages/kbn-plugin-discovery/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-plugin-helpers/src/load_kibana_platform_plugin.ts b/packages/kbn-plugin-helpers/src/load_kibana_platform_plugin.ts index c1b074540158d..f258747350520 100644 --- a/packages/kbn-plugin-helpers/src/load_kibana_platform_plugin.ts +++ b/packages/kbn-plugin-helpers/src/load_kibana_platform_plugin.ts @@ -9,7 +9,8 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; -import { parseKibanaPlatformPlugin, KibanaPlatformPlugin, createFailError } from '@kbn/dev-utils'; +import { parseKibanaPlatformPlugin, KibanaPlatformPlugin } from '@kbn/plugin-discovery'; +import { createFailError } from '@kbn/dev-utils'; export type Plugin = KibanaPlatformPlugin; diff --git a/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts b/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts index 6f02160a1cb3f..7527ae35d4357 100644 --- a/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts +++ b/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts @@ -22,6 +22,7 @@ const TYPE_SUMMARIZER_PACKAGES = [ '@kbn/analytics', '@kbn/apm-config-loader', '@kbn/apm-utils', + '@kbn/plugin-discovery', ]; type TypeSummarizerType = 'api-extractor' | 'type-summarizer'; diff --git a/src/dev/plugin_discovery/find_plugins.ts b/src/dev/plugin_discovery/find_plugins.ts index 53a53bc08e15b..7e46894813f0e 100644 --- a/src/dev/plugin_discovery/find_plugins.ts +++ b/src/dev/plugin_discovery/find_plugins.ts @@ -8,7 +8,7 @@ import Path from 'path'; import { getPluginSearchPaths } from '@kbn/config'; -import { KibanaPlatformPlugin, simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; +import { KibanaPlatformPlugin, simpleKibanaPlatformPluginDiscovery } from '@kbn/plugin-discovery'; import { REPO_ROOT } from '@kbn/utils'; diff --git a/src/dev/plugin_discovery/get_plugin_deps.ts b/src/dev/plugin_discovery/get_plugin_deps.ts index ec158e38a99d1..291c9de71b480 100644 --- a/src/dev/plugin_discovery/get_plugin_deps.ts +++ b/src/dev/plugin_discovery/get_plugin_deps.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { KibanaPlatformPlugin } from '@kbn/plugin-discovery'; interface AllOptions { id: string; diff --git a/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts b/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts index a71ada6f03fbf..39fc75fc2e379 100644 --- a/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts +++ b/src/dev/run_find_plugins_ready_migrate_to_ts_refs.ts @@ -10,7 +10,8 @@ import Path from 'path'; import Fs from 'fs'; import JSON5 from 'json5'; import { get } from 'lodash'; -import { run, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-utils'; +import { KibanaPlatformPlugin } from '@kbn/plugin-discovery'; import { getPluginDeps, findPlugins } from './plugin_discovery'; interface AllOptions { diff --git a/yarn.lock b/yarn.lock index ce7e815915a8b..d097eefeb7485 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3033,6 +3033,10 @@ version "0.0.0" uid "" +"@kbn/plugin-discovery@link:bazel-bin/packages/kbn-plugin-discovery": + version "0.0.0" + uid "" + "@kbn/plugin-generator@link:bazel-bin/packages/kbn-plugin-generator": version "0.0.0" uid "" @@ -6023,6 +6027,10 @@ version "0.0.0" uid "" +"@types/kbn__plugin-discovery@link:bazel-bin/packages/kbn-plugin-discovery/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__plugin-generator@link:bazel-bin/packages/kbn-plugin-generator/npm_module_types": version "0.0.0" uid "" From 59db86b3c772753b7bec6e46d41ee018af36dcfc Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 29 Mar 2022 15:28:05 -0400 Subject: [PATCH 020/108] [CI] Build TS Refs before api docs --- .buildkite/scripts/steps/build_api_docs.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.buildkite/scripts/steps/build_api_docs.sh b/.buildkite/scripts/steps/build_api_docs.sh index 2f63d6efa0941..dcb9a2aea7d37 100755 --- a/.buildkite/scripts/steps/build_api_docs.sh +++ b/.buildkite/scripts/steps/build_api_docs.sh @@ -2,6 +2,8 @@ set -euo pipefail +export BUILD_TS_REFS_DISABLE=false + .buildkite/scripts/bootstrap.sh echo "--- Build API Docs" From 7d94876df49c373e17ffdf7024c7f1af23ae7570 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 29 Mar 2022 19:30:00 +0000 Subject: [PATCH 021/108] Revert "[CI] Build TS Refs before api docs" This reverts commit 59db86b3c772753b7bec6e46d41ee018af36dcfc. --- .buildkite/scripts/steps/build_api_docs.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/.buildkite/scripts/steps/build_api_docs.sh b/.buildkite/scripts/steps/build_api_docs.sh index dcb9a2aea7d37..2f63d6efa0941 100755 --- a/.buildkite/scripts/steps/build_api_docs.sh +++ b/.buildkite/scripts/steps/build_api_docs.sh @@ -2,8 +2,6 @@ set -euo pipefail -export BUILD_TS_REFS_DISABLE=false - .buildkite/scripts/bootstrap.sh echo "--- Build API Docs" From 4ce6f20fec7233be830142f1a232d20bddf8e56d Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 29 Mar 2022 15:40:33 -0400 Subject: [PATCH 022/108] Security Solution: reimplement filterBrowserFieldsByFieldName (#128779) The function filterBrowserFieldsByFieldName is being run 4+ times when loading pages in the Security app. With a large number of fields, such as is found in production environment, this function can take 10+ seconds to completed. With this implementation, it should run a bit quicker. --- .../t_grid/toolbar/fields_browser/helpers.tsx | 75 ++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index c0e1076073026..293121a4d3a61 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -37,42 +37,63 @@ export const getFieldCount = (category: Partial | undefined): numb * Filters the specified `BrowserFields` to return a new collection where every * category contains at least one field name that matches the specified substring. */ -export const filterBrowserFieldsByFieldName = ({ +export function filterBrowserFieldsByFieldName({ browserFields, substring, }: { browserFields: BrowserFields; substring: string; -}): BrowserFields => { +}): BrowserFields { const trimmedSubstring = substring.trim(); + // an empty search param will match everything, so return the original browserFields if (trimmedSubstring === '') { return browserFields; } - - // filter each category such that it only contains fields with field names - // that contain the specified substring: - const filteredBrowserFields: BrowserFields = Object.keys(browserFields).reduce( - (filteredCategories, categoryId) => ({ - ...filteredCategories, - [categoryId]: { - ...browserFields[categoryId], - fields: pickBy( - ({ name }) => name != null && name.includes(trimmedSubstring), - browserFields[categoryId].fields - ), - }, - }), - {} - ); - - // only pick non-empty categories from the filtered browser fields - const nonEmptyCategories: BrowserFields = pickBy( - (category) => categoryHasFields(category), - filteredBrowserFields - ); - - return nonEmptyCategories; -}; + const result: Record> = {}; + for (const [categoryName, categoryDescriptor] of Object.entries(browserFields)) { + if (!categoryDescriptor.fields) { + // ignore any category that is missing fields. This is not expected to happen. + // eslint-disable-next-line no-continue + continue; + } + + // keep track of whether this category had a matching field, if so, we should emit it into the result + let hadAMatch = false; + + // The fields that matched, for this `categoryName` + const filteredFields: Record> = {}; + + for (const [fieldName, fieldDescriptor] of Object.entries(categoryDescriptor.fields)) { + // For historical reasons, we consider the name as it appears on the field descriptor, not the `fieldName` (attribute name) itself. + // It is unclear if there is any point in continuing to do this. + const fieldNameFromDescriptor = fieldDescriptor.name; + + if (!fieldNameFromDescriptor) { + // Ignore any field that is missing a name in its descriptor. This is not expected to happen. + // eslint-disable-next-line no-continue + continue; + } + + // Check if this field matches (via substring comparison) the passed substring + if (fieldNameFromDescriptor !== null && fieldNameFromDescriptor.includes(trimmedSubstring)) { + // this field is a match, so we should emit this category into the result object. + hadAMatch = true; + + // emit this field + filteredFields[fieldName] = fieldDescriptor; + } + } + + if (hadAMatch) { + // if at least one field matches, emit the category, but replace the `fields` attribute with the filtered fields + result[categoryName] = { + ...browserFields[categoryName], + fields: filteredFields, + }; + } + } + return result; +} /** * Filters the selected `BrowserFields` to return a new collection where every From 37952e8d29f5e60fc5f0dea40b8a817c8e1d1508 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 29 Mar 2022 15:48:46 -0400 Subject: [PATCH 023/108] [Security Solution] Update documents_volume types in telemetry to match Endpoint output (#128169) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/security_solution/server/lib/telemetry/types.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 c1c65a428f62d..15c92740e3a71 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -208,6 +208,10 @@ export interface EndpointMetrics { registry_events: DocumentsVolumeMetrics; network_events: DocumentsVolumeMetrics; overall: DocumentsVolumeMetrics; + alerts: DocumentsVolumeMetrics; + diagnostic_alerts: DocumentsVolumeMetrics; + dns_events: DocumentsVolumeMetrics; + security_events: DocumentsVolumeMetrics; }; malicious_behavior_rules: Array<{ id: string; endpoint_uptime_percent: number }>; system_impact: Array<{ From b1608537204d51a10e9b71457a7b33bf57039a5c Mon Sep 17 00:00:00 2001 From: Michael Katsoulis Date: Tue, 29 Mar 2022 22:49:48 +0300 Subject: [PATCH 024/108] [Fleet] Update add agent instructions in fleet managed mode for Kubernetes (#127703) --- .../plugins/fleet/common/constants/routes.ts | 7 + .../plugins/fleet/common/services/routes.ts | 9 + .../common/types/rest_spec/agent_policy.ts | 4 + .../agent_enrollment_flyout/hooks.tsx | 66 ++++++ .../agent_enrollment_flyout/index.tsx | 40 ++-- .../managed_instructions.tsx | 18 +- .../standalone_instructions.tsx | 40 +--- .../agent_enrollment_flyout/steps.tsx | 184 ++++++++++++--- .../agent_enrollment_flyout/types.ts | 4 + .../enrollment_instructions/manual/index.tsx | 6 +- .../manual/platform_selector.tsx | 5 +- .../fleet/public/hooks/use_request/k8s.ts | 20 ++ .../server/routes/agent_policy/handlers.ts | 57 +++++ .../fleet/server/routes/agent_policy/index.ts | 29 +++ .../fleet/server/services/agent_policy.ts | 26 ++- .../server/services/elastic_agent_manifest.ts | 220 +++++++++++++++++- .../server/types/rest_spec/agent_policy.ts | 8 + 17 files changed, 654 insertions(+), 89 deletions(-) create mode 100644 x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx create mode 100644 x-pack/plugins/fleet/public/hooks/use_request/k8s.ts diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index f2d170a35b0a8..dcc7092356a96 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -14,6 +14,7 @@ export const EPM_API_ROOT = `${API_ROOT}/epm`; export const DATA_STREAM_API_ROOT = `${API_ROOT}/data_streams`; export const PACKAGE_POLICY_API_ROOT = `${API_ROOT}/package_policies`; export const AGENT_POLICY_API_ROOT = `${API_ROOT}/agent_policies`; +export const K8S_API_ROOT = `${API_ROOT}/kubernetes`; export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; @@ -67,6 +68,12 @@ export const AGENT_POLICY_API_ROUTES = { FULL_INFO_DOWNLOAD_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/download`, }; +// Kubernetes Manifest API routes +export const K8S_API_ROUTES = { + K8S_DOWNLOAD_PATTERN: `${K8S_API_ROOT}/download`, + K8S_INFO_PATTERN: `${K8S_API_ROOT}`, +}; + // Output API routes export const OUTPUT_API_ROUTES = { LIST_PATTERN: `${API_ROOT}/outputs`, diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 06a853c6c186d..d4e8375bbaa5d 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -18,6 +18,7 @@ import { OUTPUT_API_ROUTES, SETTINGS_API_ROUTES, APP_API_ROUTES, + K8S_API_ROUTES, } from '../constants'; export const epmRouteService = { @@ -141,6 +142,14 @@ export const agentPolicyRouteService = { agentPolicyId ); }, + + getK8sInfoPath: () => { + return K8S_API_ROUTES.K8S_INFO_PATTERN; + }, + + getK8sFullDownloadPath: () => { + return K8S_API_ROUTES.K8S_DOWNLOAD_PATTERN; + }, }; export const dataStreamRouteService = { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts index cbf3c9806d388..d53da769c754b 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts @@ -77,3 +77,7 @@ export interface GetFullAgentPolicyResponse { export interface GetFullAgentConfigMapResponse { item: string; } + +export interface GetFullAgentManifestResponse { + item: string; +} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx new file mode 100644 index 0000000000000..d7b48b5a961c2 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx @@ -0,0 +1,66 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; + +import type { PackagePolicy, AgentPolicy } from '../../types'; +import { sendGetOneAgentPolicy, useStartServices } from '../../hooks'; +import { FLEET_KUBERNETES_PACKAGE } from '../../../common'; + +export function useAgentPolicyWithPackagePolicies(policyId?: string) { + const [agentPolicyWithPackagePolicies, setAgentPolicy] = useState(null); + const core = useStartServices(); + const { notifications } = core; + + useEffect(() => { + async function loadPolicy(policyIdToLoad?: string) { + if (!policyIdToLoad) { + return; + } + try { + const agentPolicyRequest = await sendGetOneAgentPolicy(policyIdToLoad); + + setAgentPolicy(agentPolicyRequest.data ? agentPolicyRequest.data.item : null); + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.agentEnrollment.loadPolicyErrorMessage', { + defaultMessage: 'An error happened while loading the policy', + }), + }); + } + } + + loadPolicy(policyId); + }, [policyId, notifications.toasts]); + + return { agentPolicyWithPackagePolicies }; +} + +export function useIsK8sPolicy(agentPolicy?: AgentPolicy) { + const [isK8s, setIsK8s] = useState<'IS_LOADING' | 'IS_KUBERNETES' | 'IS_NOT_KUBERNETES'>( + 'IS_LOADING' + ); + useEffect(() => { + async function checkifK8s() { + if (!agentPolicy) { + setIsK8s('IS_LOADING'); + return; + } + + setIsK8s( + (agentPolicy.package_policies as PackagePolicy[]).some(isK8sPackage) + ? 'IS_KUBERNETES' + : 'IS_NOT_KUBERNETES' + ); + } + checkifK8s(); + }, [agentPolicy]); + + return { isK8s }; +} + +const isK8sPackage = (pkg: PackagePolicy) => pkg.package?.name === FLEET_KUBERNETES_PACKAGE; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index 4be8eb8a03cd4..ed4cc15aec196 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -22,12 +22,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - useGetSettings, - sendGetOneAgentPolicy, - useFleetStatus, - useAgentEnrollmentFlyoutData, -} from '../../hooks'; +import { useGetSettings, useFleetStatus, useAgentEnrollmentFlyoutData } from '../../hooks'; import { FLEET_SERVER_PACKAGE } from '../../constants'; import type { PackagePolicy } from '../../types'; @@ -37,6 +32,7 @@ import { ManagedInstructions } from './managed_instructions'; import { StandaloneInstructions } from './standalone_instructions'; import { MissingFleetServerHostCallout } from './missing_fleet_server_host_callout'; import type { BaseProps } from './types'; +import { useIsK8sPolicy, useAgentPolicyWithPackagePolicies } from './hooks'; type FlyoutMode = 'managed' | 'standalone'; @@ -72,26 +68,24 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ isLoadingAgentPolicies, refreshAgentPolicies, } = useAgentEnrollmentFlyoutData(); - + const { agentPolicyWithPackagePolicies } = useAgentPolicyWithPackagePolicies(policyId); useEffect(() => { - async function checkPolicyIsFleetServer() { - if (policyId && setIsFleetServerPolicySelected) { - const agentPolicyRequest = await sendGetOneAgentPolicy(policyId); - if ( - agentPolicyRequest.data?.item && - (agentPolicyRequest.data.item.package_policies as PackagePolicy[]).some( - (packagePolicy) => packagePolicy.package?.name === FLEET_SERVER_PACKAGE - ) - ) { - setIsFleetServerPolicySelected(true); - } else { - setIsFleetServerPolicySelected(false); - } + if (agentPolicyWithPackagePolicies && setIsFleetServerPolicySelected) { + if ( + (agentPolicyWithPackagePolicies.package_policies as PackagePolicy[]).some( + (packagePolicy) => packagePolicy.package?.name === FLEET_SERVER_PACKAGE + ) + ) { + setIsFleetServerPolicySelected(true); + } else { + setIsFleetServerPolicySelected(false); } } + }, [agentPolicyWithPackagePolicies]); - checkPolicyIsFleetServer(); - }, [policyId]); + const { isK8s } = useIsK8sPolicy( + agentPolicyWithPackagePolicies ? agentPolicyWithPackagePolicies : undefined + ); const isLoadingInitialRequest = settings.isLoading && settings.isInitialRequest; @@ -154,10 +148,12 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index ba0da6c7ec83a..1e2141ea3827f 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -63,15 +63,15 @@ export const ManagedInstructions = React.memo( agentPolicies, viewDataStep, setSelectedPolicyId, + policyId, isFleetServerPolicySelected, + isK8s, settings, refreshAgentPolicies, isLoadingAgentPolicies, }) => { const fleetStatus = useFleetStatus(); - const [selectedApiKeyId, setSelectedAPIKeyId] = useState(); - const apiKey = useGetOneEnrollmentAPIKey(selectedApiKeyId); const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id); @@ -111,6 +111,8 @@ export const ManagedInstructions = React.memo( ]; }, [fleetServerInstructions]); + const enrolToken = apiKey.data ? apiKey.data.item.api_key : ''; + const steps = useMemo(() => { const fleetServerHosts = settings?.fleet_server_hosts || []; const baseSteps: EuiContainedStepProps[] = [ @@ -123,7 +125,7 @@ export const ManagedInstructions = React.memo( refreshAgentPolicies, }) : AgentEnrollmentKeySelectionStep({ agentPolicy, selectedApiKeyId, setSelectedAPIKeyId }), - DownloadStep(isFleetServerPolicySelected || false), + DownloadStep(isFleetServerPolicySelected || false, isK8s || '', enrolToken || ''), ]; if (isFleetServerPolicySelected) { baseSteps.push(...fleetServerSteps); @@ -133,7 +135,12 @@ export const ManagedInstructions = React.memo( defaultMessage: 'Enroll and start the Elastic Agent', }), children: selectedApiKeyId && apiKey.data && ( - + ), }); } @@ -155,6 +162,9 @@ export const ManagedInstructions = React.memo( isFleetServerPolicySelected, settings?.fleet_server_hosts, viewDataStep, + enrolToken, + isK8s, + policyId, ]); if (fleetStatus.isReady && settings?.fleet_server_hosts.length === 0) { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx index 4fabb36b99a45..4df0431252135 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -27,19 +27,15 @@ import { useStartServices, useLink, sendGetOneAgentPolicyFull, - sendGetOneAgentPolicy, useKibanaVersion, } from '../../hooks'; import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../services'; -import type { PackagePolicy } from '../../../common'; - -import { FLEET_KUBERNETES_PACKAGE } from '../../../common'; - import { PlatformSelector } from '../enrollment_instructions/manual/platform_selector'; import { DownloadStep, AgentPolicySelectionStep } from './steps'; import type { InstructionProps } from './types'; +import { useIsK8sPolicy, useAgentPolicyWithPackagePolicies } from './hooks'; export const StandaloneInstructions = React.memo( ({ agentPolicy, agentPolicies, refreshAgentPolicies }) => { @@ -49,12 +45,14 @@ export const StandaloneInstructions = React.memo( const [selectedPolicyId, setSelectedPolicyId] = useState(agentPolicy?.id); const [fullAgentPolicy, setFullAgentPolicy] = useState(); - const [isK8s, setIsK8s] = useState<'IS_LOADING' | 'IS_KUBERNETES' | 'IS_NOT_KUBERNETES'>( - 'IS_LOADING' - ); const [yaml, setYaml] = useState(''); const kibanaVersion = useKibanaVersion(); + const { agentPolicyWithPackagePolicies } = useAgentPolicyWithPackagePolicies(selectedPolicyId); + const { isK8s } = useIsK8sPolicy( + agentPolicyWithPackagePolicies ? agentPolicyWithPackagePolicies : undefined + ); + const KUBERNETES_RUN_INSTRUCTIONS = 'kubectl apply -f elastic-agent-standalone-kubernetes.yaml'; const STANDALONE_RUN_INSTRUCTIONS_LINUX = `curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-linux-x86_64.tar.gz @@ -84,28 +82,6 @@ sudo rpm -vi elastic-agent-${kibanaVersion}-x86_64.rpm \nsudo systemctl enable e const { docLinks } = useStartServices(); - useEffect(() => { - async function checkifK8s() { - if (!selectedPolicyId) { - return; - } - const agentPolicyRequest = await sendGetOneAgentPolicy(selectedPolicyId); - const agentPol = agentPolicyRequest.data ? agentPolicyRequest.data.item : null; - - if (!agentPol) { - setIsK8s('IS_NOT_KUBERNETES'); - return; - } - const k8s = (pkg: PackagePolicy) => pkg.package?.name === FLEET_KUBERNETES_PACKAGE; - setIsK8s( - (agentPol.package_policies as PackagePolicy[]).some(k8s) - ? 'IS_KUBERNETES' - : 'IS_NOT_KUBERNETES' - ); - } - checkifK8s(); - }, [selectedPolicyId, notifications.toasts]); - useEffect(() => { async function fetchFullPolicy() { try { @@ -178,7 +154,9 @@ sudo rpm -vi elastic-agent-${kibanaVersion}-x86_64.rpm \nsudo systemctl enable e downloadLink = isK8s === 'IS_KUBERNETES' ? core.http.basePath.prepend( - `${agentPolicyRouteService.getInfoFullDownloadPath(selectedPolicyId)}?kubernetes=true` + `${agentPolicyRouteService.getInfoFullDownloadPath( + selectedPolicyId + )}?kubernetes=true&standalone=true` ) : core.http.basePath.prepend( `${agentPolicyRouteService.getInfoFullDownloadPath(selectedPolicyId)}?standalone=true` diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index 31a56056c0bcc..54c449f74cc60 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -5,8 +5,17 @@ * 2.0. */ -import React, { useCallback, useMemo, useState } from 'react'; -import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + EuiText, + EuiButton, + EuiSpacer, + EuiCode, + EuiFlexGroup, + EuiFlexItem, + EuiCopy, + EuiCodeBlock, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import semverMajor from 'semver/functions/major'; @@ -14,36 +23,159 @@ import semverMinor from 'semver/functions/minor'; import semverPatch from 'semver/functions/patch'; import type { AgentPolicy } from '../../types'; -import { useKibanaVersion } from '../../hooks'; +import { useGetSettings, useKibanaVersion, useStartServices } from '../../hooks'; + +import { agentPolicyRouteService } from '../../../common'; + +import { sendGetK8sManifest } from '../../hooks/use_request/k8s'; import { AdvancedAgentAuthenticationSettings } from './advanced_agent_authentication_settings'; import { SelectCreateAgentPolicy } from './agent_policy_select_create'; -export const DownloadStep = (hasFleetServer: boolean) => { +export const DownloadStep = ( + hasFleetServer: boolean, + isK8s?: string, + enrollmentAPIKey?: string +) => { const kibanaVersion = useKibanaVersion(); + const core = useStartServices(); + const settings = useGetSettings(); const kibanaVersionURLString = useMemo( () => `${semverMajor(kibanaVersion)}-${semverMinor(kibanaVersion)}-${semverPatch(kibanaVersion)}`, [kibanaVersion] ); + const { notifications } = core; + + const [yaml, setYaml] = useState(); + const [fleetServer, setFleetServer] = useState(); + useEffect(() => { + async function fetchK8sManifest() { + try { + if (isK8s !== 'IS_KUBERNETES') { + return; + } + const fleetServerHosts = settings.data?.item.fleet_server_hosts; + let host = ''; + if (fleetServerHosts !== undefined && fleetServerHosts.length !== 0) { + setFleetServer(fleetServerHosts[0]); + host = fleetServerHosts[0]; + } + const query = { fleetServer: host, enrolToken: enrollmentAPIKey }; + const res = await sendGetK8sManifest(query); + if (res.error) { + throw res.error; + } + + if (!res.data) { + throw new Error('No data while fetching agent manifest'); + } + + setYaml(res.data.item); + } catch (error) { + notifications.toasts.addError(error, { + title: i18n.translate('xpack.fleet.agentEnrollment.loadk8sManifestErrorTitle', { + defaultMessage: 'Error while fetching agent manifest', + }), + }); + } + } + fetchK8sManifest(); + }, [isK8s, notifications.toasts, enrollmentAPIKey, settings.data?.item.fleet_server_hosts]); + + const altTitle = + isK8s === 'IS_KUBERNETES' + ? i18n.translate('xpack.fleet.agentEnrollment.stepDownloadAgentForK8sTitle', { + defaultMessage: 'Download the Elastic Agent Manifest', + }) + : i18n.translate('xpack.fleet.agentEnrollment.stepDownloadAgentTitle', { + defaultMessage: 'Download the Elastic Agent to your host', + }); const title = hasFleetServer ? i18n.translate('xpack.fleet.agentEnrollment.stepDownloadAgentForFleetServerTitle', { defaultMessage: 'Download the Fleet Server to a centralized host', }) - : i18n.translate('xpack.fleet.agentEnrollment.stepDownloadAgentTitle', { - defaultMessage: 'Download the Elastic Agent to your host', - }); + : altTitle; + + const altDownloadDescription = + isK8s === 'IS_KUBERNETES' ? ( + FLEET_URL, + FleetTokenVariable: FLEET_ENROLLMENT_TOKEN, + }} + /> + ) : ( + + ); + const downloadDescription = hasFleetServer ? ( ) : ( - + altDownloadDescription ); + + const linuxUsers = + isK8s !== 'IS_KUBERNETES' ? ( + + ) : ( + '' + ); + const k8sCopyYaml = + isK8s === 'IS_KUBERNETES' ? ( + + {(copy) => ( + + + + )} + + ) : ( + '' + ); + const k8sYaml = + isK8s === 'IS_KUBERNETES' ? ( + + {yaml} + + ) : ( + '' + ); + + const downloadLink = + isK8s === 'IS_KUBERNETES' + ? core.http.basePath.prepend( + `${agentPolicyRouteService.getK8sFullDownloadPath()}?fleetServer=${fleetServer}&enrolToken=${enrollmentAPIKey}` + ) + : `https://www.elastic.co/downloads/past-releases/elastic-agent-${kibanaVersionURLString}`; + + const downloadMsg = + isK8s === 'IS_KUBERNETES' ? ( + + ) : ( + + ); + return { title, children: ( @@ -51,23 +183,21 @@ export const DownloadStep = (hasFleetServer: boolean) => { {downloadDescription} - + <>{linuxUsers} - - - - + + + + + <>{downloadMsg} + + + + <>{k8sCopyYaml} + + + + <>{k8sYaml} ), }; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts index 0c447ad0870ff..edb34e876e7c8 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -25,7 +25,11 @@ export interface BaseProps { setSelectedPolicyId?: (policyId?: string) => void; + policyId?: string; + isFleetServerPolicySelected?: boolean; + + isK8s?: string; } export interface InstructionProps extends BaseProps { diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index 4279e46cbcd66..62b7cb6fac5a1 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -15,6 +15,8 @@ import { PlatformSelector } from './platform_selector'; interface Props { fleetServerHosts: string[]; apiKey: EnrollmentAPIKey; + policyId: string | undefined; + isK8s: string | undefined; } function getfleetServerHostsEnrollArgs(apiKey: EnrollmentAPIKey, fleetServerHosts: string[]) { @@ -24,6 +26,8 @@ function getfleetServerHostsEnrollArgs(apiKey: EnrollmentAPIKey, fleetServerHost export const ManualInstructions: React.FunctionComponent = ({ apiKey, fleetServerHosts, + policyId, + isK8s, }) => { const { docLinks } = useStartServices(); const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts); @@ -59,7 +63,7 @@ sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \n linuxDebCommand={linuxDebCommand} linuxRpmCommand={linuxRpmCommand} troubleshootLink={docLinks.links.fleet.troubleshooting} - isK8s={false} + isK8s={isK8s === 'IS_KUBERNETES'} /> ); }; diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/platform_selector.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/platform_selector.tsx index 76fbd7f712b6f..7fc1c827596e9 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/platform_selector.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/platform_selector.tsx @@ -36,6 +36,8 @@ const CommandCode = styled.pre({ overflow: 'auto', }); +const K8S_COMMAND = `kubectl apply -f elastic-agent-managed-kubernetes.yaml`; + export const PlatformSelector: React.FunctionComponent = ({ linuxCommand, macCommand, @@ -74,9 +76,10 @@ export const PlatformSelector: React.FunctionComponent = ({ )} + {isK8s ? ( - {linuxCommand} + {K8S_COMMAND} ) : ( <> diff --git a/x-pack/plugins/fleet/public/hooks/use_request/k8s.ts b/x-pack/plugins/fleet/public/hooks/use_request/k8s.ts new file mode 100644 index 0000000000000..72e9c668f918b --- /dev/null +++ b/x-pack/plugins/fleet/public/hooks/use_request/k8s.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 { agentPolicyRouteService } from '../../services'; + +import type { GetFullAgentManifestResponse } from '../../../common'; + +import { sendRequest } from './use_request'; + +export const sendGetK8sManifest = (query: { fleetServer?: string; enrolToken?: string } = {}) => { + return sendRequest({ + path: agentPolicyRouteService.getK8sInfoPath(), + method: 'get', + query, + }); +}; diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index dbf1db0f68f28..f0ed469ad8443 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -22,6 +22,7 @@ import type { CopyAgentPolicyRequestSchema, DeleteAgentPolicyRequestSchema, GetFullAgentPolicyRequestSchema, + GetK8sManifestRequestSchema, FleetRequestHandler, } from '../../types'; @@ -35,6 +36,7 @@ import type { DeleteAgentPolicyResponse, GetFullAgentPolicyResponse, GetFullAgentConfigMapResponse, + GetFullAgentManifestResponse, } from '../../../common'; import { defaultIngestErrorHandler } from '../../errors'; import { createAgentPolicyWithPackages } from '../../services/agent_policy_create'; @@ -328,3 +330,58 @@ export const downloadFullAgentPolicy: FleetRequestHandler< } } }; + +export const getK8sManifest: FleetRequestHandler< + undefined, + TypeOf +> = async (context, request, response) => { + try { + const fleetServer = request.query.fleetServer ?? ''; + const token = request.query.enrolToken ?? ''; + const fullAgentManifest = await agentPolicyService.getFullAgentManifest(fleetServer, token); + if (fullAgentManifest) { + const body: GetFullAgentManifestResponse = { + item: fullAgentManifest, + }; + return response.ok({ + body, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent manifest not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; + +export const downloadK8sManifest: FleetRequestHandler< + undefined, + TypeOf +> = async (context, request, response) => { + try { + const fleetServer = request.query.fleetServer ?? ''; + const token = request.query.enrolToken ?? ''; + const fullAgentManifest = await agentPolicyService.getFullAgentManifest(fleetServer, token); + if (fullAgentManifest) { + const body = fullAgentManifest; + const headers: ResponseHeaders = { + 'content-type': 'text/x-yaml', + 'content-disposition': `attachment; filename="elastic-agent-managed-kubernetes.yaml"`, + }; + return response.ok({ + body, + headers, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent manifest not found' }, + }); + } + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts index 3819b009f2763..66540e019e5a3 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts @@ -14,9 +14,12 @@ import { CopyAgentPolicyRequestSchema, DeleteAgentPolicyRequestSchema, GetFullAgentPolicyRequestSchema, + GetK8sManifestRequestSchema, } from '../../types'; import type { FleetAuthzRouter } from '../security'; +import { K8S_API_ROUTES } from '../../../common'; + import { getAgentPoliciesHandler, getOneAgentPolicyHandler, @@ -26,6 +29,8 @@ import { deleteAgentPoliciesHandler, getFullAgentPolicy, downloadFullAgentPolicy, + downloadK8sManifest, + getK8sManifest, } from './handlers'; export const registerRoutes = (router: FleetAuthzRouter) => { @@ -124,4 +129,28 @@ export const registerRoutes = (router: FleetAuthzRouter) => { }, downloadFullAgentPolicy ); + + // Get agent manifest + router.get( + { + path: K8S_API_ROUTES.K8S_INFO_PATTERN, + validate: GetK8sManifestRequestSchema, + fleetAuthz: { + fleet: { all: true }, + }, + }, + getK8sManifest + ); + + // Download agent manifest + router.get( + { + path: K8S_API_ROUTES.K8S_DOWNLOAD_PATTERN, + validate: GetK8sManifestRequestSchema, + fleetAuthz: { + fleet: { all: true }, + }, + }, + downloadK8sManifest + ); }; diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index c34104e491da8..37af4e646658b 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -53,7 +53,10 @@ import type { FullAgentConfigMap } from '../../common/types/models/agent_cm'; import { fullAgentConfigMapToYaml } from '../../common/services/agent_cm_to_yaml'; -import { elasticAgentManifest } from './elastic_agent_manifest'; +import { + elasticAgentStandaloneManifest, + elasticAgentManagedManifest, +} from './elastic_agent_manifest'; import { getPackageInfo } from './epm/packages'; import { getAgentsByKuery } from './agents'; @@ -785,7 +788,7 @@ class AgentPolicyService { }; const configMapYaml = fullAgentConfigMapToYaml(fullAgentConfigMap, safeDump); - const updateManifestVersion = elasticAgentManifest.replace( + const updateManifestVersion = elasticAgentStandaloneManifest.replace( 'VERSION', appContextService.getKibanaVersion() ); @@ -796,6 +799,25 @@ class AgentPolicyService { } } + public async getFullAgentManifest( + fleetServer: string, + enrolToken: string + ): Promise { + const updateManifestVersion = elasticAgentManagedManifest.replace( + 'VERSION', + appContextService.getKibanaVersion() + ); + let updateManifest = updateManifestVersion; + if (fleetServer !== '') { + updateManifest = updateManifest.replace('https://fleet-server:8220', fleetServer); + } + if (enrolToken !== '') { + updateManifest = updateManifest.replace('token-id', enrolToken); + } + + return updateManifest; + } + public async getFullAgentPolicy( soClient: SavedObjectsClientContract, id: string, diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts index 392ee170d02ad..4ce39483f09a3 100644 --- a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const elasticAgentManifest = ` +export const elasticAgentStandaloneManifest = ` --- apiVersion: apps/v1 kind: DaemonSet @@ -168,6 +168,7 @@ rules: - apiGroups: ["batch"] resources: - jobs + - cronjobs verbs: ["get", "list", "watch"] - apiGroups: - "" @@ -220,3 +221,220 @@ metadata: k8s-app: elastic-agent --- `; + +export const elasticAgentManagedManifest = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: elastic-agent + namespace: kube-system + labels: + app: elastic-agent +spec: + selector: + matchLabels: + app: elastic-agent + template: + metadata: + labels: + app: elastic-agent + spec: + tolerations: + - key: node-role.kubernetes.io/master + effect: NoSchedule + serviceAccountName: elastic-agent + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + containers: + - name: elastic-agent + image: docker.elastic.co/beats/elastic-agent:VERSION + env: + - name: FLEET_ENROLL + value: "1" + # Set to true in case of insecure or unverified HTTP + - name: FLEET_INSECURE + value: "true" + # The ip:port pair of fleet server + - name: FLEET_URL + value: "https://fleet-server:8220" + # If left empty KIBANA_HOST, KIBANA_FLEET_USERNAME, KIBANA_FLEET_PASSWORD are needed + - name: FLEET_ENROLLMENT_TOKEN + value: "token-id" + - name: KIBANA_HOST + value: "http://kibana:5601" + - name: KIBANA_FLEET_USERNAME + value: "elastic" + - name: KIBANA_FLEET_PASSWORD + value: "changeme" + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + securityContext: + runAsUser: 0 + resources: + limits: + memory: 500Mi + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - name: proc + mountPath: /hostfs/proc + readOnly: true + - name: cgroup + mountPath: /hostfs/sys/fs/cgroup + readOnly: true + - name: varlibdockercontainers + mountPath: /var/lib/docker/containers + readOnly: true + - name: varlog + mountPath: /var/log + readOnly: true + volumes: + - name: proc + hostPath: + path: /proc + - name: cgroup + hostPath: + path: /sys/fs/cgroup + - name: varlibdockercontainers + hostPath: + path: /var/lib/docker/containers + - name: varlog + hostPath: + path: /var/log +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: elastic-agent +subjects: + - kind: ServiceAccount + name: elastic-agent + namespace: kube-system +roleRef: + kind: ClusterRole + name: elastic-agent + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + namespace: kube-system + name: elastic-agent +subjects: + - kind: ServiceAccount + name: elastic-agent + namespace: kube-system +roleRef: + kind: Role + name: elastic-agent + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: elastic-agent-kubeadm-config + namespace: kube-system +subjects: + - kind: ServiceAccount + name: elastic-agent + namespace: kube-system +roleRef: + kind: Role + name: elastic-agent-kubeadm-config + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: elastic-agent + labels: + k8s-app: elastic-agent +rules: + - apiGroups: [""] + resources: + - nodes + - namespaces + - events + - pods + - services + - configmaps + verbs: ["get", "list", "watch"] + # Enable this rule only if planing to use kubernetes_secrets provider + #- apiGroups: [""] + # resources: + # - secrets + # verbs: ["get"] + - apiGroups: ["extensions"] + resources: + - replicasets + verbs: ["get", "list", "watch"] + - apiGroups: ["apps"] + resources: + - statefulsets + - deployments + - replicasets + verbs: ["get", "list", "watch"] + - apiGroups: + - "" + resources: + - nodes/stats + verbs: + - get + - apiGroups: [ "batch" ] + resources: + - jobs + - cronjobs + verbs: [ "get", "list", "watch" ] + # required for apiserver + - nonResourceURLs: + - "/metrics" + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: elastic-agent + # should be the namespace where elastic-agent is running + namespace: kube-system + labels: + k8s-app: elastic-agent +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: ["get", "create", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: elastic-agent-kubeadm-config + namespace: kube-system + labels: + k8s-app: elastic-agent +rules: + - apiGroups: [""] + resources: + - configmaps + resourceNames: + - kubeadm-config + verbs: ["get"] +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: elastic-agent + namespace: kube-system + labels: + k8s-app: elastic-agent +--- + +`; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts index 042129e1e0914..257cc90d453c8 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts @@ -61,3 +61,11 @@ export const GetFullAgentPolicyRequestSchema = { kubernetes: schema.maybe(schema.boolean()), }), }; + +export const GetK8sManifestRequestSchema = { + query: schema.object({ + download: schema.maybe(schema.boolean()), + fleetServer: schema.maybe(schema.string()), + enrolToken: schema.maybe(schema.string()), + }), +}; From 3eaef136d5fbb9a1194a095dcc0f15dd6198f2b6 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Tue, 29 Mar 2022 15:58:05 -0400 Subject: [PATCH 025/108] [RAM] SIimplify error banner on rules (#128705) * wip - clean up error banner * add expand error * fix lint+ * fix typo * my lint is not nice * add functional test * fix check * review + i18n fix * fix functional test --- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../components/rule_status_dropdown.tsx | 7 +- .../rules_list/components/rules_list.tsx | 352 ++++++++++++------ .../apps/triggers_actions_ui/alerts_list.ts | 36 +- 5 files changed, 271 insertions(+), 130 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cc840ab4a3534..c22beedbb4e27 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27854,7 +27854,6 @@ "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastExecutionDateTitle": "前回の実行の開始時間。", "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle": "名前", "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle": "間隔", - "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle": "ステータス", "xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "アクティブ", "xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "エラー", "xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "ステータス", @@ -27880,7 +27879,6 @@ "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.muteTitle": "ミュート", "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle": "アクション", "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.unmuteTitle": "ミュート解除", - "xpack.triggersActionsUI.sections.rulesList.dismissBunnerButtonLabel": "閉じる", "xpack.triggersActionsUI.sections.rulesList.fixLicenseLink": "修正", "xpack.triggersActionsUI.sections.rulesList.multipleTitle": "ルール", "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateDescription": "システム管理者にお問い合わせください。", @@ -27900,7 +27898,6 @@ "xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage": "ルールを読み込めません", "xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage": "ルールステータス情報を読み込めません", "xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTypesMessage": "ルールタイプを読み込めません", - "xpack.triggersActionsUI.sections.rulesList.viewBunnerButtonLabel": "表示", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "Bcc", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "Cc", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "認証", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8697109f3b927..3b85ad51fdb3d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27883,7 +27883,6 @@ "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastExecutionDateTitle": "上次执行的开始时间。", "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle": "名称", "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle": "时间间隔", - "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle": "状态", "xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "活动", "xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "错误", "xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "状态", @@ -27910,7 +27909,6 @@ "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle": "操作", "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.unmuteTitle": "取消静音", - "xpack.triggersActionsUI.sections.rulesList.dismissBunnerButtonLabel": "关闭", "xpack.triggersActionsUI.sections.rulesList.fixLicenseLink": "修复", "xpack.triggersActionsUI.sections.rulesList.multipleTitle": "规则", "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateDescription": "请联系您的系统管理员。", @@ -27931,7 +27929,6 @@ "xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage": "无法加载规则", "xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage": "无法加载规则状态信息", "xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTypesMessage": "无法加载规则类型", - "xpack.triggersActionsUI.sections.rulesList.viewBunnerButtonLabel": "查看", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "密送", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "抄送", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "身份验证", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx index 97652c5ab45aa..ff76abef65b60 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx @@ -127,7 +127,12 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ ); return ( - + { const [actionTypesFilter, setActionTypesFilter] = useState([]); const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); - const [dismissRuleErrors, setDismissRuleErrors] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1); + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( + {} + ); + const [showErrors, setShowErrors] = useState(false); useEffect(() => { (async () => { @@ -455,7 +464,73 @@ export const RulesList: React.FunctionComponent = () => { }; }; - const getRulesTableColumns = () => { + const buildErrorListItems = (_executionStatus: AlertExecutionStatus) => { + const hasErrorMessage = _executionStatus.status === 'error'; + const errorMessage = _executionStatus?.error?.message; + const isLicenseError = + _executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License; + const statusMessage = isLicenseError ? ALERT_STATUS_LICENSE_ERROR : null; + + return [ + { + title: ( + + ), + description: ( + <> + {errorMessage} + {hasErrorMessage && statusMessage && } + {statusMessage} + + ), + }, + ]; + }; + + const toggleErrorMessage = (_executionStatus: AlertExecutionStatus, ruleItem: RuleTableItem) => { + setItemIdToExpandedRowMap((itemToExpand) => { + const _itemToExpand = { ...itemToExpand }; + if (_itemToExpand[ruleItem.id]) { + delete _itemToExpand[ruleItem.id]; + } else { + _itemToExpand[ruleItem.id] = ( + + ); + } + return _itemToExpand; + }); + }; + + const toggleRuleErrors = useCallback(() => { + setShowErrors((prevValue) => { + if (!prevValue) { + const rulesToExpand = rulesState.data.reduce((acc, ruleItem) => { + if (ruleItem.executionStatus.status === 'error') { + return { + ...acc, + [ruleItem.id]: ( + + ), + }; + } + return acc; + }, {}); + setItemIdToExpandedRowMap(rulesToExpand); + } else { + setItemIdToExpandedRowMap({}); + } + return !prevValue; + }); + }, [showErrors, rulesState]); + + const getRulesTableColumns = (): Array< + | EuiTableFieldDataColumnType + | EuiTableComputedColumnType + | EuiTableActionsColumnType + > => { return [ { field: 'name', @@ -755,13 +830,10 @@ export const RulesList: React.FunctionComponent = () => { }, { field: 'enabled', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle', - { defaultMessage: 'Status' } - ), + name: '', sortable: true, truncateText: false, - width: '200px', + width: '10%', 'data-test-subj': 'rulesTableCell-status', render: (_enabled: boolean | undefined, item: RuleTableItem) => { return renderRuleStatusDropdown(item.enabled, item); @@ -769,12 +841,12 @@ export const RulesList: React.FunctionComponent = () => { }, { name: '', - width: '10%', + width: '90px', render(item: RuleTableItem) { return ( - + - + {item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId) ? ( { ); }, }, + { + align: RIGHT_ALIGNMENT, + width: '40px', + isExpander: true, + name: ( + + Expand rows + + ), + render: (item: RuleTableItem) => { + const _executionStatus = item.executionStatus; + const hasErrorMessage = _executionStatus.status === 'error'; + const isLicenseError = + _executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License; + + return isLicenseError || hasErrorMessage ? ( + toggleErrorMessage(_executionStatus, item)} + aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} + iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + /> + ) : null; + }, + }, ]; }; @@ -906,6 +1002,34 @@ export const RulesList: React.FunctionComponent = () => { const table = ( <> + {rulesStatusesTotal.error > 0 ? ( + <> + +

+ +   + +   + setRuleStatusesFilter(['error'])}> + + +

+
+ + + ) : null} {selectedIds.length > 0 && authorizedToModifySelectedRules && ( @@ -974,122 +1098,110 @@ export const RulesList: React.FunctionComponent = () => {
- - {!dismissRuleErrors && rulesStatusesTotal.error > 0 ? ( - - - + + + + + - } - iconType="rule" - data-test-subj="rulesErrorBanner" - > - setRuleStatusesFilter(['error'])} - > + + + + - - setDismissRuleErrors(true)}> + + + + - - - - - ) : null} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + +
+ {rulesStatusesTotal.error > 0 && ( + + + {!showErrors && ( + + )} + {showErrors && ( + + )} + + + )}
@@ -1148,6 +1260,8 @@ export const RulesList: React.FunctionComponent = () => { setSort(changedSort); } }} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + isExpandable={true} /> {manageLicenseModalOpts && ( { ); expect(alertsErrorBannerExistErrors).to.have.length(1); expect( - await ( - await alertsErrorBannerExistErrors[0].findByCssSelector('.euiCallOutHeader') - ).getVisibleText() - ).to.equal('Error found in 1 rule.'); + await (await alertsErrorBannerExistErrors[0].findByTagName('p')).getVisibleText() + ).to.equal(' Error found in 1 rule. Show rule with error'); }); await refreshAlertsList(); @@ -515,6 +513,36 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await testSubjects.getVisibleText('totalUnknownRulesCount')).to.be('Unknown: 0'); }); + it('Expand error in rules table when there is rule with an error associated', async () => { + const createdAlert = await createAlert({ supertest, objectRemover }); + await retry.try(async () => { + await refreshAlertsList(); + const refreshResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); + expect(refreshResults.length).to.equal(1); + expect(refreshResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); + expect(refreshResults[0].interval).to.equal('1 min'); + expect(refreshResults[0].status).to.equal('Ok'); + expect(refreshResults[0].duration).to.match(/\d{2,}:\d{2}/); + }); + + let expandRulesErrorLink = await find.allByCssSelector('[data-test-subj="expandRulesError"]'); + expect(expandRulesErrorLink).to.have.length(0); + + await createFailingAlert({ supertest, objectRemover }); + await retry.try(async () => { + await refreshAlertsList(); + expandRulesErrorLink = await find.allByCssSelector('[data-test-subj="expandRulesError"]'); + expect(expandRulesErrorLink).to.have.length(1); + }); + await refreshAlertsList(); + await testSubjects.click('expandRulesError'); + const expandedRow = await find.allByCssSelector('.euiTableRow-isExpandedRow'); + expect(expandedRow).to.have.length(1); + expect(await (await expandedRow[0].findByTagName('div')).getVisibleText()).to.equal( + 'Error from last run\nFailed to execute alert type' + ); + }); + it('should filter alerts by the alert type', async () => { await createAlert({ supertest, objectRemover }); const failingAlert = await createFailingAlert({ supertest, objectRemover }); From bde0262191ceb8d4b88ad4e56531ad0bf9af960b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 14:01:55 -0600 Subject: [PATCH 026/108] Update babel (main) (#128572) Co-authored-by: Renovate Bot Co-authored-by: spalger --- package.json | 16 +-- yarn.lock | 276 +++++++++++++++++++++++++++------------------------ 2 files changed, 154 insertions(+), 138 deletions(-) diff --git a/package.json b/package.json index 8b434d291013e..9a4c3b9faef37 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "yarn": "^1.21.1" }, "resolutions": { - "**/@babel/runtime": "^7.17.2", + "**/@babel/runtime": "^7.17.8", "**/@types/node": "16.11.7", "**/chokidar": "^3.4.3", "**/deepmerge": "^4.2.2", @@ -97,7 +97,7 @@ "puppeteer/node-fetch": "^2.6.7" }, "dependencies": { - "@babel/runtime": "^7.17.2", + "@babel/runtime": "^7.17.8", "@dnd-kit/core": "^3.1.1", "@dnd-kit/sortable": "^4.0.0", "@dnd-kit/utilities": "^2.0.0", @@ -435,11 +435,11 @@ "devDependencies": { "@apidevtools/swagger-parser": "^10.0.3", "@babel/cli": "^7.17.6", - "@babel/core": "^7.17.5", + "@babel/core": "^7.17.8", "@babel/eslint-parser": "^7.17.0", - "@babel/eslint-plugin": "^7.16.5", - "@babel/generator": "^7.17.3", - "@babel/parser": "^7.17.3", + "@babel/eslint-plugin": "^7.17.7", + "@babel/generator": "^7.17.7", + "@babel/parser": "^7.17.8", "@babel/plugin-proposal-class-properties": "^7.16.7", "@babel/plugin-proposal-export-namespace-from": "^7.16.7", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", @@ -450,7 +450,7 @@ "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.16.7", - "@babel/register": "^7.17.0", + "@babel/register": "^7.17.7", "@babel/traverse": "^7.17.3", "@babel/types": "^7.17.0", "@bazel/ibazel": "^0.16.2", @@ -521,7 +521,7 @@ "@testing-library/user-event": "^13.1.1", "@types/apidoc": "^0.22.3", "@types/archiver": "^5.1.0", - "@types/babel__core": "^7.1.18", + "@types/babel__core": "^7.1.19", "@types/base64-js": "^1.2.5", "@types/chance": "^1.0.0", "@types/chroma-js": "^1.4.2", diff --git a/yarn.lock b/yarn.lock index d097eefeb7485..0b6f13bc96b94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -79,10 +79,10 @@ dependencies: "@babel/highlight" "^7.16.7" -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.4", "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.0.tgz#86850b8597ea6962089770952075dcaabb8dba34" - integrity sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng== +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" + integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ== "@babel/core@7.12.9": version "7.12.9" @@ -106,18 +106,18 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@>=7.9.0", "@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.17.5", "@babel/core@^7.7.5": - version "7.17.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.5.tgz#6cd2e836058c28f06a4ca8ee7ed955bbf37c8225" - integrity sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA== +"@babel/core@>=7.9.0", "@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.17.8", "@babel/core@^7.7.5": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.8.tgz#3dac27c190ebc3a4381110d46c80e77efe172e1a" + integrity sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.3" - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helpers" "^7.17.2" - "@babel/parser" "^7.17.3" + "@babel/generator" "^7.17.7" + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helpers" "^7.17.8" + "@babel/parser" "^7.17.8" "@babel/template" "^7.16.7" "@babel/traverse" "^7.17.3" "@babel/types" "^7.17.0" @@ -136,17 +136,17 @@ eslint-visitor-keys "^2.1.0" semver "^6.3.0" -"@babel/eslint-plugin@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.16.5.tgz#d1770685160922059f5d8f101055e799b7cff391" - integrity sha512-R1p6RMyU1Xl1U/NNr+D4+HjkQzN5dQOX0MpjW9WLWhHDjhzN9gso96MxxOFvPh0fKF/mMH8TGW2kuqQ2eK2s9A== +"@babel/eslint-plugin@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.17.7.tgz#4ee1d5b29b79130f3bb5a933358376bcbee172b8" + integrity sha512-JATUoJJXSgwI0T8juxWYtK1JSgoLpIGUsCHIv+NMXcUDA2vIe6nvAHR9vnuJgs/P1hOFw7vPwibixzfqBBLIVw== dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.3.tgz#a2c30b0c4f89858cb87050c3ffdfd36bdf443200" - integrity sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg== +"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.17.3", "@babel/generator@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.7.tgz#8da2599beb4a86194a3b24df6c085931d9ee45ad" + integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w== dependencies: "@babel/types" "^7.17.0" jsesc "^2.5.1" @@ -167,20 +167,20 @@ "@babel/helper-explode-assignable-expression" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b" - integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" + integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== dependencies: - "@babel/compat-data" "^7.16.4" + "@babel/compat-data" "^7.17.7" "@babel/helper-validator-option" "^7.16.7" browserslist "^4.17.5" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.10.tgz#8a6959b9cc818a88815ba3c5474619e9c0f2c21c" - integrity sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg== +"@babel/helper-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.6": + version "7.17.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz#3778c1ed09a7f3e65e6d6e0f6fbfcc53809d92c9" + integrity sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" "@babel/helper-environment-visitor" "^7.16.7" @@ -191,12 +191,12 @@ "@babel/helper-split-export-declaration" "^7.16.7" "@babel/helper-create-regexp-features-plugin@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz#0cb82b9bac358eb73bfbd73985a776bfa6b14d48" - integrity sha512-fk5A6ymfp+O5+p2yCkXAu5Kyj6v0xh0RBeNcAkYUMDvvAAoxvSKXn+Jb37t/yWFiQVDFK1ELpUTD8/aLhCPu+g== + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1" + integrity sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" - regexpu-core "^4.7.1" + regexpu-core "^5.0.1" "@babel/helper-define-polyfill-provider@^0.1.5": version "0.1.5" @@ -264,11 +264,11 @@ "@babel/types" "^7.16.7" "@babel/helper-member-expression-to-functions@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz#42b9ca4b2b200123c3b7e726b0ae5153924905b0" - integrity sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q== + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4" + integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw== dependencies: - "@babel/types" "^7.16.7" + "@babel/types" "^7.17.0" "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.7.0": version "7.16.7" @@ -277,19 +277,19 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz#7665faeb721a01ca5327ddc6bba15a5cb34b6a41" - integrity sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng== +"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" + integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw== dependencies: "@babel/helper-environment-visitor" "^7.16.7" "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-simple-access" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" "@babel/helper-split-export-declaration" "^7.16.7" "@babel/helper-validator-identifier" "^7.16.7" "@babel/template" "^7.16.7" - "@babel/traverse" "^7.16.7" - "@babel/types" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" "@babel/helper-optimise-call-expression@^7.16.7": version "7.16.7" @@ -328,12 +328,12 @@ "@babel/traverse" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/helper-simple-access@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" - integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== +"@babel/helper-simple-access@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" + integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== dependencies: - "@babel/types" "^7.16.7" + "@babel/types" "^7.17.0" "@babel/helper-skip-transparent-expression-wrappers@^7.16.0": version "7.16.0" @@ -369,13 +369,13 @@ "@babel/traverse" "^7.16.8" "@babel/types" "^7.16.8" -"@babel/helpers@^7.12.5", "@babel/helpers@^7.17.2": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.2.tgz#23f0a0746c8e287773ccd27c14be428891f63417" - integrity sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ== +"@babel/helpers@^7.12.5", "@babel/helpers@^7.17.8": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.8.tgz#288450be8c6ac7e4e44df37bcc53d345e07bc106" + integrity sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw== dependencies: "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.0" + "@babel/traverse" "^7.17.3" "@babel/types" "^7.17.0" "@babel/highlight@^7.10.4", "@babel/highlight@^7.16.7": @@ -387,10 +387,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.3.tgz#b07702b982990bf6fdc1da5049a23fece4c5c3d0" - integrity sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.8": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" + integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" @@ -426,11 +426,11 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-proposal-class-static-block@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.7.tgz#712357570b612106ef5426d13dc433ce0f200c2a" - integrity sha512-dgqJJrcZoG/4CkMopzhPJjGxsIe9A8RlkQLnL/Vhhx8AA9ZuaRwGSlscSh42hazc7WSrya/IK7mTeoF0DP9tEw== + version "7.17.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz#164e8fd25f0d80fa48c5a4d1438a6629325ad83c" + integrity sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-create-class-features-plugin" "^7.17.6" "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" @@ -768,9 +768,9 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-destructuring@^7.12.1", "@babel/plugin-transform-destructuring@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.7.tgz#ca9588ae2d63978a4c29d3f33282d8603f618e23" - integrity sha512-VqAwhTHBnu5xBVDCvrvqJbtLUa++qZaWC0Fgr2mqokBlulZARGyIvZDoqbPlPaKImQ9dKAcCzbv+ul//uqu70A== + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz#49dc2675a7afa9a5e4c6bdee636061136c3408d1" + integrity sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -845,22 +845,22 @@ babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-commonjs@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz#cdee19aae887b16b9d331009aa9a219af7c86afe" - integrity sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA== + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz#d86b217c8e45bb5f2dbc11eefc8eab62cf980d19" + integrity sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA== dependencies: - "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-module-transforms" "^7.17.7" "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-simple-access" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-systemjs@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz#887cefaef88e684d29558c2b13ee0563e287c2d7" - integrity sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw== + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz#81fd834024fae14ea78fbe34168b042f38703859" + integrity sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw== dependencies: "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-module-transforms" "^7.17.7" "@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-validator-identifier" "^7.16.7" babel-plugin-dynamic-import-node "^2.3.3" @@ -924,15 +924,15 @@ "@babel/plugin-transform-react-jsx" "^7.16.7" "@babel/plugin-transform-react-jsx@^7.12.1", "@babel/plugin-transform-react-jsx@^7.12.12", "@babel/plugin-transform-react-jsx@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.16.7.tgz#86a6a220552afd0e4e1f0388a68a372be7add0d4" - integrity sha512-8D16ye66fxiE8m890w0BpPpngG9o9OVBBy0gH2E+2AR7qMR2ZpTYJEqLxAsoroenMId0p/wMW+Blc0meDgu0Ag== + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz#eac1565da176ccb1a715dae0b4609858808008c1" + integrity sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" "@babel/helper-module-imports" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-jsx" "^7.16.7" - "@babel/types" "^7.16.7" + "@babel/types" "^7.17.0" "@babel/plugin-transform-react-pure-annotations@^7.16.7": version "7.16.7" @@ -1148,7 +1148,7 @@ "@babel/helper-validator-option" "^7.16.7" "@babel/plugin-transform-typescript" "^7.16.7" -"@babel/register@^7.12.1", "@babel/register@^7.17.0": +"@babel/register@^7.12.1": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.17.0.tgz#8051e0b7cb71385be4909324f072599723a1f084" integrity sha512-UNZsMAZ7uKoGHo1HlEXfteEOYssf64n/PNLHGqOKq/bgYcu/4LrQWAHJwSCb3BRZK8Hi5gkJdRcwrGTO2wtRCg== @@ -1159,6 +1159,17 @@ pirates "^4.0.5" source-map-support "^0.5.16" +"@babel/register@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.17.7.tgz#5eef3e0f4afc07e25e847720e7b987ae33f08d0b" + integrity sha512-fg56SwvXRifootQEDQAu1mKdjh5uthPzdO0N6t358FktfL4XjAVXuH58ULoiW8mesxiOgNIrxiImqEwv0+hRRA== + dependencies: + clone-deep "^4.0.1" + find-cache-dir "^2.0.0" + make-dir "^2.1.0" + pirates "^4.0.5" + source-map-support "^0.5.16" + "@babel/runtime-corejs3@^7.10.2": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz#02c3029743150188edeb66541195f54600278419" @@ -1167,10 +1178,10 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.0", "@babel/runtime@^7.16.0", "@babel/runtime@^7.17.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" - integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.0", "@babel/runtime@^7.16.0", "@babel/runtime@^7.17.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" + integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== dependencies: regenerator-runtime "^0.13.4" @@ -1183,7 +1194,7 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.0", "@babel/traverse@^7.17.3", "@babel/traverse@^7.4.5": +"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.4.5": version "7.17.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== @@ -5286,10 +5297,10 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" -"@types/babel__core@^7.1.18": - version "7.1.18" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8" - integrity sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ== +"@types/babel__core@^7.1.19": + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -24605,18 +24616,23 @@ regedit@^5.0.0: stream-slicer "0.0.6" through2 "^0.6.3" -regenerate-unicode-properties@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" - integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== +regenerate-unicode-properties@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" + integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== dependencies: - regenerate "^1.4.0" + regenerate "^1.4.2" regenerate@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + regenerator-runtime@^0.10.5: version "0.10.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" @@ -24661,17 +24677,17 @@ regexpp@^3.0.0, regexpp@^3.1.0, regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regexpu-core@^4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" - integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== +regexpu-core@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3" + integrity sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw== dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.0.1" + regjsgen "^0.6.0" + regjsparser "^0.8.2" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" registry-auth-token@^4.0.0: version "4.1.1" @@ -24694,15 +24710,15 @@ registry-url@^5.0.0: dependencies: rc "^1.2.8" -regjsgen@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" - integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== +regjsgen@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" + integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== -regjsparser@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" - integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== +regjsparser@^0.8.2: + version "0.8.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" + integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== dependencies: jsesc "~0.5.0" @@ -28464,23 +28480,23 @@ unicode-byte-truncate@^1.0.0: is-integer "^1.0.6" unicode-substring "^0.1.0" -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" - integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== +unicode-match-property-value-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" + integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== unicode-properties@^1.2.2: version "1.3.1" @@ -28490,10 +28506,10 @@ unicode-properties@^1.2.2: base64-js "^1.3.0" unicode-trie "^2.0.0" -unicode-property-aliases-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" - integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg== +unicode-property-aliases-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" + integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== unicode-substring@^0.1.0: version "0.1.0" From 33b85f8968756183d5cd04e402e28209094bc343 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Tue, 29 Mar 2022 16:06:42 -0400 Subject: [PATCH 027/108] [Security Solution] Use session view plugin to render session viewer in alerts, events and timeline (#127520) --- .../common/ecs/process/index.ts | 9 + .../common/types/timeline/index.ts | 1 + x-pack/plugins/security_solution/kibana.json | 1 + .../components/alerts_viewer/alerts_table.tsx | 2 +- .../events_tab/events_query_tab_body.tsx | 2 +- .../components/events_viewer/index.test.tsx | 25 +- .../common/components/events_viewer/index.tsx | 30 +- .../public/common/mock/global_state.ts | 1 + .../public/common/mock/timeline_results.ts | 2 + .../components/alerts_table/actions.test.tsx | 1 + .../alerts_table/default_config.tsx | 1 + .../components/alerts_table/index.tsx | 2 +- .../components/graph_overlay/index.test.tsx | 119 +++- .../components/graph_overlay/index.tsx | 167 ++--- .../__snapshots__/index.test.tsx.snap | 598 +++++++++--------- .../side_panel/event_details/index.tsx | 4 +- .../hooks/use_detail_panel.test.tsx | 150 +++++ .../side_panel/hooks/use_detail_panel.tsx | 138 ++++ .../timeline/body/actions/index.tsx | 45 ++ .../components/timeline/body/index.tsx | 2 +- .../components/timeline/body/translations.ts | 7 + .../timeline/eql_tab_content/index.tsx | 2 +- .../timeline/graph_tab_content/index.tsx | 31 +- .../timelines/components/timeline/index.tsx | 12 +- .../timeline/pinned_tab_content/index.tsx | 2 +- .../timeline/query_tab_content/index.tsx | 2 +- .../timeline/session_tab_content/index.tsx | 58 ++ .../session_tab_content}/translations.ts | 7 + .../use_session_view.test.tsx | 137 ++++ .../session_tab_content/use_session_view.tsx | 230 +++++++ .../timeline/tabs_content/index.tsx | 63 +- .../timeline/tabs_content/translations.ts | 7 + .../timelines/store/timeline/actions.ts | 5 + .../timelines/store/timeline/defaults.ts | 1 + .../timelines/store/timeline/epic.test.ts | 1 + .../timelines/store/timeline/helpers.ts | 20 + .../public/timelines/store/timeline/model.ts | 2 + .../timelines/store/timeline/reducer.test.ts | 1 + .../timelines/store/timeline/reducer.ts | 6 + .../plugins/security_solution/public/types.ts | 4 +- .../public/components/process_tree/helpers.ts | 11 +- .../public/components/process_tree/hooks.ts | 9 +- .../components/process_tree_alerts/styles.ts | 2 +- .../components/process_tree_node/index.tsx | 2 +- .../public/components/session_view/index.tsx | 2 +- .../public/components/session_view/styles.ts | 12 +- x-pack/plugins/session_view/public/index.ts | 2 +- x-pack/plugins/session_view/public/types.ts | 12 +- .../timelines/common/ecs/process/index.ts | 9 + .../timelines/common/types/timeline/index.ts | 1 + .../components/t_grid/integrated/index.tsx | 1 + .../timeline/factory/helpers/constants.ts | 9 + 52 files changed, 1434 insertions(+), 536 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/index.tsx rename x-pack/plugins/security_solution/public/timelines/components/{graph_overlay => timeline/session_tab_content}/translations.ts (73%) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx diff --git a/x-pack/plugins/security_solution/common/ecs/process/index.ts b/x-pack/plugins/security_solution/common/ecs/process/index.ts index 2a58c6d5b47d0..02122c776e95d 100644 --- a/x-pack/plugins/security_solution/common/ecs/process/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/process/index.ts @@ -11,6 +11,9 @@ export interface ProcessEcs { Ext?: Ext; command_line?: string[]; entity_id?: string[]; + entry_leader?: ProcessSessionData; + session_leader?: ProcessSessionData; + group_leader?: ProcessSessionData; exit_code?: number[]; hash?: ProcessHashData; parent?: ProcessParentData; @@ -25,6 +28,12 @@ export interface ProcessEcs { working_directory?: string[]; } +export interface ProcessSessionData { + entity_id?: string[]; + pid?: string[]; + name?: string[]; +} + export interface ProcessHashData { md5?: string[]; sha1?: string[]; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 93dd6f9efb671..d2e9c2a6715fe 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -482,6 +482,7 @@ export enum TimelineTabs { notes = 'notes', pinned = 'pinned', eql = 'eql', + session = 'session', } /** diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index bd18b5d4acc31..cf13586a1a03f 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -22,6 +22,7 @@ "licensing", "maps", "ruleRegistry", + "sessionView", "taskManager", "timelines", "triggersActionsUi", diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index 0dd137a2321c6..7e71174c85a14 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -80,7 +80,7 @@ const AlertsTableComponent: React.FC = ({ const dispatch = useDispatch(); const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); const { filterManager } = useKibana().services.data.query; - const ACTION_BUTTON_COUNT = 4; + const ACTION_BUTTON_COUNT = 5; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx index cfd6546470d4a..30b7bd4f53e60 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx @@ -84,7 +84,7 @@ const EventsQueryTabBodyComponent: React.FC = }) => { const dispatch = useDispatch(); const { globalFullScreen } = useGlobalFullScreen(); - const ACTION_BUTTON_COUNT = 4; + const ACTION_BUTTON_COUNT = 5; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index bd3321511156d..9da27bc470a94 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -9,9 +9,8 @@ import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import '../../mock/match_media'; -import { waitFor } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; @@ -61,37 +60,27 @@ const testProps = { start: from, }; describe('StatefulEventsViewer', () => { - const mount = useMountAppended(); - (useTimelineEvents as jest.Mock).mockReturnValue([false, mockEventViewerResponse]); test('it renders the events viewer', async () => { - const wrapper = mount( + const wrapper = render( ); - await waitFor(() => { - wrapper.update(); - - expect(wrapper.text()).toMatchInlineSnapshot(`"hello grid"`); - }); + expect(wrapper.getByText('hello grid')).toBeTruthy(); }); // InspectButtonContainer controls displaying InspectButton components test('it renders InspectButtonContainer', async () => { - const wrapper = mount( + const wrapper = render( ); - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`InspectButtonContainer`).exists()).toBe(true); - }); + expect(wrapper.getByTestId(`hoverVisibilityContainer`)).toBeTruthy(); }); test('it closes field editor when unmounted', async () => { @@ -101,14 +90,14 @@ describe('StatefulEventsViewer', () => { return {}; }); - const wrapper = mount( + const { unmount } = render( ); expect(mockCloseEditor).not.toHaveBeenCalled(); - wrapper.unmount(); + unmount(); expect(mockCloseEditor).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 0053ed13923d4..d46ab4b62be68 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -24,7 +24,6 @@ import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererDataView } from '../../containers/sourcerer'; import type { EntityType } from '../../../../../timelines/common'; import { TGridCellAction } from '../../../../../timelines/common/types'; -import { DetailsPanel } from '../../../timelines/components/side_panel'; import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants'; import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; @@ -33,6 +32,7 @@ import { useFieldBrowserOptions, FieldEditorActions, } from '../../../timelines/components/fields_browser'; +import { useSessionView } from '../../../timelines/components/timeline/session_tab_content/use_session_view'; const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; @@ -105,6 +105,7 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, kqlMode, + sessionViewId, showCheckboxes, sort, } = defaultModel, @@ -155,11 +156,19 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; - const graphOverlay = useMemo( - () => - graphEventId != null && graphEventId.length > 0 ? : null, - [graphEventId, id] - ); + + const { DetailsPanel, SessionView, Navigation } = useSessionView({ + entityType, + timelineId: id, + }); + + const graphOverlay = useMemo(() => { + const shouldShowOverlay = + (graphEventId != null && graphEventId.length > 0) || sessionViewId !== null; + return shouldShowOverlay ? ( + + ) : null; + }, [graphEventId, id, sessionViewId, SessionView, Navigation]); const setQuery = useCallback( (inspect, loading, refetch) => { dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); @@ -239,14 +248,7 @@ const StatefulEventsViewerComponent: React.FC = ({ })} - + {DetailsPanel} ); diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index bd90892a43fc6..31948a13db0f1 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -321,6 +321,7 @@ export const mockGlobalState: State = { end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, + sessionViewId: null, show: false, showCheckboxes: false, pinnedEventIds: {}, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 2de29a8c3acf8..4bbdca8564a8e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2011,6 +2011,7 @@ export const mockTimelineModel: TimelineModel = { savedObjectId: 'ef579e40-jibber-jabber', selectAll: false, selectedEventIds: {}, + sessionViewId: null, show: false, showCheckboxes: false, sort: [ @@ -2132,6 +2133,7 @@ export const defaultTimelineProps: CreateTimelineProps = { savedObjectId: null, selectAll: false, selectedEventIds: {}, + sessionViewId: null, show: false, showCheckboxes: false, sort: [{ columnId: '@timestamp', columnType: 'number', sortDirection: Direction.desc }], diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 4f8882ee823b3..e75cfcd6befa9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -312,6 +312,7 @@ describe('alert actions', () => { savedObjectId: null, selectAll: false, selectedEventIds: {}, + sessionViewId: null, show: true, showCheckboxes: false, sort: [ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index dc8c5bf4de65e..7dc3561628193 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -176,4 +176,5 @@ export const requiredFieldsForActions = [ 'file.hash.sha256', 'host.os.family', 'event.code', + 'process.entry_leader.entity_id', ]; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index c82c0c11237ee..b4f81e3e5f0e4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -104,7 +104,7 @@ export const AlertsTableComponent: React.FC = ({ const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); const { addWarning } = useAppToasts(); - const ACTION_BUTTON_COUNT = 4; + const ACTION_BUTTON_COUNT = 5; const getGlobalQuery = useCallback( (customFilters: Filter[]) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index e8d144f07827f..90a5798108d88 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import { waitFor } from '@testing-library/react'; -import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; import React from 'react'; +import '@testing-library/jest-dom'; import { useGlobalFullScreen, useTimelineFullScreen, @@ -37,6 +37,24 @@ jest.mock('../../../resolver/view/use_state_syncing_actions'); const useStateSyncingActionsMock = useStateSyncingActions as jest.Mock; jest.mock('../../../resolver/view/use_sync_selected_node'); +jest.mock('../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../common/lib/kibana'); + return { + ...original, + useKibana: () => ({ + services: { + sessionView: { + getSessionView: () =>
, + }, + data: { + search: { + search: jest.fn(), + }, + }, + }, + }), + }; +}); describe('GraphOverlay', () => { const { storage } = createSecuritySolutionStorageMock(); @@ -54,20 +72,18 @@ describe('GraphOverlay', () => { }); describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => { - test('it has 100% width when NOT in full screen mode', async () => { - const wrapper = mount( + test('it has 100% width when NOT in full screen mode', () => { + const wrapper = render( - + } Navigation={
} /> ); - await waitFor(() => { - const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('width', '100%'); - }); + const overlayContainer = wrapper.getByTestId('overlayContainer'); + expect(overlayContainer).toHaveStyleRule('width', '100%'); }); - test('it has a fixed position when in full screen mode', async () => { + test('it has a fixed position when in full screen mode', () => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: true, setGlobalFullScreen: jest.fn(), @@ -77,20 +93,18 @@ describe('GraphOverlay', () => { setTimelineFullScreen: jest.fn(), }); - const wrapper = mount( + const wrapper = render( - + } Navigation={
} /> ); - await waitFor(() => { - const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('position', 'fixed'); - }); + const overlayContainer = wrapper.getByTestId('overlayContainer'); + expect(overlayContainer).toHaveStyleRule('position', 'fixed'); }); test('it gets index pattern from default data view', () => { - mount( + render( { storage )} > - + } Navigation={
} /> ); @@ -123,20 +137,18 @@ describe('GraphOverlay', () => { describe('when used in the active timeline', () => { const timelineId = TimelineId.active; - test('it has 100% width when NOT in full screen mode', async () => { - const wrapper = mount( + test('it has 100% width when NOT in full screen mode', () => { + const wrapper = render( - + } Navigation={
} /> ); - await waitFor(() => { - const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('width', '100%'); - }); + const overlayContainer = wrapper.getByTestId('overlayContainer'); + expect(overlayContainer).toHaveStyleRule('width', '100%'); }); - test('it has 100% width when the active timeline is in full screen mode', async () => { + test('it has 100% width when the active timeline is in full screen mode', () => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: false, setGlobalFullScreen: jest.fn(), @@ -146,20 +158,18 @@ describe('GraphOverlay', () => { setTimelineFullScreen: jest.fn(), }); - const wrapper = mount( + const wrapper = render( - + } Navigation={
} /> ); - await waitFor(() => { - const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('width', '100%'); - }); + const overlayContainer = wrapper.getByTestId('overlayContainer'); + expect(overlayContainer).toHaveStyleRule('width', '100%'); }); test('it gets index pattern from Timeline data view', () => { - mount( + render( { storage )} > - + } Navigation={
} /> ); expect(useStateSyncingActionsMock.mock.calls[0][0].indices).toEqual(mockIndexNames); }); + + test('it renders session view controls', () => { + (useGlobalFullScreen as jest.Mock).mockReturnValue({ + globalFullScreen: false, + setGlobalFullScreen: jest.fn(), + }); + (useTimelineFullScreen as jest.Mock).mockReturnValue({ + timelineFullScreen: true, + setTimelineFullScreen: jest.fn(), + }); + + const wrapper = render( + + } + Navigation={
{'Close Session'}
} + /> +
+ ); + + expect(wrapper.getByText('Close Session')).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 64475147edc9d..694003311e6c8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -5,25 +5,10 @@ * 2.0. */ -import { - EuiButtonEmpty, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiToolTip, - EuiLoadingSpinner, -} from '@elastic/eui'; -import React, { useCallback, useMemo, useEffect } from 'react'; +import React, { useMemo, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiLoadingSpinner } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; - -import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; -import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; -import { - FULL_SCREEN_TOGGLED_CLASS_NAME, - SCROLLING_DISABLED_CLASS_NAME, -} from '../../../../common/constants'; import { useGlobalFullScreen, useTimelineFullScreen, @@ -33,7 +18,6 @@ import { TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; import { isFullScreen } from '../timeline/body/column_headers'; -import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions'; import { inputsActions } from '../../../common/store/actions'; import { Resolver } from '../../../resolver/view'; import { @@ -41,7 +25,6 @@ import { startSelector, endSelector, } from '../../../common/components/super_date_picker/selectors'; -import * as i18n from './translations'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { sourcererSelectors } from '../../../common/store'; @@ -66,71 +49,35 @@ const StyledResolver = styled(Resolver)` height: 100%; `; -const FullScreenButtonIcon = styled(EuiButtonIcon)` - margin: 4px 0 4px 0; +const ScrollableFlexItem = styled(EuiFlexItem)` + ${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`} + overflow: hidden; + width: 100%; `; -interface OwnProps { - timelineId: TimelineId; -} - -interface NavigationProps { - fullScreen: boolean; - globalFullScreen: boolean; - onCloseOverlay: () => void; +interface GraphOverlayProps { timelineId: TimelineId; - timelineFullScreen: boolean; - toggleFullScreen: () => void; + SessionView: JSX.Element | null; + Navigation: JSX.Element | null; } -const NavigationComponent: React.FC = ({ - fullScreen, - globalFullScreen, - onCloseOverlay, +const GraphOverlayComponent: React.FC = ({ timelineId, - timelineFullScreen, - toggleFullScreen, -}) => ( - - - - {i18n.CLOSE_ANALYZER} - - - {timelineId !== TimelineId.active && ( - - - - - - )} - -); - -NavigationComponent.displayName = 'NavigationComponent'; - -const Navigation = React.memo(NavigationComponent); - -const GraphOverlayComponent: React.FC = ({ timelineId }) => { + SessionView, + Navigation, +}) => { const dispatch = useDispatch(); - const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); - const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); + const { timelineFullScreen } = useTimelineFullScreen(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const graphEventId = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).graphEventId ); + const sessionViewId = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId + ); + const getStartSelector = useMemo(() => startSelector(), []); const getEndSelector = useMemo(() => endSelector(), []); const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []); @@ -163,24 +110,6 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { ); const isInTimeline = timelineId === TimelineId.active; - const onCloseOverlay = useCallback(() => { - const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null; - // Since EUI changes these values directly as a side effect, need to add them back on close. - if (isDataGridFullScreen) { - if (timelineId === TimelineId.active) { - document.body.classList.add('euiDataGrid__restrictBody'); - } else { - document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody'); - } - } else { - if (timelineId === TimelineId.active) { - setTimelineFullScreen(false); - } else { - setGlobalFullScreen(false); - } - } - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); - }, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen]); useEffect(() => { return () => { @@ -192,20 +121,6 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { }; }, [dispatch, timelineId]); - const toggleFullScreen = useCallback(() => { - if (timelineId === TimelineId.active) { - setTimelineFullScreen(!timelineFullScreen); - } else { - setGlobalFullScreen(!globalFullScreen); - } - }, [ - timelineId, - setTimelineFullScreen, - timelineFullScreen, - setGlobalFullScreen, - globalFullScreen, - ]); - const getDefaultDataViewSelector = useMemo( () => sourcererSelectors.defaultDataViewSelector(), [] @@ -219,21 +134,32 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { [defaultDataView.patternList, isInTimeline, timelinePatterns] ); - if (fullScreen && !isInTimeline) { + if (!isInTimeline && sessionViewId !== null) { + if (fullScreen) { + return ( + + + {Navigation} + {SessionView} + + + ); + } else { + return ( + + + {Navigation} + {SessionView} + + + ); + } + } else if (fullScreen && !isInTimeline) { return ( - - - + {Navigation} {graphEventId !== undefined ? ( @@ -256,16 +182,7 @@ const GraphOverlayComponent: React.FC = ({ timelineId }) => { - - - + {Navigation} {graphEventId !== undefined ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 01089552be251..6ea24e5ca57f6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -380,6 +380,313 @@ Array [ runtimeMappings={Object {}} tabType="query" timelineId="test" + > + + +
+ + + +
+ +
+ +
+ + + +
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+ +
+ +
+ +
+ +
+ + + + + +
+ , + .c0 { + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + margin-top: 8px; +} + +.c1 .euiFlyoutBody__overflow { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + overflow: hidden; +} + +.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + overflow: hidden; + padding: 0 16px 16px; +} + +
+
- , - .c0 { - -webkit-flex: 0 1 auto; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - margin-top: 8px; -} - -.c1 .euiFlyoutBody__overflow { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; -} - -.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; - padding: 0 16px 16px; -} - -
-
@@ -157,7 +161,9 @@ exports[`Storyshots renderers/DropdownFilter with choices and new value 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
@@ -218,7 +224,9 @@ exports[`Storyshots renderers/DropdownFilter with choices and value 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
@@ -261,7 +269,9 @@ exports[`Storyshots renderers/DropdownFilter with new value 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot index 5abd1e9fd05b6..e82b6bf082b05 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot @@ -42,13 +42,17 @@ exports[`Storyshots renderers/TimeFilter default 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + + > + + @@ -91,7 +95,9 @@ exports[`Storyshots renderers/TimeFilter default 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + + @@ -150,13 +156,17 @@ exports[`Storyshots renderers/TimeFilter with absolute time bounds 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + + > + + @@ -235,7 +245,9 @@ exports[`Storyshots renderers/TimeFilter with absolute time bounds 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + + @@ -294,13 +306,17 @@ exports[`Storyshots renderers/TimeFilter with commonlyUsedRanges 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + + > + + @@ -343,7 +359,9 @@ exports[`Storyshots renderers/TimeFilter with commonlyUsedRanges 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + + @@ -402,13 +420,17 @@ exports[`Storyshots renderers/TimeFilter with dateFormat 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + + > + + @@ -451,7 +473,9 @@ exports[`Storyshots renderers/TimeFilter with dateFormat 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + + @@ -510,13 +534,17 @@ exports[`Storyshots renderers/TimeFilter with relative time bounds 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + + > + + @@ -595,7 +623,9 @@ exports[`Storyshots renderers/TimeFilter with relative time bounds 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot index e3badfa833090..7c0a2ad18c3dc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot @@ -65,7 +65,9 @@ exports[`Storyshots arguments/AxisConfig extended 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
@@ -143,7 +145,9 @@ exports[`Storyshots arguments/AxisConfig/components extended 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot index 238fe7c259c6e..9755e1b53b868 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot @@ -47,7 +47,9 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -120,7 +122,9 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -192,7 +196,9 @@ exports[`Storyshots arguments/DateFormat with preset format 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot index 2159e49e2bcf1..ecd8e53ce1d25 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot @@ -57,7 +57,9 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -140,7 +142,9 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -222,7 +226,9 @@ exports[`Storyshots arguments/NumberFormat with preset format 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot index 6db24bd0b984c..587b07ca4f932 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot @@ -81,7 +81,9 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - /> + > + + @@ -111,7 +113,9 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + + @@ -142,7 +146,9 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - /> + > + + @@ -169,7 +175,9 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -260,7 +268,9 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - /> + > + + @@ -290,7 +300,9 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + + @@ -321,7 +333,9 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - /> + > + + @@ -348,7 +362,9 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot index dd650e9f4c697..5409f9c444df0 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot @@ -26,7 +26,9 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
@@ -122,7 +126,9 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` color="subdued" data-euiicon-type="importAction" size="xxl" - /> + > + +
+ > + +
+ > + +
@@ -380,7 +390,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - /> + > + +
@@ -410,7 +422,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + +
@@ -441,7 +455,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - /> + > + +
@@ -468,7 +484,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + +
@@ -548,7 +566,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - /> + > + + @@ -578,7 +598,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + + @@ -609,7 +631,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - /> + > + + @@ -636,7 +660,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot b/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot index 056b87294f245..5d83b2718f916 100644 --- a/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot @@ -129,7 +129,9 @@ Array [ + > + + ,
+ > + +
,
+ > + +
,
+ > + +
, ] diff --git a/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot index cb3598430c7ef..057bd37b71c20 100644 --- a/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot @@ -394,7 +394,9 @@ exports[`Storyshots components/Color/ColorManager interactive 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + @@ -805,7 +809,9 @@ Array [ color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + , @@ -886,7 +894,9 @@ Array [ color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + , @@ -965,7 +977,9 @@ Array [ color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + , diff --git a/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot b/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot index a0d27eafb23dc..53651c8fe33f2 100644 --- a/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot @@ -393,7 +393,9 @@ Array [ className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + @@ -758,7 +760,9 @@ exports[`Storyshots components/Color/ColorPalette six colors, wrap at 4 1`] = ` className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + @@ -1040,7 +1044,9 @@ Array [ className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot index 6ef3eec47e701..557f94c26fac9 100644 --- a/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot @@ -237,7 +237,9 @@ exports[`Storyshots components/Color/ColorPicker interactive 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + @@ -318,7 +322,9 @@ exports[`Storyshots components/Color/ColorPicker six colors 1`] = ` className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + @@ -526,7 +532,9 @@ exports[`Storyshots components/Color/ColorPicker six colors 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + @@ -786,7 +796,9 @@ exports[`Storyshots components/Color/ColorPicker six colors, value missing 1`] = color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + @@ -846,7 +860,9 @@ exports[`Storyshots components/Color/ColorPicker three colors 1`] = ` className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + @@ -970,7 +986,9 @@ exports[`Storyshots components/Color/ColorPicker three colors 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot index d8c660923e3d7..feb04e68ca1d3 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot @@ -27,7 +27,9 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
@@ -224,7 +228,9 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - /> + > + +
+ > + +
+ > + +
@@ -646,7 +656,9 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
@@ -843,7 +857,9 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - /> + > + +
+ > + +
+ > + +
@@ -1146,7 +1166,9 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = ` className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - /> + > + +
+ > + + + > + + Test Datasource @@ -70,14 +74,18 @@ exports[`Storyshots components/datasource/DatasourceComponent simple datasource color="inherit" data-euiicon-type="arrowRight" size="m" - /> + > + + + > + + Test Datasource diff --git a/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot b/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot index 14640fe266839..05cec59522ae7 100644 --- a/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot @@ -19,7 +19,9 @@ exports[`Storyshots components/Elements/ElementCard with click handler 1`] = ` className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - /> + > + +
+ > + +
+ > + +
+ > + +
diff --git a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot index d3ab369dcc32c..0863fd13af607 100644 --- a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot @@ -59,7 +59,9 @@ exports[`Storyshots Home Home Page 1`] = ` color="inherit" data-euiicon-type="plusInCircleFilled" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot index 8f00060a1dd1c..fa3789124ce81 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot @@ -28,7 +28,9 @@ exports[`Storyshots Home/Components/Empty Prompt Empty Prompt 1`] = ` color="subdued" data-euiicon-type="importAction" size="xxl" - /> + > + +
+ > + +
@@ -73,7 +75,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiFilePicker__icon" data-euiicon-type="importAction" size="m" - /> + > + +
@@ -150,7 +154,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -296,7 +302,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiTableSortIcon" data-euiicon-type="sortDown" size="m" - /> + > + + @@ -460,7 +468,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="exportAction" size="m" - /> + > + +
@@ -486,7 +496,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="copy" size="m" - /> + > + +
@@ -632,7 +644,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="exportAction" size="m" - /> + > + +
@@ -658,7 +672,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="copy" size="m" - /> + > + +
@@ -804,7 +820,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="exportAction" size="m" - /> + > + +
@@ -830,7 +848,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="copy" size="m" - /> + > + +
@@ -873,7 +893,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -915,7 +937,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - /> + > + +
    + > + +
diff --git a/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot index dbb591582e909..e96302525aea4 100644 --- a/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot @@ -73,7 +73,9 @@ exports[`Storyshots components/ItemGrid complex grid 1`] = ` + > + +
+ > + +
+ > + +
@@ -125,13 +131,19 @@ exports[`Storyshots components/ItemGrid icon grid 1`] = ` > + > + + + > + + + > + + `; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot index 6f139df7c8773..9f462d9a4d6cd 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot @@ -34,7 +34,9 @@ exports[`Storyshots components/SavedElementsModal/ElementControls has two button color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -61,7 +63,9 @@ exports[`Storyshots components/SavedElementsModal/ElementControls has two button color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot index 70ee9f543d768..fbab31e5c8c5b 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot @@ -85,7 +85,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -112,7 +114,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -192,7 +196,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -219,7 +225,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -299,7 +307,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -326,7 +336,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot index fd6f29178aa91..e0b7f40657cf8 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot @@ -26,7 +26,9 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
@@ -99,7 +103,9 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` color="subdued" data-euiicon-type="vector" size="xxl" - /> + > + +
+ > + +
+ > + +
@@ -327,7 +337,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -354,7 +366,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -434,7 +448,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -461,7 +477,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -541,7 +559,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -568,7 +588,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -634,7 +656,9 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
+ > + +
@@ -787,7 +815,9 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -814,7 +844,9 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot index 6bf2535131afc..d5e5af856909b 100644 --- a/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot @@ -72,7 +72,9 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="sortUp" size="m" - /> + > + + @@ -98,7 +100,9 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="arrowUp" size="m" - /> + > + + @@ -124,7 +128,9 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="m" - /> + > + + @@ -150,7 +156,9 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot index f21ffcf1a70ea..2a1e12c1e0b74 100644 --- a/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot @@ -57,7 +57,9 @@ exports[`Storyshots components/Tags/Tag as health 1`] = ` + > + +
+ > + +
+ > + +
+ > + +
+ > + +
+ > + +
@@ -244,7 +246,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorBold" size="m" - /> + > + + @@ -274,7 +278,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorItalic" size="m" - /> + > + + @@ -304,7 +310,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorUnderline" size="m" - /> + > + + @@ -348,7 +356,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorAlignLeft" size="m" - /> + > + + @@ -384,7 +394,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorAlignCenter" size="m" - /> + > + + @@ -420,7 +432,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorAlignRight" size="m" - /> + > + + @@ -598,7 +612,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -690,7 +706,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorBold" size="m" - /> + > + + @@ -720,7 +738,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorItalic" size="m" - /> + > + + @@ -750,7 +770,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorUnderline" size="m" - /> + > + + @@ -794,7 +816,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorAlignLeft" size="m" - /> + > + + @@ -830,7 +854,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorAlignCenter" size="m" - /> + > + + @@ -866,7 +892,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorAlignRight" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot index f5351b0d8ea5f..0d8a5c0cf4e5d 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot @@ -20,7 +20,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot index 6c70364f9679c..72e1b4d6ef909 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot @@ -20,7 +20,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + @@ -255,7 +259,9 @@ Array [ color="inherit" data-euiicon-type="save" size="m" - /> + > + + @@ -310,7 +316,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + @@ -486,7 +496,9 @@ Array [ color="inherit" data-euiicon-type="save" size="m" - /> + > + + @@ -541,7 +553,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + @@ -717,7 +733,9 @@ Array [ color="inherit" data-euiicon-type="save" size="m" - /> + > + + @@ -772,7 +790,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot index 7d43840e431ab..ac27b0443585a 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot @@ -28,7 +28,9 @@ exports[`Storyshots components/Variables/VarConfig default 1`] = ` color="inherit" data-euiicon-type="arrowRight" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot index b6d842ac44e21..57fbd4c2109cd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot @@ -33,7 +33,9 @@ exports[`Storyshots components/WorkpadFilters/FiltersGroupComponent default 1`] color="inherit" data-euiicon-type="arrowRight" size="m" - /> + > + +
+ > + +
@@ -1467,7 +1473,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + @@ -2809,7 +2817,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` + > + +
+ > + +
+ > + +
@@ -2949,7 +2963,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + @@ -3107,7 +3123,9 @@ exports[`Storyshots shareables/Canvas contextual: hello 1`] = ` + > + +
+ > + +
+ > + +
@@ -3247,7 +3269,9 @@ exports[`Storyshots shareables/Canvas contextual: hello 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot index 90ebc1900d731..6a8d67a70ad1a 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot @@ -1280,7 +1280,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` + > + +
+ > + +
+ > + +
@@ -1420,7 +1426,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + @@ -1532,7 +1540,9 @@ exports[`Storyshots shareables/Footer contextual: hello 1`] = ` + > + +
+ > + +
+ > + +
@@ -1672,7 +1686,9 @@ exports[`Storyshots shareables/Footer contextual: hello 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot index 9edb6f1fda62f..f2b92754b6d6f 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot @@ -34,7 +34,9 @@ exports[`Storyshots shareables/Footer/PageControls component 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - /> + > + +
+ > + +
@@ -131,7 +135,9 @@ exports[`Storyshots shareables/Footer/PageControls contextual: austin 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - /> + > + +
+ > + +
@@ -228,7 +236,9 @@ exports[`Storyshots shareables/Footer/PageControls contextual: hello 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - /> + > + +
+ > + +
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot index 2b326fd0ec51a..ea19100f6da87 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot @@ -34,7 +34,9 @@ exports[`Storyshots shareables/Footer/Title component 1`] = ` + > + +
+ > + +
+ > + +
+ > + + + > + + @@ -212,12 +216,16 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: on, 5 className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + @@ -376,12 +384,16 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings contextual 1`] = className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot index 265cbe460607d..3c3f26bce7e9e 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot @@ -39,7 +39,9 @@ exports[`Storyshots shareables/Footer/Settings component 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + +
@@ -87,7 +89,9 @@ exports[`Storyshots shareables/Footer/Settings contextual 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot index 1aafb9cc6b664..d07e5a9edc8ad 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot @@ -59,12 +59,16 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: off 1` className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + @@ -147,12 +151,16 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: on 1`] className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + @@ -235,12 +243,16 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings contextual 1`] = className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index 4fd56525541a6..63fc2e2695a3a 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -264,7 +264,7 @@ describe('ConnectorsDropdown', () => { wrapper: ({ children }) => {children}, }); - const tooltips = screen.getAllByLabelText( + const tooltips = screen.getAllByText( 'This connector is deprecated. Update it, or create a new one.' ); expect(tooltips[0]).toBeInTheDocument(); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx index af803cfc14e05..8cb8b7f23b439 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx @@ -25,7 +25,7 @@ describe('Markdown', () => { test('it renders the expected link text', () => { const result = appMockRender.render({markdownWithLink}); - expect(removeExternalLinkText(result.getByTestId('markdown-link').textContent)).toEqual( + expect(removeExternalLinkText(result.getByTestId('markdown-link').textContent)).toContain( 'External Site' ); }); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx index 863e5e85d9ef3..eb1f82cc01e37 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -138,7 +138,7 @@ describe('Background Search Session Management Table', () => { expect(table.find('tbody td').map((node) => node.text())).toMatchInlineSnapshot(` Array [ "App", - "Namevery background search ", + "Namevery background search Info", "# Searches0", "StatusExpired", "Created2 Dec, 2020, 00:19:32", diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx index 0626a946f8848..d44625b1641ac 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx @@ -65,7 +65,6 @@ export const IndexSetupDatasetFilter: React.FC<{ isSelected={isVisible} onClick={show} iconType="arrowDown" - size="s" > { // Dropdown should be visible and processor status should equal "success" expect(exists('documentsDropdown')).toBe(true); - const initialProcessorStatusLabel = find('processors>0.processorStatusIcon').props()[ - 'aria-label' - ]; + const initialProcessorStatusLabel = find('processors>0.processorStatusIcon').props() + .children; expect(initialProcessorStatusLabel).toEqual('Success'); // Open flyout and click clear all button @@ -320,9 +319,8 @@ describe('Test pipeline', () => { // Verify documents and processors were reset expect(exists('documentsDropdown')).toBe(false); expect(exists('addDocumentsButton')).toBe(true); - const resetProcessorStatusIconLabel = find('processors>0.processorStatusIcon').props()[ - 'aria-label' - ]; + const resetProcessorStatusIconLabel = find('processors>0.processorStatusIcon').props() + .children; expect(resetProcessorStatusIconLabel).toEqual('Not run'); }); }); @@ -332,7 +330,7 @@ describe('Test pipeline', () => { it('should show "inactive" processor status by default', async () => { const { find } = testBed; - const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; + const statusIconLabel = find('processors>0.processorStatusIcon').props().children; expect(statusIconLabel).toEqual('Not run'); }); @@ -352,7 +350,7 @@ describe('Test pipeline', () => { actions.closeTestPipelineFlyout(); // Verify status - const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; + const statusIconLabel = find('processors>0.processorStatusIcon').props().children; expect(statusIconLabel).toEqual('Success'); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 36fd1581cb9b6..2ad20bf0a43e2 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -617,7 +617,9 @@ describe('DatatableComponent', () => { wrapper.setProps({ data: newData }); wrapper.update(); - expect(wrapper.find('[data-test-subj="dataGridHeader"]').children().first().text()).toEqual( + // Using .toContain over .toEqual because this element includes text from + // which can't be seen, but shows in the text content + expect(wrapper.find('[data-test-subj="dataGridHeader"]').children().first().text()).toContain( 'new a' ); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index c86fdcc33b15f..c20f1c37c6c67 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -120,7 +120,10 @@ describe('IndexPattern Field Item', () => { it('should display displayName of a field', () => { const wrapper = mountWithIntl(); - expect(wrapper.find('[data-test-subj="lnsFieldListPanelField"]').first().text()).toEqual( + + // Using .toContain over .toEqual because this element includes text from + // which can't be seen, but shows in the text content + expect(wrapper.find('[data-test-subj="lnsFieldListPanelField"]').first().text()).toContain( 'bytesLabel' ); }); diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index fda479f2888ce..0fd589e4886e3 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap index 4fa45c4bec5ce..cf977731ee452 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription featuresExternal link(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription featuresExternal link(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription featuresExternal link(opens in a new tab or window).

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap index 622bff86ead16..0880eddcc1683 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap index c5b5e5e65ab38..41501a7eedb62 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap @@ -87,10 +87,11 @@ Array [ > Elasticsearch Service Console + > + External link + @@ -106,10 +107,11 @@ Array [ > Logs and metrics + > + External link + @@ -125,10 +127,11 @@ Array [ > the documentation page. + > + External link + diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap index dda853a28239f..faab608e7af14 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap @@ -158,10 +158,11 @@ Array [ > Elasticsearch Service Console + > + External link + @@ -177,10 +178,11 @@ Array [ > Logs and metrics + > + External link + @@ -196,10 +198,11 @@ Array [ > the documentation page. + > + External link + diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js index a6987fa19d1ee..26af30ba17c04 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js @@ -252,7 +252,7 @@ describe('', () => { ], [ '', - remoteCluster2.name, + remoteCluster2.name.concat('Info'), //Tests include the word "info" to account for the rendered text coming from EuiIcon 'Not connected', PROXY_MODE, remoteCluster2.proxyAddress, @@ -261,7 +261,7 @@ describe('', () => { ], [ '', - remoteCluster3.name, + remoteCluster3.name.concat('Info'), //Tests include the word "info" to account for the rendered text coming from EuiIcon 'Not connected', PROXY_MODE, remoteCluster2.proxyAddress, @@ -360,7 +360,7 @@ describe('', () => { ({ rows } = table.getMetaData('remoteClusterListTable')); expect(rows.length).toBe(2); - expect(rows[0].columns[1].value).toEqual(remoteCluster2.name); + expect(rows[0].columns[1].value).toContain(remoteCluster2.name); }); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx index 7052f724cd1cc..006ae053940d8 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx @@ -40,7 +40,7 @@ describe('FeatureTableCell', () => { ); - expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature "`); + expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature Info"`); expect(wrapper.find(EuiIconTip).props().content).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts index 3a70ff5713bd9..f375263c960c3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts @@ -60,7 +60,7 @@ export function getDisplayedFeaturePrivileges( acc[feature.id][key] = { ...acc[feature.id][key], - primaryFeaturePrivilege: primary.text().trim(), + primaryFeaturePrivilege: primary.text().replaceAll('Info', '').trim(), // Removing the word "info" to account for the rendered text coming from EuiIcon hasCustomizedSubFeaturePrivileges: findTestSubject(primary, 'additionalPrivilegesGranted').length > 0, }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx index 6070924523f63..a53be08380698 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx @@ -158,7 +158,7 @@ describe('ExceptionEntries', () => { expect(parentValue.text()).toEqual(getEmptyValue()); expect(nestedField.exists('.euiToolTipAnchor')).toBeTruthy(); - expect(nestedField.text()).toEqual('host.name'); + expect(nestedField.text()).toContain('host.name'); expect(nestedOperator.text()).toEqual('is'); expect(nestedValue.text()).toEqual('some name'); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx index 7a9c36a986afd..9796ae2624a73 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx @@ -58,7 +58,7 @@ describe('Modal Inspect', () => { expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="index-pattern-title"]').text() - ).toBe('Index pattern '); + ).toContain('Index pattern '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="index-pattern-description"]') @@ -66,7 +66,7 @@ describe('Modal Inspect', () => { ).toBe('auditbeat-*, filebeat-*, packetbeat-*, winlogbeat-*'); expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="query-time-title"]').text() - ).toBe('Query time '); + ).toContain('Query time '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="query-time-description"]') @@ -76,7 +76,7 @@ describe('Modal Inspect', () => { wrapper .find('.euiDescriptionList__title span[data-test-subj="request-timestamp-title"]') .text() - ).toBe('Request timestamp '); + ).toContain('Request timestamp '); }); test('Click on request Tab', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index 97f93b9732c02..adab4db904d6a 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -105,7 +105,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders props passed in as link', () => { @@ -463,7 +463,7 @@ describe('Custom Links', () => { describe('WhoisLink', () => { test('it renders ip passed in as domain', () => { const wrapper = mountWithIntl({'Example Link'}); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders correct href', () => { @@ -488,7 +488,7 @@ describe('Custom Links', () => { {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders correct href', () => { @@ -519,7 +519,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders correct href', () => { @@ -548,7 +548,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders correct href when port is a number', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx index da3785648de62..68588c9338b4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx @@ -20,7 +20,7 @@ describe('Markdown', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="markdown-link"]').first().text()) - ).toEqual('External Site'); + ).toContain('External Site'); }); test('it renders the expected href', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/optional_eui_tour_step.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/optional_eui_tour_step.tsx new file mode 100644 index 0000000000000..e08389ba250a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/optional_eui_tour_step.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 React, { FC } from 'react'; + +import { EuiTourStepProps, EuiTourStep, DistributiveOmit } from '@elastic/eui'; + +/** + * This component can be used for tour steps, when tour step is optional + * If stepProps are not supplied, step will not be rendered, only children component will be + */ +export const OptionalEuiTourStep: FC<{ + stepProps: DistributiveOmit | undefined; +}> = ({ children, stepProps }) => { + if (!stepProps) { + return <>{children}; + } + + return ( + + <>{children} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx index 480d200c6756f..ec56dd6934463 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx @@ -54,9 +54,9 @@ describe('Port', () => { ); - expect(removeExternalLinkText(wrapper.find('[data-test-subj="port"]').first().text())).toEqual( - '443' - ); + expect( + removeExternalLinkText(wrapper.find('[data-test-subj="port"]').first().text()) + ).toContain('443'); }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx index 3332111d14f8b..bb8b4683c9d30 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx @@ -205,7 +205,7 @@ describe('SourceDestination', () => { removeExternalLinkText( wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() ) - ).toEqual('10.1.2.3:80'); + ).toContain('10.1.2.3:80'); }); test('it renders destination.packets', () => { @@ -329,7 +329,7 @@ describe('SourceDestination', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) - ).toEqual('192.168.1.2:9987'); + ).toContain('192.168.1.2:9987'); }); test('it renders source.packets', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx index f16cd7dbb109f..6168d98765253 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx @@ -984,7 +984,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() ) - ).toEqual('9987'); + ).toContain('9987'); }); test('it renders the expected destination port when type is `destination`, and both destinationIp and destinationPort are populated', () => { @@ -1038,7 +1038,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() ) - ).toEqual('80'); + ).toContain('80'); }); test('it renders the expected source port when type is `source`, but only sourcePort is populated', () => { @@ -1092,7 +1092,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() ) - ).toEqual('9987'); + ).toContain('9987'); }); test('it renders the expected destination port when type is `destination`, and only destinationPort is populated', () => { @@ -1147,7 +1147,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() ) - ).toEqual('80'); + ).toContain('80'); }); test('it does NOT render the badge when type is `source`, but both sourceIp and sourcePort are undefined', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx index 4ebb804eab8a4..8b3f0bfdb107a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx @@ -51,7 +51,7 @@ describe('CertificateFingerprint', () => { removeExternalLinkText( wrapper.find('[data-test-subj="certificate-fingerprint-link"]').first().text() ) - ).toEqual('3f4c57934e089f02ae7511200aee2d7e7aabd272'); + ).toContain('3f4c57934e089f02ae7511200aee2d7e7aabd272'); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx index 31f2fec942490..ddbba7f2bc9f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx @@ -48,7 +48,7 @@ describe('Ja3Fingerprint', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) - ).toEqual('fff799d91b7c01ae3fe6787cfc895552'); + ).toContain('fff799d91b7c01ae3fe6787cfc895552'); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx index 8a88a7182af03..9ccabf2f47d44 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx @@ -204,7 +204,7 @@ describe('Netflow', () => { removeExternalLinkText( wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() ) - ).toEqual('10.1.2.3:80'); + ).toContain('10.1.2.3:80'); }); test('it renders destination.packets', () => { @@ -340,7 +340,7 @@ describe('Netflow', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) - ).toEqual('192.168.1.2:9987'); + ).toContain('192.168.1.2:9987'); }); test('it renders source.packets', () => { @@ -374,7 +374,7 @@ describe('Netflow', () => { .first() .text() ) - ).toEqual('tls.client_certificate.fingerprint.sha1-value'); + ).toContain('tls.client_certificate.fingerprint.sha1-value'); }); test('it hyperlinks tls.fingerprints.ja3.hash site to compare the fingerprint against a known set of signatures', () => { @@ -390,7 +390,7 @@ describe('Netflow', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) - ).toEqual('tls.fingerprints.ja3.hash-value'); + ).toContain('tls.fingerprints.ja3.hash-value'); }); test('it hyperlinks tls.server_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { @@ -418,7 +418,7 @@ describe('Netflow', () => { .first() .text() ) - ).toEqual('tls.server_certificate.fingerprint.sha1-value'); + ).toContain('tls.server_certificate.fingerprint.sha1-value'); }); test('it renders network.transport', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 6ea24e5ca57f6..ebb807a590124 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -2686,11 +2686,12 @@ exports[`Details Panel Component DetailsPanel:NetworkDetails: rendering it shoul type="popout" > + > + External link + { + return str.replaceAll('External link', ''); +}; + jest.mock('../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { @@ -90,7 +96,7 @@ describe('get_column_renderer', () => { {row} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -109,7 +115,7 @@ describe('get_column_renderer', () => { {row} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -128,7 +134,7 @@ describe('get_column_renderer', () => { {row} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 2d06c040c5b00..f8693d4a4f8ea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -53,7 +53,11 @@ describe('SuricataDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + const removeEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( + 'External link', + '' + ); + expect(removeEuiIconText).toEqual( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index 61ea659964e4d..2022904e548aa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -72,7 +72,12 @@ describe('suricata_row_renderer', () => { {children} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + + const extractEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( + 'External link', + '' + ); + expect(extractEuiIconText).toContain( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index ae2caa8ce8401..4b93c5accb590 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -83,6 +83,12 @@ import { import * as i18n from './translations'; import { RowRenderer } from '../../../../../../../common/types'; +// EuiIcons coming from .testenv render the icon's aria-label as a span +// extractEuiIcon removes the aria-label before checking for equality +const extractEuiIconText = (str: string) => { + return str.replaceAll('External link', ''); +}; + jest.mock('../../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { @@ -1130,7 +1136,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'NETWORK SERVICE\\NT AUTHORITY@windows-endpoint-1accepted a connection viasvchost.exe(328)with resultsuccessEndpoint network eventincomingtcpSource10.1.2.3:64557North AmericaUnited States🇺🇸USNorth CarolinaConcordDestination10.50.60.70:3389' ); }); @@ -1214,7 +1220,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'NETWORK SERVICE\\NT AUTHORITY@win2019-endpoint-1made a http request viasvchost.exe(2232)Endpoint network eventoutgoinghttptcpSource10.1.2.3:51570Destination10.11.12.13:80North AmericaUnited States🇺🇸USArizonaPhoenix' ); }); @@ -1243,7 +1249,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' ); }); @@ -1272,7 +1278,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' ); }); @@ -1298,7 +1304,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'NETWORK SERVICE\\NT AUTHORITY@windows-endpoint-1disconnected viasvchost.exe(328)Endpoint network eventincomingtcpSource10.20.30.40:64557North AmericaUnited States🇺🇸USNorth CarolinaConcord(42.47%)1.2KB(57.53%)1.6KBDestination10.11.12.13:3389' ); }); @@ -1327,7 +1333,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' ); }); @@ -1356,7 +1362,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' ); }); @@ -1385,7 +1391,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' ); }); @@ -1414,7 +1420,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' ); }); @@ -1722,7 +1728,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 62836cbffb2b5..9af22fca0c707 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -14,6 +14,12 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ZeekDetails } from './zeek_details'; +// EuiIcons coming from .testenv render the icon's aria-label as a span +// extractEuiIcon removes the aria-label before checking for equality +const extractEuiIconText = (str: string) => { + return str.replaceAll('External link', ''); +}; + jest.mock('../../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { @@ -53,7 +59,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); @@ -68,7 +74,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'CyIrMA1L1JtLqdIuoldnsudpSource206.189.35.240:57475Destination67.207.67.3:53' ); }); @@ -83,7 +89,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'CZLkpC22NquQJOpkwehttp302Source206.189.35.240:36220Destination192.241.164.26:80' ); }); @@ -98,7 +104,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'noticeDropped:falseScan::Port_Scan8.42.77.171 scanned at least 15 unique ports of host 207.154.238.205 in 0m0sSource8.42.77.171' ); }); @@ -113,7 +119,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'CmTxzt2OVXZLkGDaResslTLSv12TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256Source188.166.66.184:34514Destination91.189.95.15:443' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index b60a2965bfd70..fda83c0ade12b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -71,7 +71,12 @@ describe('zeek_row_renderer', () => { {children} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + + const extractEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( + 'External link', + '' + ); + expect(extractEuiIconText).toContain( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index 3f27b80359131..726716c7f53ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -101,7 +101,11 @@ describe('ZeekSignature', () => { test('should render value', () => { const wrapper = mount(); - expect(removeExternalLinkText(wrapper.text())).toEqual('abc'); + const extractEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( + 'External link', + '' + ); + expect(extractEuiIconText).toEqual('abc'); }); test('should render value and link', () => { diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index 60fe9d2bd7128..a99a6fdb81167 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -769,7 +769,7 @@ describe('', () => { const stateMessage = find('snapshotDetail.state.value').text(); try { - expect(stateMessage).toBe(expectedMessage); + expect(stateMessage).toContain(expectedMessage); // Messages may include the word "Info" to account for the rendered text coming from EuiIcon } catch { throw new Error( `Expected snapshot state message "${expectedMessage}" for state "${state}, but got "${stateMessage}".` diff --git a/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx b/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx index 5ac75f92ea45f..f3846cd784ccc 100644 --- a/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx +++ b/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx @@ -110,7 +110,7 @@ describe('Modal Inspect', () => { expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="index-pattern-title"]').text() - ).toBe('Index pattern '); + ).toContain('Index pattern '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="index-pattern-description"]') @@ -118,7 +118,7 @@ describe('Modal Inspect', () => { ).toBe('auditbeat-*, filebeat-*, packetbeat-*, winlogbeat-*'); expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="query-time-title"]').text() - ).toBe('Query time '); + ).toContain('Query time '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="query-time-description"]') @@ -128,7 +128,7 @@ describe('Modal Inspect', () => { wrapper .find('.euiDescriptionList__title span[data-test-subj="request-timestamp-title"]') .text() - ).toBe('Request timestamp '); + ).toContain('Request timestamp '); }); test('Click on request Tab', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index d23f1cfacf94b..6942a7708db78 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -107,10 +107,12 @@ describe('health check', () => { const [action] = queryAllByText(/Learn more/i); expect(description.textContent).toMatchInlineSnapshot( - `"You must enable API keys to use Alerting. Learn more.(opens in a new tab or window)"` + `"You must enable API keys to use Alerting. Learn more.External link(opens in a new tab or window)"` ); - expect(action.textContent).toMatchInlineSnapshot(`"Learn more.(opens in a new tab or window)"`); + expect(action.textContent).toMatchInlineSnapshot( + `"Learn more.External link(opens in a new tab or window)"` + ); expect(action.getAttribute('href')).toMatchInlineSnapshot( `"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-settings.html#api-key-service-settings"` @@ -141,12 +143,12 @@ describe('health check', () => { const description = queryByRole(/banner/i); expect(description!.textContent).toMatchInlineSnapshot( - `"You must configure an encryption key to use Alerting. Learn more.(opens in a new tab or window)"` + `"You must configure an encryption key to use Alerting. Learn more.External link(opens in a new tab or window)"` ); const action = queryByText(/Learn/i); expect(action!.textContent).toMatchInlineSnapshot( - `"Learn more.(opens in a new tab or window)"` + `"Learn more.External link(opens in a new tab or window)"` ); expect(action!.getAttribute('href')).toMatchInlineSnapshot( `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alert-action-settings-kb.html#general-alert-action-settings"` @@ -179,12 +181,12 @@ describe('health check', () => { const description = queryByText(/You must enable/i); expect(description!.textContent).toMatchInlineSnapshot( - `"You must enable API keys and configure an encryption key to use Alerting. Learn more.(opens in a new tab or window)"` + `"You must enable API keys and configure an encryption key to use Alerting. Learn more.External link(opens in a new tab or window)"` ); const action = queryByText(/Learn/i); expect(action!.textContent).toMatchInlineSnapshot( - `"Learn more.(opens in a new tab or window)"` + `"Learn more.External link(opens in a new tab or window)"` ); expect(action!.getAttribute('href')).toMatchInlineSnapshot( `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alerting-setup.html#alerting-prerequisites"` diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx index 737501f444300..e7cafb23ee0fa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx @@ -373,9 +373,12 @@ describe('execution duration overview', () => { const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]'); expect(avgExecutionDurationPanel.exists()).toBeTruthy(); expect(avgExecutionDurationPanel.first().prop('color')).toEqual('warning'); - expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual( - 'Average duration16:44:44.345' - ); + + const avgExecutionDurationStat = wrapper + .find('EuiStat[data-test-subj="avgExecutionDurationStat"]') + .text() + .replaceAll('Info', ''); + expect(avgExecutionDurationStat).toEqual('Average duration16:44:44.345'); expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeTruthy(); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx index ee485f8aee0c0..3576d7e34fd0b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx @@ -63,5 +63,5 @@ test('Can delete drilldowns', () => { test('Error is displayed', () => { const screen = render(); - expect(screen.getByLabelText('an error')).toBeInTheDocument(); + expect(screen.getByText('an error')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap index 51753d2ce8bb3..bf25513a6bc2c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap @@ -179,10 +179,11 @@ exports[`PingListExpandedRow renders link to docs if body is not recorded but it > docs + > + External link + diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap index 80b751d8e243b..29d1ba922de8f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap @@ -72,10 +72,11 @@ Array [ > Set tags + > + External link + diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx index 63b4d2945a51c..671371093c819 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx @@ -11,7 +11,7 @@ import { render } from '../../../../../lib/helper/rtl_helpers'; describe('NetworkRequestsTotal', () => { it('message in case total is greater than fetched', () => { - const { getByText, getByLabelText } = render( + const { getByText } = render( { ); expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); - expect(getByLabelText('Info')).toBeInTheDocument(); + expect(getByText('Info')).toBeInTheDocument(); }); it('message in case total is equal to fetched requests', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx index 7558a82e45df4..4241a7238ecd6 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx @@ -13,8 +13,8 @@ import { TestWrapper } from './waterfall_marker_test_helper'; describe('', () => { it('renders a dot icon when `field` is an empty string', () => { - const { getByLabelText } = render(); - expect(getByLabelText('An icon indicating that this marker has no field associated with it')); + const { getByText } = render(); + expect(getByText('An icon indicating that this marker has no field associated with it')); }); it('renders an embeddable when opened', async () => { diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx index 2b899aad783d7..d232b12f3a47b 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx @@ -45,20 +45,22 @@ describe('KeyUXMetrics', () => { }; }; + // Tests include the word "info" between the task and time to account for the rendered text coming from + // the EuiIcon (tooltip) embedded within each stat description expect( - getAllByText(checkText('Longest long task duration271 ms'))[0] + getAllByText(checkText('Longest long task durationInfo271 ms'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('Total long tasks duration520 ms'))[0] + getAllByText(checkText('Total long tasks durationInfo520 ms'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('No. of long tasks3'))[0] + getAllByText(checkText('No. of long tasksInfo3'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('Total blocking time271 ms'))[0] + getAllByText(checkText('Total blocking timeInfo271 ms'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('First contentful paint1.27 s'))[0] + getAllByText(checkText('First contentful paintInfo1.27 s'))[0] ).toBeInTheDocument(); }); }); diff --git a/yarn.lock b/yarn.lock index 0b6f13bc96b94..d78a3567a18c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1516,10 +1516,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@51.1.0": - version "51.1.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-51.1.0.tgz#338b710ae7a819bb7c3b8e1916080610e0b8e691" - integrity sha512-pjbBSkfDPAjXBRCMk4zsyZ3sPpf70XVcbOzr4BzT0MW38uKjEgEh6nu1aCdnOi+jVSHRtziJkX9rD8BRDWfsnw== +"@elastic/eui@52.2.0": + version "52.2.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-52.2.0.tgz#761101a29b96a4b5270ef93541dab7bb27f5ca50" + integrity sha512-XboYerntCOTHWHYMWJGzJtu5JYO6pk5IWh0ZHJEQ4SEjmLbTV2bFrVBTO/8uaU7GhV9/RNIo7BU5wHRyYP7z1g== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" From 865f7fc1e121e8a21483c93e4941b076507ed0a6 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Tue, 29 Mar 2022 22:41:00 +0200 Subject: [PATCH 030/108] [SharedUX] Add url-loader to BAZEL.build (#128650) * [SharedUX] Fix url path * Add url-loader as bazel dependency --- packages/kbn-shared-ux-components/BUILD.bazel | 2 ++ .../src/solution_avatar/solution_avatar.scss | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kbn-shared-ux-components/BUILD.bazel b/packages/kbn-shared-ux-components/BUILD.bazel index 16dfe9e0620e1..d2ce1a7d2e0f6 100644 --- a/packages/kbn-shared-ux-components/BUILD.bazel +++ b/packages/kbn-shared-ux-components/BUILD.bazel @@ -11,6 +11,7 @@ SOURCE_FILES = glob( "src/**/*.tsx", "src/**/*.scss", "src/**/*.mdx", + "src/**/*.svg", ], exclude = [ "**/*.test.*", @@ -50,6 +51,7 @@ RUNTIME_DEPS = [ "@npm//classnames", "@npm//react-use", "@npm//react", + "@npm//url-loader", ] # In this array place dependencies necessary to build the types, which will include the diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss index 3064ef0a04a67..1402f272a15cf 100644 --- a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss +++ b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss @@ -7,7 +7,7 @@ line-height: 100px; border-radius: 100px; display: inline-block; - background: $euiColorEmptyShade url('/assets/texture.svg') no-repeat; + background: $euiColorEmptyShade url('assets/texture.svg') no-repeat; background-size: cover, 125%; text-align: center; } From 02a146f7e4fd069c51d964da37460d63d980e829 Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Tue, 29 Mar 2022 22:46:39 +0200 Subject: [PATCH 031/108] Add a tour showing new rules search capabilities (#128759) --- .../security_solution/common/constants.ts | 2 +- .../detection_engine/rules/api.test.ts | 6 +- .../detection_engine/rules/utils.test.ts | 6 +- .../detection_engine/rules/utils.ts | 2 + .../rules_feature_tour_context.tsx | 74 +++++----- .../rules/all/feature_tour/translations.ts | 15 ++ .../detection_engine/rules/all/index.test.tsx | 7 +- .../rules_table_filters.test.tsx | 9 +- .../rules_table_filters.tsx | 32 +++-- .../pages/detection_engine/rules/index.tsx | 135 +++++++++--------- .../detection_engine/rules/translations.ts | 3 +- 11 files changed, 164 insertions(+), 127 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 591c7d68e17cb..f7bdc889f9c33 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -443,7 +443,7 @@ export const RULES_TABLE_PAGE_SIZE_OPTIONS = [5, 10, 20, 50, RULES_TABLE_MAX_PAG * we will need to update this constant with the corresponding version. */ export const RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY = - 'securitySolution.rulesManagementPage.newFeaturesTour.v8.1'; + 'securitySolution.rulesManagementPage.newFeaturesTour.v8.2'; export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY = 'securitySolution.ruleDetails.ruleExecutionLog.showMetrics.v8.2'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index c8d8b5bb6ffd0..3c534ca7294a5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -143,7 +143,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - '(alert.attributes.name: "hello world" OR alert.attributes.params.index: "hello world" OR alert.attributes.params.threat.tactic.id: "hello world" OR alert.attributes.params.threat.tactic.name: "hello world" OR alert.attributes.params.threat.technique.id: "hello world" OR alert.attributes.params.threat.technique.name: "hello world")', + '(alert.attributes.name: "hello world" OR alert.attributes.params.index: "hello world" OR alert.attributes.params.threat.tactic.id: "hello world" OR alert.attributes.params.threat.tactic.name: "hello world" OR alert.attributes.params.threat.technique.id: "hello world" OR alert.attributes.params.threat.technique.name: "hello world" OR alert.attributes.params.threat.technique.subtechnique.id: "hello world" OR alert.attributes.params.threat.technique.subtechnique.name: "hello world")', page: 1, per_page: 20, sort_field: 'enabled', @@ -172,7 +172,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - '(alert.attributes.name: "\\" OR (foo:bar)" OR alert.attributes.params.index: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo:bar)")', + '(alert.attributes.name: "\\" OR (foo:bar)" OR alert.attributes.params.index: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.subtechnique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.subtechnique.name: "\\" OR (foo:bar)")', page: 1, per_page: 20, sort_field: 'enabled', @@ -383,7 +383,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - 'alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags:("hello" AND "world") AND (alert.attributes.name: "ruleName" OR alert.attributes.params.index: "ruleName" OR alert.attributes.params.threat.tactic.id: "ruleName" OR alert.attributes.params.threat.tactic.name: "ruleName" OR alert.attributes.params.threat.technique.id: "ruleName" OR alert.attributes.params.threat.technique.name: "ruleName")', + 'alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags:("hello" AND "world") AND (alert.attributes.name: "ruleName" OR alert.attributes.params.index: "ruleName" OR alert.attributes.params.threat.tactic.id: "ruleName" OR alert.attributes.params.threat.tactic.name: "ruleName" OR alert.attributes.params.threat.technique.id: "ruleName" OR alert.attributes.params.threat.technique.name: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.id: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.name: "ruleName")', page: 1, per_page: 20, sort_field: 'enabled', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts index e3d2300972a51..a26a4aec3ec02 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts @@ -27,7 +27,7 @@ describe('convertRulesFilterToKQL', () => { const kql = convertRulesFilterToKQL({ ...filterOptions, filter: 'foo' }); expect(kql).toBe( - '(alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo")' + '(alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")' ); }); @@ -35,7 +35,7 @@ describe('convertRulesFilterToKQL', () => { const kql = convertRulesFilterToKQL({ ...filterOptions, filter: '" OR (foo: bar)' }); expect(kql).toBe( - '(alert.attributes.name: "\\" OR (foo: bar)" OR alert.attributes.params.index: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo: bar)")' + '(alert.attributes.name: "\\" OR (foo: bar)" OR alert.attributes.params.index: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.subtechnique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.subtechnique.name: "\\" OR (foo: bar)")' ); }); @@ -66,7 +66,7 @@ describe('convertRulesFilterToKQL', () => { }); expect(kql).toBe( - `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags:(\"tag1\" AND \"tag2\") AND (alert.attributes.name: \"foo\" OR alert.attributes.params.index: \"foo\" OR alert.attributes.params.threat.tactic.id: \"foo\" OR alert.attributes.params.threat.tactic.name: \"foo\" OR alert.attributes.params.threat.technique.id: \"foo\" OR alert.attributes.params.threat.technique.name: \"foo\")` + `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags:("tag1" AND "tag2") AND (alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")` ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts index f5e52fd6362c1..069746223731c 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts @@ -16,6 +16,8 @@ const SEARCHABLE_RULE_PARAMS = [ 'alert.attributes.params.threat.tactic.name', 'alert.attributes.params.threat.technique.id', 'alert.attributes.params.threat.technique.name', + 'alert.attributes.params.threat.technique.subtechnique.id', + 'alert.attributes.params.threat.technique.subtechnique.name', ]; /** diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/rules_feature_tour_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/rules_feature_tour_context.tsx index aaa483e49fca7..b0f0b0f17923c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/rules_feature_tour_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/rules_feature_tour_context.tsx @@ -43,23 +43,13 @@ const tourConfig: EuiTourState = { const stepsConfig: EuiStatelessTourStep[] = [ { step: 1, - title: 'A new feature', - content:

{'This feature allows for...'}

, - stepsTotal: 2, + title: i18n.SEARCH_CAPABILITIES_TITLE, + content:

{i18n.SEARCH_CAPABILITIES_DESCRIPTION}

, + stepsTotal: 1, children: <>, onFinish: noop, maxWidth: TOUR_POPOVER_WIDTH, }, - { - step: 2, - title: 'Another feature', - content:

{'This another feature allows for...'}

, - stepsTotal: 2, - children: <>, - onFinish: noop, - anchorPosition: 'rightUp', - maxWidth: TOUR_POPOVER_WIDTH, - }, ]; const RulesFeatureTourContext = createContext(null); @@ -82,39 +72,43 @@ export const RulesFeatureTourContextProvider: FC = ({ children }) => { const [tourSteps, tourActions, tourState] = useEuiTour(stepsConfig, restoredState); - const enhancedSteps = useMemo(() => { - return tourSteps.map((item, index, array) => { - return { + const enhancedSteps = useMemo( + () => + tourSteps.map((item, index) => ({ ...item, content: ( <> {item.content} - - - - - - - - - + {tourSteps.length > 1 && ( + <> + + + + + + + + + + + )} ), - }; - }); - }, [tourSteps, tourActions]); + })), + [tourSteps, tourActions] + ); const providerValue = useMemo( () => ({ steps: enhancedSteps, actions: tourActions }), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/translations.ts index bfcda64bb13dd..88b5489b01eaa 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/translations.ts @@ -13,3 +13,18 @@ export const TOUR_TITLE = i18n.translate( defaultMessage: "What's new", } ); + +export const SEARCH_CAPABILITIES_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle', + { + defaultMessage: 'Enhanced search capabilities', + } +); + +export const SEARCH_CAPABILITIES_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription', + { + defaultMessage: + 'It is now possible to search rules by index patterns, like "filebeat-*", or by MITRE ATT&CK™ tactics or techniques, like "Defense Evasion" or "TA0005".', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 3b24dda539174..8e44475a7992e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -12,6 +12,7 @@ import { useKibana } from '../../../../../common/lib/kibana'; import { TestProviders } from '../../../../../common/mock'; import '../../../../../common/mock/formatted_relative'; import '../../../../../common/mock/match_media'; +import { RulesFeatureTourContextProvider } from './feature_tour/rules_feature_tour_context'; import { AllRules } from './index'; jest.mock('../../../../../common/components/link_to'); @@ -67,7 +68,8 @@ describe('AllRules', () => { rulesNotInstalled={0} rulesNotUpdated={0} /> - + , + { wrappingComponent: RulesFeatureTourContextProvider } ); await waitFor(() => { @@ -90,7 +92,8 @@ describe('AllRules', () => { rulesNotInstalled={0} rulesNotUpdated={0} /> - + , + { wrappingComponent: RulesFeatureTourContextProvider } ); await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx index 816ffdfa9dad6..88b8e952d215a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx @@ -10,13 +10,16 @@ import { mount } from 'enzyme'; import { RulesTableFilters } from './rules_table_filters'; import { TestProviders } from '../../../../../../common/mock'; +import { RulesFeatureTourContextProvider } from '../feature_tour/rules_feature_tour_context'; jest.mock('../rules_table/rules_table_context'); describe('RulesTableFilters', () => { it('renders no numbers next to rule type button filter if none exist', async () => { const wrapper = mount( - , + + + , { wrappingComponent: TestProviders } ); @@ -30,7 +33,9 @@ describe('RulesTableFilters', () => { it('renders number of custom and prepackaged rules', async () => { const wrapper = mount( - , + + + , { wrappingComponent: TestProviders } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index b4c81ae5a177d..7e3263f6bb26a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -11,11 +11,13 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, + EuiTourStep, } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; import React, { useCallback } from 'react'; import styled from 'styled-components'; import * as i18n from '../../translations'; +import { useRulesFeatureTourContext } from '../feature_tour/rules_feature_tour_context'; import { useRulesTableContext } from '../rules_table/rules_table_context'; import { TagsFilterPopover } from './tags_filter_popover'; @@ -23,6 +25,14 @@ const FilterWrapper = styled(EuiFlexGroup)` margin-bottom: ${({ theme }) => theme.eui.euiSizeXS}; `; +const SearchBarWrapper = styled(EuiFlexItem)` + & .euiPopover__anchor { + // This is needed to "cancel" styles passed down from EuiTourStep that + // interfere with EuiFieldSearch and don't allow it to take the full width + display: block; + } +`; + interface RulesTableFiltersProps { rulesCustomInstalled: number | null; rulesInstalled: number | null; @@ -45,6 +55,8 @@ const RulesTableFiltersComponent = ({ const { showCustomRules, showElasticRules, tags: selectedTags } = filterOptions; + const { steps } = useRulesFeatureTourContext(); + const handleOnSearch = useCallback( (filterString) => setFilterOptions({ filter: filterString.trim() }), [setFilterOptions] @@ -69,15 +81,17 @@ const RulesTableFiltersComponent = ({ return ( - - - + + + + + { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); @@ -177,77 +178,79 @@ const RulesPageComponent: React.FC = () => { showCheckBox /> - - - - - {loadPrebuiltRulesAndTemplatesButton && ( - {loadPrebuiltRulesAndTemplatesButton} - )} - {reloadPrebuiltRulesAndTemplatesButton && ( - {reloadPrebuiltRulesAndTemplatesButton} - )} - - + + + + + + {loadPrebuiltRulesAndTemplatesButton && ( + {loadPrebuiltRulesAndTemplatesButton} + )} + {reloadPrebuiltRulesAndTemplatesButton && ( + {reloadPrebuiltRulesAndTemplatesButton} + )} + + + + {i18n.UPLOAD_VALUE_LISTS} + + + + - {i18n.UPLOAD_VALUE_LISTS} + {i18n.IMPORT_RULE} - - - - - {i18n.IMPORT_RULE} - - - - - {i18n.ADD_NEW_RULE} - - - - - {(prePackagedRuleStatus === 'ruleNeedUpdate' || - prePackagedTimelineStatus === 'timelineNeedUpdate') && ( - + + + {i18n.ADD_NEW_RULE} + + + + + {(prePackagedRuleStatus === 'ruleNeedUpdate' || + prePackagedTimelineStatus === 'timelineNeedUpdate') && ( + + )} + - )} - - - + + + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index f99ebc2c72c26..09949cc5c1a09 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -408,7 +408,8 @@ export const SEARCH_RULES = i18n.translate( export const SEARCH_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.searchPlaceholder', { - defaultMessage: 'Search by rule name, index pattern, or MITRE ATT&CK tactic or technique', + defaultMessage: + 'Rule name, index pattern (e.g., "filebeat-*"), or MITRE ATT&CK™ tactic or technique (e.g., "Defense Evasion" or "TA0005")', } ); From fefb24b7170358b9772d1009926cf4ab3b05a8b3 Mon Sep 17 00:00:00 2001 From: Andrew Tate Date: Tue, 29 Mar 2022 16:05:57 -0500 Subject: [PATCH 032/108] [Lens] Disable auto apply design updates (#128412) --- .../lens/public/app_plugin/app.test.tsx | 3 +- x-pack/plugins/lens/public/app_plugin/app.tsx | 2 + .../lens/public/app_plugin/lens_top_nav.tsx | 25 +++ .../lens/public/app_plugin/mounter.tsx | 1 + .../lens/public/app_plugin/settings_menu.tsx | 108 +++++++++++ .../plugins/lens/public/app_plugin/types.ts | 5 + .../lens/public/assets/render_dark@2x.png | Bin 0 -> 10732 bytes .../lens/public/assets/render_light@2x.png | Bin 0 -> 10795 bytes .../editor_frame/suggestion_panel.scss | 8 + .../editor_frame/suggestion_panel.test.tsx | 2 +- .../editor_frame/suggestion_panel.tsx | 63 +++---- .../geo_field_workspace_panel.tsx | 2 +- .../workspace_panel/workspace_panel.test.tsx | 103 ++++++++--- .../workspace_panel/workspace_panel.tsx | 138 ++++++++++---- .../workspace_panel_wrapper.scss | 26 ++- .../workspace_panel_wrapper.test.tsx | 38 +--- .../workspace_panel_wrapper.tsx | 175 +++++++----------- .../apps/lens/disable_auto_apply.ts | 18 +- .../test/functional/apps/lens/smokescreen.ts | 2 +- .../test/functional/page_objects/lens_page.ts | 56 ++++-- 20 files changed, 510 insertions(+), 265 deletions(-) create mode 100644 x-pack/plugins/lens/public/app_plugin/settings_menu.tsx create mode 100644 x-pack/plugins/lens/public/assets/render_dark@2x.png create mode 100644 x-pack/plugins/lens/public/assets/render_light@2x.png diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index e7cdcfe4707cb..f5da098e028f9 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { Subject } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { App } from './app'; @@ -83,6 +83,7 @@ describe('Lens App', () => { datasourceMap, visualizationMap, topNavMenuEntryGenerators: [], + theme$: new Observable(), }; } diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 6312225af579b..19b213815145a 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -58,6 +58,7 @@ export function App({ contextOriginatingApp, topNavMenuEntryGenerators, initialContext, + theme$, }: LensAppProps) { const lensAppServices = useKibana().services; @@ -402,6 +403,7 @@ export function App({ initialContextIsEmbedded={initialContextIsEmbedded} topNavMenuEntryGenerators={topNavMenuEntryGenerators} initialContext={initialContext} + theme$={theme$} /> {getLegacyUrlConflictCallout()} {(!isLoading || persistedDoc) && ( diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index e30ea0d44aef3..b48b16074c55e 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -8,6 +8,7 @@ import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useStore } from 'react-redux'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { LensAppServices, @@ -21,6 +22,7 @@ import { tableHasFormulas } from '../../../../../src/plugins/data/common'; import { exporters } from '../../../../../src/plugins/data/public'; import type { DataView } from '../../../../../src/plugins/data_views/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { toggleSettingsMenuOpen } from './settings_menu'; import { setState, useLensSelector, @@ -140,6 +142,17 @@ function getLensTopNavConfig(options: { tooltip: tooltips.showExportWarning, }); + topNavMenu.push({ + label: i18n.translate('xpack.lens.app.settings', { + defaultMessage: 'Settings', + }), + run: actions.openSettings, + testId: 'lnsApp_settingsButton', + description: i18n.translate('xpack.lens.app.settingsAriaLabel', { + defaultMessage: 'Open the Lens settings menu', + }), + }); + if (showCancel) { topNavMenu.push({ label: i18n.translate('xpack.lens.app.cancel', { @@ -200,6 +213,7 @@ export const LensTopNavMenu = ({ initialContextIsEmbedded, topNavMenuEntryGenerators, initialContext, + theme$, }: LensTopNavMenuProps) => { const { data, @@ -233,6 +247,7 @@ export const LensTopNavMenu = ({ visualization, filters, } = useLensSelector((state) => state.lens); + const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false); useEffect(() => { @@ -337,6 +352,8 @@ export const LensTopNavMenu = ({ application.capabilities, ]); + const lensStore = useStore(); + const topNavConfig = useMemo(() => { const baseMenuEntries = getLensTopNavConfig({ showSaveAndReturn: @@ -465,6 +482,12 @@ export const LensTopNavMenu = ({ columns: meta.columns, }); }, + openSettings: (anchorElement: HTMLElement) => + toggleSettingsMenuOpen({ + lensStore, + anchorElement, + theme$, + }), }, }); return [...(additionalMenuEntries || []), ...baseMenuEntries]; @@ -497,6 +520,8 @@ export const LensTopNavMenu = ({ filters, indexPatterns, data.query.timefilter.timefilter, + lensStore, + theme$, ]); const onQuerySubmitWrapped = useCallback( diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 10337c26f14ef..e1419c8fe96f2 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -249,6 +249,7 @@ export async function mountApp( initialContext={initialContext} contextOriginatingApp={historyLocationState?.originatingApp} topNavMenuEntryGenerators={topNavMenuEntryGenerators} + theme$={core.theme.theme$} /> ); diff --git a/x-pack/plugins/lens/public/app_plugin/settings_menu.tsx b/x-pack/plugins/lens/public/app_plugin/settings_menu.tsx new file mode 100644 index 0000000000000..37f4feb5f5520 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/settings_menu.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, { useCallback } from 'react'; +import ReactDOM from 'react-dom'; +import type { CoreTheme } from 'kibana/public'; +import { EuiPopoverTitle, EuiSwitch, EuiWrappingPopover } from '@elastic/eui'; +import { Observable } from 'rxjs'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { Store } from 'redux'; +import { Provider } from 'react-redux'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + disableAutoApply, + enableAutoApply, + LensAppState, + selectAutoApplyEnabled, + useLensDispatch, + useLensSelector, +} from '../state_management'; +import { trackUiEvent } from '../lens_ui_telemetry'; +import { writeToStorage } from '../settings_storage'; +import { AUTO_APPLY_DISABLED_STORAGE_KEY } from '../editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper'; + +const container = document.createElement('div'); +let isOpen = false; + +function SettingsMenu({ + anchorElement, + onClose, +}: { + anchorElement: HTMLElement; + onClose: () => void; +}) { + const autoApplyEnabled = useLensSelector(selectAutoApplyEnabled); + + const dispatch = useLensDispatch(); + + const toggleAutoApply = useCallback(() => { + trackUiEvent('toggle_autoapply'); + + writeToStorage( + new Storage(localStorage), + AUTO_APPLY_DISABLED_STORAGE_KEY, + String(autoApplyEnabled) + ); + dispatch(autoApplyEnabled ? disableAutoApply() : enableAutoApply()); + }, [dispatch, autoApplyEnabled]); + + return ( + + + + + toggleAutoApply()} + data-test-subj="lnsToggleAutoApply" + /> + + ); +} + +function closeSettingsMenu() { + ReactDOM.unmountComponentAtNode(container); + document.body.removeChild(container); + isOpen = false; +} + +export function toggleSettingsMenuOpen(props: { + lensStore: Store; + anchorElement: HTMLElement; + theme$: Observable; +}) { + if (isOpen) { + closeSettingsMenu(); + return; + } + + isOpen = true; + document.body.appendChild(container); + + const element = ( + + + + + + + + ); + ReactDOM.render(element, container); +} diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index ff6092c42eaa5..1812e7dd8cdb0 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -8,11 +8,13 @@ import type { History } from 'history'; import type { OnSaveProps } from 'src/plugins/saved_objects/public'; import { DiscoverStart } from 'src/plugins/discover/public'; +import { Observable } from 'rxjs'; import { SpacesApi } from '../../../spaces/public'; import type { ApplicationStart, AppMountParameters, ChromeStart, + CoreTheme, ExecutionContextStart, HttpStart, IUiSettingsClient, @@ -73,6 +75,7 @@ export interface LensAppProps { initialContext?: VisualizeEditorContext | VisualizeFieldContext; contextOriginatingApp?: string; topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[]; + theme$: Observable; } export type RunSave = ( @@ -107,6 +110,7 @@ export interface LensTopNavMenuProps { initialContextIsEmbedded?: boolean; topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[]; initialContext?: VisualizeFieldContext | VisualizeEditorContext; + theme$: Observable; } export interface HistoryLocationState { @@ -157,4 +161,5 @@ export interface LensTopNavActions { cancel: () => void; exportToCSV: () => void; getUnderlyingDataUrl: () => string | undefined; + openSettings: (anchorElement: HTMLElement) => void; } diff --git a/x-pack/plugins/lens/public/assets/render_dark@2x.png b/x-pack/plugins/lens/public/assets/render_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..553fc827c9429eca7a0bc7ffb9ca37af094cd272 GIT binary patch literal 10732 zcmX9^1yCH%)4k(x*TX%*EqKrnEO>B-g9LY%AQ#*5cL}cH4hX?Dxc&Kk z-`4Kz^qbfHx~pbtrfN6(y}BYEHWfAi0KijvC#MAf03rW9n4o8*zDqj@06^h+uc{;e zY$}>0%A3ZhsF-s^4@(xRBny0`M9E zcnq)ybfl4_P+-E$T%+JUm1Wo#b>QFh5j1gC*5{&n0PI@`)BgZm;hX;+v13+9bN5egAK9xf0q%WxNr-{n2OZc| z`T8Bet~q}8mb~~xq~TJu@e&mH3t;~6Qo8EA3*r5G9A{aFo zJaN;F_!l*L=2^8^R9@EpJY(!VwbZo%l%ybG3<6wiqJdKv)`1Pw0b#A6jkxLZ?DcF5 zs9aYIJ)!U$NK#>3tc$ajhH&e9*3*G(PKI^YjsEgzv(uRt7Z=v!@7Z#7H9;+&dNLgS zHQx)eXZpL!-km;Q{Vg@+*~xKnLFV#&{%1dh0RUEzlAMf=@A5(RsPhURp}dgK8+~P9l_P79ncJ%E7HEsFp-L{(<7AQw-Oz>T+c~@)G#IP!k75 zM5O(grp!ALp8js4T!tXh&TK8{v19#^Zrw58ISP`%0Suv#QJqb(*-7C=ae6f2#kZ+9 zS(AJ$l$xigWcG>sTYsY@c(Ut?HOiX(mNRjC!%!R?X5i2&Y0_u;K;HXxbZO&+%Of1r zl*#w#*BZAVn9wq>jUpmHa4v8#?YQI~T!gS*BsNl+E+Hvl@ z#^`qX!7@$eCT1x0lXX@I^SADKWD~2ysTbyM&Vzi2KGhTVDh1d{@(lbEm&Q@Z1$E1S z5EX-&#(pXCPk)H~x9Sp?A5ra$ZPSY+S(N%@#6S22d$MI94R`L~z6bRPkNjg{zLg9evg>KfAQAIp1B29u)hSYIE6To-ttTr*jO(W^v5NOA^tL zbLLsoK5!W4@g!r8sAH}AUt{svuTxv)-!TMZG&Nju!V~aE=+BA_gw+>rycCNO?W2isR zjCW!h@XdJwXTANz3z?ila!bu)%pfBbbbqo|)7$UVFYIQ+#Xdhnu^jdh z5knF#LnoZz#g#KV= z9N1bH&t7#BM_-`rL>~8+TnH^W3#SDuO3Ha0dZO+Jj^p9tPtdMB!47Mw#AUm^x1?AN zDeq6`hJKdjyH=-G9iWmt`4{()X%J>$s~6L_D(SzN3s%J&kDw~=CEyNC`@u@^upuIB zJcQbHE4$Y8PXP~PA(3uwq>H+Y)<=yU6)ssz#;f}ltsE_kkiiY0&ftbXm_z$M7bHxx z-0;`ssi6)>9KXLvUK1@C6j&cyG^PkbS#{C$BcJ&)P+wuMo^KKB+G~qTMkIu(PBy{g zRA+fJN7c=DkNR$SS!ds#DlKIMc|`djQr~J)$nXkv;gOFilXo)ybu(pm`KMW~6h|+C zPaLr(+wV{_ZEHUSQeEzp@{{1hoO$uLM@x*e5$FAadu)TvF+QcpD)ErUJP46?8YT4~ z^U(S>^qX7=@}o0EoZD0c!T;#by}jG?YZ3ak^Icgq`71nC);%!SnCwbzDvh_$q&XSm z(dKatGq!3va}keZ>3dG_)D73K#uWVc?9kX-!5Lqg-nN0^ophFK4>ok#f#UZLu?!R? zT)#2qrC;f*GGTLx$QMe%yD?ojdJCXU*9U`#)DA_e2MuBVZc-;F+f_iqmFK-2ic z=}A?KhDs#oY<+KtQaP3%cnd6#VaMH#rNU*^w>ntsxRk=QB)KNHRf{x%U+Rz-5%@*G z2mz5%ruIq0C{bgBBZ{XiLfj@IBt$Tu9MZms%3=SuDx+(!@PN#h+8>~C3A>1WCj3

8fJe$5qpp47?2=+^l0SB@wfZHQJ460pyNV@sE^A#sKuUETmSeSe43Om=Cma>d^f z`-VAsW`7VCH}BDynX}I#rC}vT&CmOyL}ubY2s=C)>WTnKihRtjqid4~TI&vRiH3LV ze9PFDx$a0TIhBpj#7)DCpsEK8sz|A3R?kDXE3zu$ z2p`0zFJ@1SqveJ5X)%L={wob|u6Xmoa-Cn06A_3^V10>9LV>zU2X!f+z2oT!c6HZ2 z_bfzzYobt7v?q#7&8Y588Zr3h35w*vP+%4 z#HU*ry!!R+q?A3Q|^A2NGBx7vzzQ^Eq;%2-jq^GFhlLJ%i|?CiOLSHmqE zJByT6Nik3hg-qqhTMZP#^yBu*3fNc)wON!wBi-{kF5Ru>-H^f80Py#dS^a zd3dQCtDOvF@o|GC+TIAPn!2p1Wq1uyVXJ2PpWR(SGvIuVA8fedlLS{LvKc$sV-#De zh=j^VoP;lW-q~{1=l^p^>25R zys0Vr5Q~+C0KcQ>Me;%J0f7O(NB?ACbC%_mz2Q}b_Zl;kmm^gYq8aYcPw4CmGZs{V zlxwZbMkA7GQO87R*K%LAk#vvPpSp~sbp<_6oK`kKYqQ*Ca#D*cRpWOVATT>O>vk@* zHA7e4g_OIvWSz8nEZ;JoZt0DF%bqx1# z_L^VH5kDeA3}Sg*_WAkNV3uy%sZ`GW6v=}fW}Jj|T!#UlW$pFeWhxS)MAsP9uvO!v0=$!h+q^9O%KW?3qXn!eDAUjT%J>mV; zn_*=>4^I8XyV|^qi{UVEy%qP9#~3Z3%VR>P6e6 zFv@WqY(83hwJkh`XeY|C#V@3wfnWz@+2=3nnVxAOK0eTM(GMwhRX|P|! zPUpf~*~BqB@ziVZ!Zg5wrl_HoRg(GOFb5?+UnAJk!SVh3G$i_fkV_}@7)McQX#*&( z((t)R;PKj3Q(uH)_5!ew+oDH>XsZW$D)47%p^j0dt_V2mcrpq6HX}MbZmPD(wcZsu zS$GK82F{dNkF8V|%K!5^Ls_~MBe6c)nioecBjZtjp(O2;x1uOFQOei^|VnI}X8@T>Wmq-rI zu-%%_5Rg`-CR95Mt1bUXE5a#87-Go*k+Ek@k|+0JZpxtgp4XZd}kf((bJl-0Qd*}9m`~C?7 zlX;Ugs7Gh-&#=(>hkxJOJIKdE{C{+EMF%4PaCHLFnT$Tjfu}yyHV`tcq8UiN?(aU# z+sGG?gl;a0PLA^DB_&Pa2nCQZwDlJ&yCd{1TEb-2-1ruW8gjsJJCuP@Becm=oLnrI za++YOlo(z>#0S$@neLa75FB*z=Q!L@{gko*;+C@jBk=eja9|HT!R({oYhW9|;F8BU z!$i&(V$Rr31ki>dz7QI**kujx#C_>ecOVj$SHfC!^3 zPA=+rqNfi~tC~$`dd6nvOI=hGCZu+M`197*eX{{YUJ%9wlp>hIFpv_lU^o`+MI2{B zl4T|pR6res8;Xfy;p@$hVh~^TnheQ?do`@R2HV# zw0z9DF-r|9)5f-_CTvclKzZ9(hKe`#wxqR__Lo<cYowC!$JE*w~>E9h9~0T)c^OJ#f7{d;@5Rw}57Xrxf6a4H1599{zpXGF&mM?p?5T5F zsn0cc>t>|AU5%GF7efe6+PK}((!X}fwsI#tZQOsKlF}{5q*$;hpsDg$|H>&mOW;4xILLIJWKp{dOajKW~K=Dt^ zNo?za&u<4;)SOLO(W!%64ZiL7LZTI&gobSwckuz=;5Ya29p3Rk69=^a4L~>{R^cgb zUp%Dl6cwXw9nTeQ-hl$!rE(y}7*KEFW6&hN^&Du(a zV-|3~vfB959`AkuP)AuCdCiuOzAb2na=>$#(+n^e4Yw-5U+nnIIGM#7&{d~u95g^l zvKkv@2b3WA$B9(H0-5)9)A`rgF>g1C0cucLzFfW$RkR^e!*4uMk#*vRj>5yRopVoY z5`gH#v}17~aGTJsa9QSHs^PEd1MHu{x#7l?{(}ADhugp78tKuL; znl}c;g>YAU@&D??;ExvF#J=x1ZtK&c+4()gszHx843*&#B{79~mqT}rrv!S^&Dpvd zX)8X7e~H4S=nG&$wT`CIh5MSmY0aau#}AC$ld=WGjWX-lDc>=@?Sp5AGs5y z11z9!PfGF1CqQgJ-%mzAO9e-8TDtmB%`YCUbwS3}a3P3HJn9U7L>!hp^L=X$ zr~neIEI;3+eBoQ$A`Zkb*`-H~`(kI{-gdWhj4HtgqDTzWRCN^0zt4j@$wE(6Q?)Y!X&CY?(UC4_u-ulPHt3~8={!$m*ET?L0HmJ=GIw( zW@iIX^w}@BsM!X!QSw}L*fGWClcUxipX?e_HyoR$q)k%4wD&xSjZKUlO{fz9eG^n+ zi3R9}1psEJvZRogZI*u=y-?!X)?4w(F4$#tFR=KI=q#~_34TR{X+V?H64{Uo;^g@{ zl_Ze9pk$B4x-#u!b?BAtZ^>X$M}-i(H0)ZWA{Zm`7Bro(WkJtdZDonGU6}Yn7k`)H zcl=gI_Vo!eR{z{hJW-kzy~-r$1vGFt+3bSo2I3Fj&3UbZh5njswTA4H;m-)WZf1H< zSOylrVQ2*h`jxwtHJ|@hX0k;&(-spjWCvtU&3{7bEnB^XQ$pVl3RMOatU?nj!O1~Z zlex>#avFdMwsdt*Btfnv6v?X|Ge(;+rEv(0%}iQMqcjG2&Q!EfJ&7KeEkXGDy6hU zLI|6lv}W$tnOY+1B2g^^qlS97iu~|Jz2!dWIy2s=EpSjyHWYKA@JI>p9;Cn@-w1Um z?e}GZYp%9OL5+bFsaAjpI7Z1gI~vpz>|M+Z8a{1efC>3ydWWCNFL@$?Qbt94c-33{ z!!_#K<#uqgO*T6ctYl9_Qfi~&4)4;){L2hH@218V#P?;JK_gyv*MiRk<6W;MJT9;y zBCO59Zw}CN$(%YfaDLwju*sUft=Kk%6#)atUULb(M!48&HA6dGqpe#1sOSa@6QA$AGIyP<;J+7gsgxF0-Hqu(m4>?+UO!lEwNNOfw zoM_-6AJc0DZ^_CC3{89sm{;3 zPZ4%K3xO}*{gP6~F;(S?Nr5EwCK%+uK&80=x^>%Tf_n`c=pmpy2_$X^jRmP2nZr~r z-3gTlgr*jZ0*LXjCB|44K^pAUl|f@ijMTMVyAqlxGzffk@s!i9F43xhg-wW61hnLU z1j~$r%4;YahYG%vh1dl)_zQ^MIBCFR$liZ9Nj&dfK5=4Buf|a%)gQ0)c!S~_(Fj*Z zH}C*k!Gg_%{hwgfpYB>Cp#m8FJ)!R(bt?&fU`Yw9a{7DZuby*H2G?`J4gv4sq^vXQ zLqMD9Z<-xf8p=e+J|Hl;Y|2RuP|tcG($3NW`!z58^wOir9EB}!JbRrH824|uKntX2 zfjNwDPpF(VfV;hOmpV`R^(g_3MyimPr@P6}g3Szj;+>Y&!J?NvoCd`(2d~c6Vi3er zq(rWo3r)laVc>RoY=YsQ7guPSncirUkX0lJg;cAukFXRrN6kXrj#bG$~&g& zftKsHr*1?Ne(wq8`!tGtr-H6T*~Y90DOYPTq@kZy8w#eff>bX=9Tq;?fKmSjFRIon z*_^4AwN%I7P$>4Cp_y|BBA3^WyTZuDD&ns3VJjIGvjv!5BNa)qZNjmRyW+p zTRF`o*hG8O;hBi*YVlRfwbK(1&VUs;M#Nu^j&Ja``*Zy&*;EJrrmZXdSi1U0?0>}C z<^lqilQ-s?a0V(Jr|ATX<-hh?Dx4LZR5}44O8|Z1fFPI@LamhM6$J!HkJTs)u{Zgq zVoto=*w!(g;7~#6wBTBIvzr2i}Q_r`L zK@o;HRVDeSh9?^YMlxlsrdPu~;rl+@jW1G-d%(8sm!A`rR=ed4jC z_mT1T%aZsL;}T^7Bk+fJ*-3}Cn^-`@;A(gLzpUXG1|8bmmTp&LOG}O(M)n?;h=X$h zA6}De{U=dLJCFj0eJ4QM!IZ%&r~(CG>@(C=Mf5c-Wtz&VUS7?dHLd&(R6zq7Y@2 zSLJ%%(KGoNoKLpKXFJ-}xh@+Pd^>$IT$Ir-c)u6)ma~E}?o)?ju$;U~DC@+c{|opvuLl=3UTYYv8!XCGPYI=e9HUNHhgzoU=xY(O1WjD;vXuC%X8Nn|$1 zgYj!E%OO&p6L>cAZ#Yq1ZLVBTi>2WeM-RZ*FJ<`nY*vp^2q@!nTFO^rubys5QzTPA zozZnopM13N1=7eW8@eb3G|zs_GWCTg)?G$DLM?hpquh_ODn%hmZ?JP)n2u0T$BGg` z+hMI(DNE#>f2TM%Sgf!<$0T^Kk$9)J$SI@QGcYG?#KIOTKdeIXvxq@iW4 zV$@@`a=jg@y`x1VBy3B6vsVM=`^0|+VFJt*!3np3N6W_ZHQT|}Q;d@+&5ubn?JH`B zLBN}$F_{;cJQ_W@(SG{A=I2o}6PK6Yt{Dc48>ns1_lne^i9VqFryYY9#MM`|HRrZ= zedgFvW&{}G>78?}bO^@3?!%-2uJV}k(EIsI;U@?c#z8+oExbXd)tutwMMjjbkVLMe zCzfYd%eST0bOO&7h>yQr4dp{89jAUsLL5{UemXs|odaE!_r2GwS_(wHCx;)79N*v} z&|ifq{jp?`K*0wLa&&zoC|D&xZ2qT>86-2K@@Sdn1Yg+9t9nKdm&*^1Ab-6D)BtzX zdsale;izO|O|w5G2>03D8O5w|wHGD38psEhhhil{siBjHf9N`Zp&=AU9L)s~R9xua zPd_pEAXCI@g77d!xDevT>=%gQ&d zGGsQx*;+YV%xKxxcTtvJx5o*#GOJKyRI};^n={7KW{=BE;n|6Cq&e6a2(^DNoko_m z<>UTkUf%5w#dCCd;J0;s8dSZ$z)13fHL}?*c2;mBUN|~4qxqPH{N&79y;ED8tCS#; z)3hu@WsdqD9)it*i4_uWSvxhYKHF0u(mJ-5wMN;j+>n+e5$^3>9PaY%?$c@>p7`cb z#IPNVUTckJBKMQc_LwEr;!Nb9A4otcbwUZICpBxY2TCb2pERoxMivYD5oxdx>Ag54 z_;h3aim2_de|!|*WlzFU$X(`#$7BQ@ZHh_yQgufXI!3fKH%m{6Rb_{M-n1H>;N5lb z6Q=TuLP7vN6W`Ccl!jh10YdcNfwJ8#q{e zW*@A?+TAYz`X8S@)ORpO_o=kL62E!#HYU1>BQz^qr~Dd2DeTxnJR4@aYJ%3I@Sf>3 zZfN#_BI8CX**+%Q+a5;Zu5XJ$NHV(Z-eZp&_}M{9=1}-8ar7#x&0W2($-eV4b-TTw zrk;782uoV=PbbvL4mw5i1MC}|FzNfv!ND9zpInmD%w?1;(-ui)Ti`@P@9%_fyj$JQ zukH!qs+O|g#$86f1wF>!w5sfkLyK@$rU4Tvb0Oe@z_) z=jIK#?}3z%f%|n5o|ckw=5Z{8lEoy|hQ`01dUPad=|T}v79}|9(v)9$ zCl2BSZ7uL4%~LQ)o8VrrD?i^&*rPWY(O<#f#AkdXh2f%Uz6x}Ncfom(G&-4QM zgZS%ZI|ZOw_|5u6^eo|iEVjmnU-w1C)%P_l0Lm4--$wtM6m5Wlt_?cAYMVq2Ry^49 z=<}C;UPHZ1i?`~UNE<;s(P%L=Flt9bJ!FbH(q#RV?Dw=>lJ4N=VJkUQ$2z+M39-<} z+gTAV-nydbLL)NWxpO0UES_-uN~c@lKDFyB)Xxmc{JpcFgb~mg=hRif?`zz>yOwV_ zZx|9na>^f?i+IP54QH=h!g=Fd2?cr)bUTp=H-U{qM6>r66-!h`afU>LF>)SsFCTA8 z2Jg`Y3UW(&YV7cB)oO_i&-tRkXU2&)cpqR#?~y9qn6*UMp9&X;62lfBn#WuS?_>oi zH!ke0CCX24J|&oU!GfRmgm3zVwvtXFI~$DcC@_m%+bn`3${GdxmCKdoWjcPTn0Kqt z_g!_Rz26gDLs(~2Wep$sh4l_*2{gPE!pZI#o#_9Qp%myEl393C?;*5?DM|3ko1!3` z$6G}Gq8rQ7NX+0$JdkFS+=@*mu9Eu5S#>kxqpB#qW;4e8=^HMVqu|s2-2n-@i4;@@ zGg!c&x=0~4fz6!#tE}3b*BMGdqttVTk(Kg3ZJNRD2$a+A?ge2S>lWG`!cBW3Si4)d zk8)JYe4cZI@_O_hYBl)CGe^xY6Gr0Vbl^!c+s+%FyVJb@n{a7G>xg0*^bt8(i?=Ws@#}1lvjIoQnE_ObvUnmlN1*>Ec|71Lu z*>86mXuO@$nGp9bv1ZNM!G#km`t(9Wq<{ZA$*W^I{(y%WHP_$E=%a6l|Lcy6QY-XQ zoZ?9wz5$;Uml8ik)l7}?;wRH7N*kl52^2L%M`(&V>oNYp<@dfSZHm}Y{J>@N(nk@+ zuVKHprEp(Ap5-oxZ&$iXRw|rJRV(!5z9Hwhmgh_UO@(mCDE95<)#ao+%bTo?Lx@sI zQS7<%*3Pq_p1ku4IR=n|_i>nT7H)zo^ b0f2bvie76Emw(Sc;Q^H7)#a++LPGurtp1JY literal 0 HcmV?d00001 diff --git a/x-pack/plugins/lens/public/assets/render_light@2x.png b/x-pack/plugins/lens/public/assets/render_light@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..497f50866a294d1bd14c6bf351d446784b8a5533 GIT binary patch literal 10795 zcmV+`D%919P)jC(%_e+ zy|B8^#?IYBRFz6$hr`R;zQ)@4Hf|b0t5vJ&(`7zbHfP> z6e(+}0(Qy;Z@vZy4bIfx0C&sL*yPI5-ve~W0t5vD0t3(3-=nX{$j;mrY`HFi!2x&6 z0d~s+X0Q!glN1^$9A2e^m9WFh;XbkO5|!eXyW0SD$^di51|BS8wdbM3;hVwWyTa3> zuEZK6J{zg^00000cg)Y$;s9o`0BE!Tb;$sB$^dP?0BX1ZaKHd?!T@Z%5SHZ^l-2-m zzW{W{0BgDf6&?zT+W>OI6rAe@DLomV<^XfW0CL3*kl+-5xEG@E0BN=jRC)ml4+DMC z5NVkdnB)pSTm&F42ZPoSklYcI;0#J@6^X|WUy1-qfC)cai^b^)f6NkesR%Vp42RSc zn&$xu5V+9f06S&?EKLAenE)O$01p}f4;cUl4HI;z0AZ#Vq3$$|&lZWs4_}D@HCg}` zCIC{534_rKN^1f|a|=pr#M0t)tk_PO(i(xwZr-O!0001MbW%=J02vem1q}!b5ymz) z;_+`M#>O^Mnhy_*e$o(rMB-t9zcnDJ5Vt__V5y*tQ&J+tuy1bSn>HuL-?+AZaN?7g+Z|Z04GOD zL_t(|+U%J-lhZI1g>@f#6wYMwav)@2hX4aquq_2YfMFY`sj2e+OU90@D>-sVsIa52 zN$mC7@$u2oy|OA43WY+UP$(1%g+ie)B8ZLQumJ+1-=_-y1RGr=LW2(bAi^m@qc*B= zC1kW~m1XD6syq&$Qyde4EdaG{W>Xw}iC^Dd9Tru~^TpxS`_&#AxR(&8?-V&0ASeyR z&3V}VUL4PvE!JOFzuE&3JdLyn*nAlO7yl5tLvUA|oWcm7kHb_CA3q+p+sBuy{Tu+z zNlHXOYiz6P&O13y=#m$n#(nf-cD#~m@$~ou`s|oB1nkMrs9D`NHU#F^<1JKs7$g|l zr^Wq7vPJK;hKJ)#%$9#qm<@U}8{~V;ithe9e|H6QZ|E4_gpOiOjKf$*u|hJD-T>|8T1vNfhU>RkYq@NZ5y^AfYa)u z$w35nW7UZHF7YDQMX%&^u}xuTZ4D8K5U^}zKhb0)CI=}ylP;wbTwuOF=GgZOJq^Ri zfwT!*lX)GYVF)qbFsp%C4+NA^a1oRK9CY;*vbeh+XU%5lp7DD7KKaj?LkO6w3~ZC( zGLh-dVC+?$U-mNB+xM{aUmlvl9x$vys2$`O7*ROlpkPd$N{}K&ahCbpOl{`BeH}DD zQlz2N+DVWM1kz|cRp*kK#nEb2>d?kPgqejP$&qj)1ShFHQ`2$55mEqvygz!d2x(CN@r`| z!yI}78yOzaRj4zm?p+dD)`1dGjSmYw(6XURbz7|hlxf{&zH{4)z=R>jDXJ12)*@S) zyQp>De&$LUHKIizLI7u}uQFZgD~uA(?fZNQC#q0Iu*kHNWeC=pCzj>{Qm=bfp3$^+ zwi}+C0S)u7DLY<2WZn-UVmtALT}w!DeIZjrnnYi(aP8XGuGt=ui3tJo9h%PANs0hX ztQx+b*|%C@7pyY#JhjR#772LiIBDJ%8n{N?9j33d0l zMEob{p?lfSIe}%rWCJ02%`NHwzU|n=-Z7A}$p}wTDkHtCGV7AE!nur6*;wo^AsO8h zTyTIksn#NwBbuly(&|5k(Wjv}H5(E(GqUpwXVpG5TbKx3{h{t!cAEyGXy(pyK@_2q zAObCfSA?Q8K&uE9Hf>NBAR!P-#1EkP1r}`iYLsN^>+8AWj8!=?XM>cwO?-TO?$ZH< zp8)kO2=5X;gT|;Ji>_wp8MbtDS0@`T3MvGr8fW(gq*?xm5X`0{d{5npQE?fKlI6`< z1^-Q2JS5UlP%A52wnRzfOKM|+BT6v#k|FtwXt zalh1i2U)cUT_W6pNCFD-!B`Bv;l&ZI0(Av4$L#=ROu;-m@bRbEO|TGSjl2+oH<(`> z6D+Ptb0{=GBogdOa)vIYYawCC8963Mqv(GocZ7EAv2Q|vw1S`C&wPY)5y9d9fW#Xv zXr?1`eZvdZN(E67m&5Oqr#(rdx#12*6REt%woCfMGnk1KjIy+pQ=J)fSkhG2^iiua z6l)RBs`CASH!C_KL+~U1vfdni;TcSqNdp@>db6qRtG$Cm(>v6VhXs?VCZ=amZ2`^^ zQKkq^DDftZIQAUoH48A3yr=6(I;<%ds6CaD=wo_D=0XG@%cuSx(`3P79InUGv-p&X z5Axj4cx}y- z>Igt@{?Q_z;KTDQ{B*vtNdq@LocaW&bnFK6srMU( zje)Ws>+Q7Ta0<4fF;o}~zaTTAT|3#f@>cKfY((Gdsle|Rvk}>B_~uhslC7VwMS3wI zIkAG9xQd@+uTY{nU?Yr&{kp^#&t2nrL8UtRjxCT?LEP18uY{FP;kqQqG3FY`@Q}{$ z)j=kqvO2!y8*0wc!!#V-P1J1&6FsWw*gOv^+3q`J7zG^T3VZ&i??{S#8cYJnrIx7d z{q@!)L?^77DNJ?k7bTbzAP}14#oFTmwMWY*ONaijzT&^Y90>gF_KBNML@&I(k_IsB~ zWJ2Nb=ngnCPK`P+Xr_S<+5*G0wF9*AqkC!Waez#N)!=@=e&Yd@iegwj=55Fq^I=&f z7)&_x_DvW>KE_5VKzG38_qMq7E#iu_&Tz)DpK3#kh8Vnvh8DNSv2PRetp{5!W9qdG zBO2mizwXP93_~+_ zxF0>L8PD;X10?pq+Q3;QOrst&M05ND8ZZX8=Ftsc2g4V>1z;8dCApgNw*s{L;J9ie z-+TPFdhT$@Qc5+Yc-dN*o1SXGXUjorGB0R#J}05Md=dHb)p5ZHQFNXAmhd%Mu0rvu zYUh7Ha82b)qgyV3GyHLMac902%xc^a0ApVZ44bC%rL|i&bY?_d_!e;Xi=3>pLkPi* z{fPTF2$V1FOg4bQE?h0zu;H^w7?z+FNNt%Xf?I&gmv1_{IctYm1mhUvw(ZKlzyGZ9 zH#>naO1Ws82iPwLF#9wJly5pYvpKhd&Q7@JJIVdW-nqOw4MbsFCz&)TD&hltfr^MI zqFY4}Q54+iQn%`6Qn*VaL`c9$N^BQxy>bic^$ou8@dx^7?P)rhoSe)|sp7W~TzIj+ z`}Mr$6%Ru(QIhtf0MP~Ic~4eyy9hTq`f(W9zVf;oN51>Wtdbgr<6$C-8g!8<52$b; zn5+IjzP$i}+gBbv)XZLNcinJ*IZ!J~MTtT{jGo|NH9ov+%D{B{%Eg&aYPdCHM3#th zg%BvP0~`+i|7A935V(D}m0x}-m+vvyM>8wOPKLwpKs+P&7A`G9G_KQniKtetAbidB zm~tPT%4&-0BH&DmLu>l(Zi=yAJ&zV(G<69JHx@6F+MqCoUgv`MvjlKg%dp(Ckgs^4c9ea>#N+_Z?Zgc~K!T?lV7`OhEQLM#( z{RwHwnt`FUkXvJmf~pKRFs@PU1$>R@ehR*o=lcH{KwWu2bu92v%G{ALh zE9i=1GNlQaRVKG*!m8**-irF_(0*T=D#mG!c>|C4^H~N=kD7_7uP*_rRRJmm%%>Np zHxyGRw}k^va3HZDC~pLq0g{>8ZLcW=^e3kOShvgGvH8A6bG0x0h&p21^*eTfnhu9asz&E9k9G|;Ee6On;wQ8xpP}~ zz~mTA2MiY;xU+v@4VE^1^jX3!cLYWD0q3+Tf(RJIc#HGdl{?;GX~R+47dDk_7IsAk zLQv8WFi7tNlBcvUOwN`K@Q60spjQzbc81Gpl;Xg?@}`mn10LW&I>PQOg3Ybka=wc- z9SysRC3#pEvI!V>oPL(;(E?=RcvUtE@U^GT_16mB00rLVZpPb_0_D*@vnHj*z+hLaFdHSA&gkGrOn*h-* z7Lz0geg(jju)Ld^;$W~NXE!5&Q|P3gc54OCVGm~_YAVCO4RI*u0W;3a_N8%h2zFqw zBkwsg0yvy&RJ%zn&!>66`Z&M`eFJxTt64q(x&T=9mMte25gyoA%5DhYrXR3J2rYv& z@2cp8THz)huE7sL0E27&32&DWA)t7WiZc^{dq@RYaE^pe9T)cLJqz$90ET0~jC`cvFoXx`Jc|*)F}@^>D^UdFtZ4x53L0P-L=XtW ztEKX*^2Pv%$hcgy637}Rxc3CWI!6jFDS8WYfY;0bP8v0I=~Jvrp#x@wQ!z&lDtZfp zfWK&M%K>%J7C}}Ysi9H8J}cSZ3vMc(%#sG``SV;DGg=*D+>bP~0EpX`1K#(bJ~mWB}|JX61~t>CteHO)g9(^9B1F z1iYsDiqiN3GP>w}_NKYcc$eg4kIr}d{*5*OX9NQw)vRIl5;frQU6G#SaCm8#bm;Ri z7Jcb}(=2^2;vD%F7}XjI z7 zMQK>adz}H8H=D^}ed`Gh6gLLU$AFdH^3fT*&{|Lcs{;YpIO$jV=Ru_Qb_Mg)umf~WE7YK&Pg8fE>}r^pw#8(36N3vjEVTswUFr+Lpq?9~Ds z+Wg}70-H+cfMFKwS9Z%s;6>vGfUTM=U4Fnn%Vf^biKS}>a2gm^l%O@5{SYA!E4jzy z%Xcf)G6OgcOe+(h#HNJ@Re<^2zG(-VjhYL~J{_=q14Ev(PdKkE)8@b9j7jcA#EbwQ z1?CkCtAv3s;@}o~v1ujdjV1txfeob#Ae^DIh$;s<_@$w)5x{w1Lw#YL@K@1%RT-8u zn6Za4HaFZn4%?Ixkqp-Q^;kthO&ES;Iz$ufo98c9-hEyVBt5@R2ki4*bA+9$4L;lm z;ME#AD@&w%QBlAM;m#z0=GC3_`n~pVt-K0~dfcW1X2r^jwY*m4lW4x35P@Ib8Fq=? z91xb*>Ia=bt>4ggAP5xgYSox+txrU?OA=C#^o1n3Sf|1QXKeL5uxA7j zIrE6pnuYkwc2SStjQPH6(*T1?zWU4}U>sHyHu$x~%74@?{Z4#AA_VB&E8yD*@ZOTx zidSg{2JTUVb1L5&EQe;jNWSkn$5I?<<@x&adu=jc5C>z|won7#$QOW5)Y<(IKT zb)VY`kpY7kjMy;1yL18LbphpeYN$Dk9jbfR956v1p-R)*7ZD)_#w-l+)zrqz_Cf%+ zYyrjtF%$!66DXIr`ukEoEe0^xEQ0|snJ{(VS}dAr6{*tf0^V&KFeoMRN@~<}8V9vL z+60Vu8jg9uA|`IKj!0*ZsU`JSn}G37!?gf-jkv$BhtwQ;WyFhr4{%6yWUQW0>YoF7 z-L?uCgf(&o0S{~fCU%l zZT(*Y48t^Q!=9L2zJ6Ny517)fz9)|>U%&YK0RLd`>{^@#qA(18?wz2hH~tE5{Y}F= zpBb72f;JZcMU=&g9~Z0Lsvx_H{y|MT?J>IcjC7`0=zGzoVN=*Ar)SQb%sF#y7l4l= zYasZ?ASVaFY+EVP>u3*5Ix7!=S@%opP@{bcX(11Q8TY`XAi3hVg**UeRz9+XpKPo^ zT9gOCjQukE9&<~l-W>q{L-X$ANo+F`Qke^IS?w9%kbhv>kxb(7l4hALxI()pQ{_oy z(Z%t{&y&gI^ONJa%HuDY0c+dr31CV&CY9VHADjbl?n1#uhCxmT_gna4C+W&-slv1z zCOpdkxI)vvO#!s~1em5|>9oi=(eMD8y#kz}ly;U8umg-v3YdFr|Nj+(FZL@Jk$+{+ z_tXZQ8n7-@#)T&t0aKw-O3k-TSN=g$Lbku3Gp|ws&an88cM|h=ClAlr0>zgvUL5~< zbL5tQEu%@ z9S)KKzOxI!({lo}IH633s+oqP0xlf+uXItK1=vRk0Y8?f z!FcE6r?e#av}#1tV^4V;2Uz>-U;CI4PB*p!0Liud>ap#WLAMh zTd1UfuQN@NvIBm%q5m}txTiT*0hrBzUFY>Mpm{aA40Rahr0ze@XLv&%3Nc*?0{!nltzHH2rA8rizO>0 z#Q+`{YOUAFGq|2_h}arr225c@cEF#a+seLzO%X*DQYz8QSqszwu^Qg%5Ccz>0X|}y zgXRY;qg$bT?aU^s$`nz6$9@5|gfz>KvcMqo^SG-M407DAeYZ0LCVgOBFdL>(IBWh5 zaEYbDg$_YBVH9zVYtWn@@Ck3gJmA(F09{|k83&k5m{L7O?Xk|RHE9y=m$??>`2m-w zf21lO{VU)Kz|w-7G7@k=DltftQHS3_mj*DqCwP9qLe57^Enkv-~EvsgCUd4uK z9i$%HK4c9@=jgNrSimWEroEaRu#>|ysv7;1($n!~z-B!pdGr?rSkALYx|FtX0DKa_OrQK~sR8RSggE><$JII(Fbzp|n0ZO^p0R+#7mTGOEYJko1-wxr z$yAx!c0Dyz$6n~Qi zS_ZIRyNE(Ig9{xm&}n$UcH|;-VG4Q_hl-{ev4C&hyMO=gQV;Imzjf>84Sr1o>2@N( zl^dDr5BAQcMQR`j!y=-nv&hUY=%T3jg^C9iJcxo9#e<3)a8q%Zrx*?@G!t_D8keRVTR6x z0>(v@N;26zJ;Hz%NkJ5J$>bupaxckBBqmi3%SYx!akuu>E0Z^UG&#jM;q`@S)anJ( z0`n9L7*eD$pd}l3vfw%v(RL8z%J3SCPO0+YH$62u!#Lr!0Zz?S2iGfm0ETiL&TB#@ z)!+f=C^$%6P$l^C%s3hz@T)hyb%Ke6*9JJM-yasypl>W-NMgN*!j56uyK0L|SW!*w z<*^HaaVk0h-s%r{n<+=E4Y0ynf_ei^h=gp?mq2#!>T%lYH8+DlE6lO}&5fuexHqou&gvBRMcV(ia7J{4>^=D(g zVeaD#DCDbYn~#4%a$_>RuwoKC!>k((1?;#Q&Wi+|GpVu3NOq5d3uAY>Nf|^lQbhF0 z+jv5d_2+RRaDTb79!qqpMQmO@d|^7>o|M|2KEAMZ`0DXvv+55=SLD!4MQsq^#99{= zoB2oquq_&j_kF@NqD5|vHVCEZ>(z_bfgi9U2sf40_n~C-^$3Sl(++L1VKB6NEt8zo z#>p+`O93p~&U7EWfK`-X3^}h#bE>495+*t!{D379r1Llp9bjQP)FNB*7*ejBZS!F; zwC_(W8L6O1r8Qb2*%0#s{xV+>nP?e8ZlGNdEBr2kj0#Z^&6}5xjSsd%X`LlR+3y2B z-gXa7xN(uGx~~&3BqLaM>Ia-qt+ZVQuD|LH=|Cm^=D!-S)}fj=p>tt4Yl2b+aA)^$ z;zV=6Ro#aJR^dF9%ZRa{MjT+L(lsOt8HO(5&@M^Pk}3Ac)O{p2O0=nuh;$Yaq<<6e zvB`3F(Ez^5wMPsGj15yzBHZK``vKd!X15Yqr-h{am z^Qw4V`$&C%cH|HJ0AqH8womQq1b9z3!2gd9bPIlO+tlq=eR|5XxI6-02F%ZH8_lG4 zkq5kQ1k3_Hy1WASsG-+@_cfr5fPH{5yFn*iW#A8~1?joUdF7?H&QQ0DDS1b#w*~;q=JY>^=fs2^hH>wDki z>m&ylCOiUe1K!->9yZZ*4ZvI!0TUhpw*YUga1YLap=~_j6t|yX1Ps8lRqg=>3{BWX z113BIc7SKAq6Zl8YQVcV?H&QwfKhvZdw>D|0`LY4c+Uvf>gAgy9d`S+V88&3#*-l? zd~pBz_2M)1wv_u_trt}?k9;cCJjMR3WsbkD|F)igUydF;TW2Rs;|XKJe^l2}aw@9! z8!WP;kaoZt^RA?g#V`Q!~kr!Y1M1eF}5JYb&Erm zwDwy-;nP3F7w!St>jQj?fs4BE-_k=R4BITrrE9Cm>{`FZZUl-I8ZHTq7b+ET`Rn1I z;tTfx1NH$%5uRDv_-lGn_4lQ@u|u;t2;GoLc^1TV(_Y%{qFTg}+o(`J`~Et8#BzFZ z{Sdde9)KV0`o~Q8(rd^%-y}s_nl$a~5&i6SQ-fO&EgHa;ZD0C9-LhsZEo7)Hn#kQQw)bHYWMY(QrCIXnrLFB&&jgRl{m zNjzjswf&e;q2`(BNli;Oz$)RZ#yfzL5$_KtEJNfAT-YaL6^O|s>LR#GTk;5zb~e^o zz2H^d(Rrm(Q$}R88KHkCM84P+@()l1;xY-V2+qwQ4M3TeA`YoL+lUDTWyK2Za)5kc z4|a>;fjHWYd(47X69T5LR47-4YApmA8~7U;y?hoZU)X~UUjvR{Ch?T*!dmLadmETf z=`gZ(^LSSJWMBEBde~_Y2V}z!*$$YIS>6*&xGtwA+t7_1iBH~x^@X)*yt6_(7#rRx zVCv@V3ehknjuon;qZ?U1e#_jYi}tRNLxpxIHoRmzPAZ~Y7(RIW9RN0QWjq)`GObgPTJGl_y6hn9G&51Yn5R}~$E4UcOybi_cZ z#juQ%BvqewpCQkrnP1kC9q)bR3wxMhf%~7s_j_ACcN!?o_?L2XGrt*G%KDS6pYQm} z7xpk~1suLZ7cO9=f^CCH2S0+UCYN+US<>UVi+pcbublZVaM3Sx5b*fc!U^IF!Z@PM z1=QwImPXmY2mB{)$K@~V8-XA7*k>$Yqy@r|WxuMWgTzrLGL1#iXde>xk}vGRe}Rj+ zqq7BUSS`w)N(6zWV&KBW5XjCoHG4@sQ5+I>mM>-x{tH~xr33_Q0|XNusYv@6wRCFW zgQDWSgM48RaqtU<=Mu1uQs#qwI+J_`g7c<|D7qa|_{1IOD6UIEm*_-XtS#BTP^PED zeK8~nWptzI`;PLxb}IOWj7X;vPnib<8zArKZ28SsW8rHp?rg7pfd%X#pg@bW9Dc|= zu(`z{Ody+H~NEFzQ;a6-9XuVNNRQ<&yI^jR<;y0T4?@oqKL}S1z z5!eI8Mt=5(<{wt^8^!+6Oy%0^(=+3Khd1A9)W3ekCL=xefnzV+SS5ENbt6x%0 z2bbExkMG89y0uZJgDr~L@Fgu$+#PyVn-=rqtLM4cdx&C2iwR%<v&i!XCyX! zNvpips-l*BI1&E}780oc~X+M&@bgsNrRB7T_LnSNIy#{$8ko$2rW zt}5d5SHD$7jQ;K)tD?*4`FweJs){bDeK#HM3cg>XWdIwDR&G=W@ - - -

- -

- - dispatchLens(applyChanges())} - data-test-subj="lnsSuggestionApplyChanges" - > - - -
- + const renderApplyChangesPrompt = () => ( + + +

+ +

+
+ + dispatchLens(applyChanges())} + data-test-subj="lnsApplyChanges__suggestions" + > + +
); - const suggestionsUI = ( + const renderSuggestionsUI = () => ( <> {currentVisualization.activeId && !hideSuggestions && (
- {changesApplied ? suggestionsUI : applyChangesPrompt} + {changesApplied ? renderSuggestionsUI() : renderApplyChangesPrompt()}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx index 2ed4f223994c6..44d6a7118aca9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx @@ -52,7 +52,7 @@ export function GeoFieldWorkspacePanel(props: Props) {

{getVisualizeGeoFieldMessage(props.fieldType)}

- + { let mockVisualization: jest.Mocked; let mockVisualization2: jest.Mocked; @@ -115,7 +121,7 @@ describe('workspace_panel', () => { instance = mounted.instance; instance.update(); - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); + expect(instance.find('[data-test-subj="workspace-drag-drop-prompt"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -133,7 +139,7 @@ describe('workspace_panel', () => { instance = mounted.instance; instance.update(); - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); + expect(instance.find('[data-test-subj="workspace-drag-drop-prompt"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -151,7 +157,7 @@ describe('workspace_panel', () => { instance = mounted.instance; instance.update(); - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); + expect(instance.find('[data-test-subj="workspace-drag-drop-prompt"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -218,8 +224,10 @@ describe('workspace_panel', () => { instance = mounted.instance; instance.update(); + const getExpression = () => instance.find(expressionRendererMock).prop('expression'); + // allows initial render - expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + expect(getExpression()).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={datasource} | testVis" @@ -233,9 +241,9 @@ describe('workspace_panel', () => { }, }); }); + instance.update(); - // nothing should change - expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + expect(getExpression()).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={datasource} | testVis" @@ -247,7 +255,7 @@ describe('workspace_panel', () => { instance.update(); // should update - expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + expect(getExpression()).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={new-datasource} | new-vis" @@ -260,22 +268,12 @@ describe('workspace_panel', () => { testVis: { ...mockVisualization, toExpression: () => 'other-new-vis' }, }, }); - }); - - // should not update - expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` - "kibana - | lens_merge_tables layerIds=\\"first\\" tables={new-datasource} - | new-vis" - `); - - act(() => { mounted.lensStore.dispatch(enableAutoApply()); }); instance.update(); // reenabling auto-apply triggers an update as well - expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + expect(getExpression()).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={other-new-datasource} | other-new-vis" @@ -339,13 +337,41 @@ describe('workspace_panel', () => { expect(isSaveable()).toBe(false); }); - it('should allow empty workspace as initial render when auto-apply disabled', async () => { - mockVisualization.toExpression.mockReturnValue('testVis'); + it('should show proper workspace prompts when auto-apply disabled', async () => { const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; + const configureValidVisualization = () => { + mockVisualization.toExpression.mockReturnValue('testVis'); + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + act(() => { + instance.setProps({ + visualizationMap: { + testVis: { ...mockVisualization, toExpression: () => 'new-vis' }, + }, + }); + }); + }; + + const deleteVisualization = () => { + act(() => { + instance.setProps({ + visualizationMap: { + testVis: { ...mockVisualization, toExpression: () => null }, + }, + }); + }); + }; + + const dragDropPromptShowing = () => instance.exists(SELECTORS.dragDropPrompt); + + const applyChangesPromptShowing = () => instance.exists(SELECTORS.applyChangesPrompt); + + const visualizationShowing = () => instance.exists(expressionRendererMock); + const mounted = await mountWithProvider( { visualizationMap={{ testVis: mockVisualization, }} + ExpressionRenderer={expressionRendererMock} />, { preloadedState: { @@ -367,7 +394,37 @@ describe('workspace_panel', () => { instance = mounted.instance; instance.update(); - expect(instance.exists('[data-test-subj="empty-workspace"]')).toBeTruthy(); + expect(dragDropPromptShowing()).toBeTruthy(); + + configureValidVisualization(); + instance.update(); + + expect(dragDropPromptShowing()).toBeFalsy(); + expect(applyChangesPromptShowing()).toBeTruthy(); + + instance.find(SELECTORS.applyChangesButton).simulate('click'); + instance.update(); + + expect(visualizationShowing()).toBeTruthy(); + + deleteVisualization(); + instance.update(); + + expect(visualizationShowing()).toBeTruthy(); + + act(() => { + mounted.lensStore.dispatch(applyChanges()); + }); + instance.update(); + + expect(visualizationShowing()).toBeFalsy(); + expect(dragDropPromptShowing()).toBeTruthy(); + + configureValidVisualization(); + instance.update(); + + expect(dragDropPromptShowing()).toBeFalsy(); + expect(applyChangesPromptShowing()).toBeTruthy(); }); it('should execute a trigger on expression event', async () => { @@ -921,9 +978,7 @@ describe('workspace_panel', () => { expect(showingErrors()).toBeFalsy(); // errors should appear when problem changes are applied - act(() => { - lensStore.dispatch(applyChanges()); - }); + instance.find(SELECTORS.applyChangesButton).simulate('click'); instance.update(); expect(showingErrors()).toBeTruthy(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 0ad237109d08b..d349428902c82 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -20,6 +20,7 @@ import { EuiPageContentBody, EuiButton, EuiSpacer, + EuiTextColor, } from '@elastic/eui'; import type { CoreStart, ApplicationStart } from 'kibana/public'; import type { DataPublicPluginStart, ExecutionContextSearch } from 'src/plugins/data/public'; @@ -47,6 +48,8 @@ import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/publ import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { DropIllustration } from '../../../assets/drop_illustration'; +import applyChangesIllustrationDark from '../../../assets/render_dark@2x.png'; +import applyChangesIllustrationLight from '../../../assets/render_light@2x.png'; import { getOriginalRequestErrorMessages, getUnknownVisualizationTypeError, @@ -69,11 +72,14 @@ import { selectAutoApplyEnabled, selectTriggerApplyChanges, selectDatasourceLayers, + applyChanges, + selectChangesApplied, } from '../../../state_management'; import type { LensInspector } from '../../../lens_inspector_service'; import { inferTimeField } from '../../../utils'; import { setChangesApplied } from '../../../state_management/lens_slice'; import type { Datatable } from '../../../../../../../src/plugins/expressions/public'; +import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../config_panel/dimension_container'; export interface WorkspacePanelProps { visualizationMap: VisualizationMap; @@ -143,6 +149,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ const activeDatasourceId = useLensSelector(selectActiveDatasourceId); const datasourceStates = useLensSelector(selectDatasourceStates); const autoApplyEnabled = useLensSelector(selectAutoApplyEnabled); + const changesApplied = useLensSelector(selectChangesApplied); const triggerApply = useLensSelector(selectTriggerApplyChanges); const [localState, setLocalState] = useState({ @@ -201,7 +208,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ [activeVisualization, visualization.state, activeDatasourceId, datasourceMap, datasourceStates] ); - const _expression = useMemo(() => { + // if the expression is undefined, it means we hit an error that should be displayed to the user + const unappliedExpression = useMemo(() => { if (!configurationValidationError?.length && !missingRefsErrors.length && !unknownVisError) { try { const ast = buildExpression({ @@ -254,20 +262,23 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ ]); useEffect(() => { - dispatchLens(setSaveable(Boolean(_expression))); - }, [_expression, dispatchLens]); + dispatchLens(setSaveable(Boolean(unappliedExpression))); + }, [unappliedExpression, dispatchLens]); useEffect(() => { if (!autoApplyEnabled) { - dispatchLens(setChangesApplied(_expression === localState.expressionToRender)); + dispatchLens(setChangesApplied(unappliedExpression === localState.expressionToRender)); } }); useEffect(() => { if (shouldApplyExpression) { - setLocalState((s) => ({ ...s, expressionToRender: _expression })); + setLocalState((s) => ({ + ...s, + expressionToRender: unappliedExpression, + })); } - }, [_expression, shouldApplyExpression]); + }, [unappliedExpression, shouldApplyExpression]); const expressionExists = Boolean(localState.expressionToRender); useEffect(() => { @@ -332,15 +343,23 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ } }, [suggestionForDraggedField, expressionExists, dispatchLens]); - const renderEmptyWorkspace = () => { + const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); + + const renderDragDropPrompt = () => { return ( +

{!expressionExists @@ -352,26 +371,25 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ })}

- {!expressionExists && ( <> -

- {i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', { - defaultMessage: 'Lens is the recommended editor for creating visualizations', - })} -

-

- - - {i18n.translate('xpack.lens.editorFrame.goToForums', { - defaultMessage: 'Make requests and give feedback', - })} - - + +

+ {i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', { + defaultMessage: 'Lens is the recommended editor for creating visualizations', + })} +

+ +

+ + {i18n.translate('xpack.lens.editorFrame.goToForums', { + defaultMessage: 'Make requests and give feedback', + })} +

)} @@ -379,11 +397,47 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ ); }; - const renderVisualization = () => { - if (localState.expressionToRender === null) { - return renderEmptyWorkspace(); - } + const renderApplyChangesPrompt = () => { + const applyChangesString = i18n.translate('xpack.lens.editorFrame.applyChanges', { + defaultMessage: 'Apply changes', + }); + return ( + + {applyChangesString} +

+ + {i18n.translate('xpack.lens.editorFrame.applyChangesWorkspacePrompt', { + defaultMessage: 'Apply changes to render visualization', + })} + +

+

+ dispatchLens(applyChanges())} + data-test-subj="lnsApplyChanges__workspace" + > + {applyChangesString} + +

+
+ ); + }; + + const renderVisualization = () => { return ( { + const renderWorkspace = () => { const customWorkspaceRenderer = activeDatasourceId && datasourceMap[activeDatasourceId]?.getCustomWorkspaceRenderer && @@ -413,9 +467,19 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ ) : undefined; - return customWorkspaceRenderer ? ( - customWorkspaceRenderer() - ) : ( + if (customWorkspaceRenderer) { + return customWorkspaceRenderer(); + } + + const hasSomethingToRender = localState.expressionToRender !== null; + + const renderWorkspaceContents = hasSomethingToRender + ? renderVisualization + : !changesApplied + ? renderApplyChangesPrompt + : renderDragDropPrompt; + + return ( - {renderVisualization()} + {renderWorkspaceContents()} ); @@ -444,7 +508,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ visualizationMap={visualizationMap} isFullscreen={isFullscreen} > - {renderDragDrop()} + {renderWorkspace()} ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss index 9b4502ea81944..a3287179f13cd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss @@ -31,6 +31,9 @@ &.lnsWorkspacePanelWrapper--fullscreen { margin-bottom: 0; + .lnsWorkspacePanelWrapper__pageContentBody { + box-shadow: none; + } } } @@ -77,6 +80,10 @@ justify-content: center; align-items: center; transition: background-color $euiAnimSpeedFast ease-in-out; + + .lnsWorkspacePanel__actions { + margin-top: $euiSizeL; + } } .lnsWorkspacePanelWrapper__toolbar { @@ -84,6 +91,7 @@ &.lnsWorkspacePanelWrapper__toolbar--fullscreen { padding: $euiSizeS $euiSizeS 0 $euiSizeS; + background-color: #FFF; } & > .euiFlexItem { @@ -91,14 +99,18 @@ } } -.lnsDropIllustration__adjustFill { - fill: $euiColorFullShade; +.lnsWorkspacePanel__promptIllustration { + overflow: visible; // Shows arrow animation when it gets out of bounds + margin-top: 0; + margin-bottom: -$euiSize; + + margin-right: auto; + margin-left: auto; + max-width: 176px; + max-height: 176px; } .lnsWorkspacePanel__dropIllustration { - overflow: visible; // Shows arrow animation when it gets out of bounds - margin-top: $euiSizeL; - margin-bottom: $euiSizeXXL; // Drop shadow values is a dupe of @euiBottomShadowMedium but used as a filter // Hard-coded px values OK (@cchaos) // sass-lint:disable-block indentation @@ -108,6 +120,10 @@ drop-shadow(0 2px 2px transparentize($euiShadowColor, .8)); } +.lnsDropIllustration__adjustFill { + fill: $euiColorFullShade; +} + .lnsDropIllustration__hand { animation: lnsWorkspacePanel__illustrationPulseArrow 5s ease-in-out 0s infinite normal forwards; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx index 3aab4d6e7d85c..fcdbedff74a8d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx @@ -17,7 +17,7 @@ import { disableAutoApply, selectTriggerApplyChanges, } from '../../../state_management'; -import { setChangesApplied } from '../../../state_management/lens_slice'; +import { enableAutoApply, setChangesApplied } from '../../../state_management/lens_slice'; describe('workspace_panel_wrapper', () => { let mockVisualization: jest.Mocked; @@ -83,19 +83,7 @@ describe('workspace_panel_wrapper', () => { } private get applyChangesButton() { - return this._instance.find('button[data-test-subj="lensApplyChanges"]'); - } - - private get autoApplyToggleSwitch() { - return this._instance.find('button[data-test-subj="lensToggleAutoApply"]'); - } - - toggleAutoApply() { - this.autoApplyToggleSwitch.simulate('click'); - } - - public get autoApplySwitchOn() { - return this.autoApplyToggleSwitch.prop('aria-checked'); + return this._instance.find('button[data-test-subj="lnsApplyChanges__toolbar"]'); } applyChanges() { @@ -135,28 +123,24 @@ describe('workspace_panel_wrapper', () => { harness = new Harness(instance); }); - it('toggles auto-apply', async () => { + it('shows and hides apply-changes button depending on whether auto-apply is enabled', async () => { store.dispatch(disableAutoApply()); harness.update(); - expect(selectAutoApplyEnabled(store.getState())).toBeFalsy(); - expect(harness.autoApplySwitchOn).toBeFalsy(); expect(harness.applyChangesExists).toBeTruthy(); - harness.toggleAutoApply(); + store.dispatch(enableAutoApply()); + harness.update(); - expect(selectAutoApplyEnabled(store.getState())).toBeTruthy(); - expect(harness.autoApplySwitchOn).toBeTruthy(); expect(harness.applyChangesExists).toBeFalsy(); - harness.toggleAutoApply(); + store.dispatch(disableAutoApply()); + harness.update(); - expect(selectAutoApplyEnabled(store.getState())).toBeFalsy(); - expect(harness.autoApplySwitchOn).toBeFalsy(); expect(harness.applyChangesExists).toBeTruthy(); }); - it('apply-changes button works', () => { + it('apply-changes button applies changes', () => { store.dispatch(disableAutoApply()); harness.update(); @@ -199,13 +183,11 @@ describe('workspace_panel_wrapper', () => { harness.update(); expect(harness.applyChangesDisabled).toBeFalsy(); - expect(harness.autoApplySwitchOn).toBeFalsy(); expect(harness.applyChangesExists).toBeTruthy(); - // enable auto apply - harness.toggleAutoApply(); + store.dispatch(enableAutoApply()); + harness.update(); - expect(harness.autoApplySwitchOn).toBeTruthy(); expect(harness.applyChangesExists).toBeFalsy(); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index ab7dad2cb5fea..bb85ed4019412 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -8,12 +8,9 @@ import './workspace_panel_wrapper.scss'; import React, { useCallback } from 'react'; -import { EuiPageContent, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiButton } from '@elastic/eui'; +import { EuiPageContent, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { trackUiEvent } from '../../../lens_ui_telemetry'; -import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../../types'; import { NativeRenderer } from '../../../native_renderer'; import { ChartSwitch } from './chart_switch'; @@ -27,13 +24,10 @@ import { useLensSelector, selectChangesApplied, applyChanges, - enableAutoApply, - disableAutoApply, selectAutoApplyEnabled, } from '../../../state_management'; import { WorkspaceTitle } from './title'; import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../config_panel/dimension_container'; -import { writeToStorage } from '../../../settings_storage'; export const AUTO_APPLY_DISABLED_STORAGE_KEY = 'autoApplyDisabled'; @@ -90,17 +84,6 @@ export function WorkspacePanelWrapper({ [dispatchLens] ); - const toggleAutoApply = useCallback(() => { - trackUiEvent('toggle_autoapply'); - - writeToStorage( - new Storage(localStorage), - AUTO_APPLY_DISABLED_STORAGE_KEY, - String(autoApplyEnabled) - ); - dispatchLens(autoApplyEnabled ? disableAutoApply() : enableAutoApply()); - }, [dispatchLens, autoApplyEnabled]); - const warningMessages: React.ReactNode[] = []; if (activeVisualization?.getWarningMessages) { warningMessages.push( @@ -119,102 +102,82 @@ export function WorkspacePanelWrapper({ }); return ( <> -
- - - - {!isFullscreen && ( - - - - - - {activeVisualization && activeVisualization.renderToolbar && ( - - - - )} - - - )} - - + {!(isFullscreen && (autoApplyEnabled || warningMessages?.length)) && ( +
+ + {!isFullscreen && ( + + - - {!autoApplyEnabled && ( + {activeVisualization && activeVisualization.renderToolbar && ( -
- dispatchLens(applyChanges())} - size="s" - data-test-subj="lensApplyChanges" - > - - -
+
)}
-
- - - {warningMessages && warningMessages.length ? ( - {warningMessages} - ) : null} - - -
+ )} + + + + {warningMessages?.length ? ( + {warningMessages} + ) : null} + + {!autoApplyEnabled && ( + +
+ dispatchLens(applyChanges())} + size="m" + data-test-subj="lnsApplyChanges__toolbar" + > + + +
+
+ )} +
+
+
+
+ )} { + it('should preserve apply-changes button with full-screen datasource', async () => { await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.disableAutoApply(); + await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'formula', @@ -56,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { PageObjects.lens.toggleFullscreen(); - expect(await PageObjects.lens.getAutoApplyToggleExists()).to.be.ok(); + expect(await PageObjects.lens.applyChangesExists('toolbar')).to.be.ok(); PageObjects.lens.toggleFullscreen(); @@ -79,9 +83,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // assert that changes haven't been applied - await PageObjects.lens.waitForEmptyWorkspace(); + await PageObjects.lens.waitForWorkspaceWithApplyChangesPrompt(); - await PageObjects.lens.applyChanges(); + await PageObjects.lens.applyChanges('workspace'); await PageObjects.lens.waitForVisualization('xyVisChart'); }); @@ -89,11 +93,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should hide suggestions when a change is made', async () => { await PageObjects.lens.switchToVisualization('lnsDatatable'); - expect(await PageObjects.lens.getAreSuggestionsPromptingToApply()).to.be.ok(); + expect(await PageObjects.lens.applyChangesExists('suggestions')).to.be.ok(); - await PageObjects.lens.applyChanges(true); + await PageObjects.lens.applyChanges('suggestions'); - expect(await PageObjects.lens.getAreSuggestionsPromptingToApply()).not.to.be.ok(); + expect(await PageObjects.lens.applyChangesExists('suggestions')).not.to.be.ok(); }); }); } diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index c2a98d2d5dedc..13525ed0ec9c2 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -145,7 +145,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getLayerCount()).to.eql(2); await PageObjects.lens.removeLayer(); await PageObjects.lens.removeLayer(); - await testSubjects.existOrFail('empty-workspace'); + await testSubjects.existOrFail('workspace-drag-drop-prompt'); }); it('should edit settings of xy line chart', async () => { diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index fa46052705c91..5a95b195fb0c0 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -276,7 +276,13 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont async waitForEmptyWorkspace() { await retry.try(async () => { - await testSubjects.existOrFail(`empty-workspace`); + await testSubjects.existOrFail(`workspace-drag-drop-prompt`); + }); + }, + + async waitForWorkspaceWithApplyChangesPrompt() { + await retry.try(async () => { + await testSubjects.existOrFail(`workspace-apply-changes-prompt`); }); }, @@ -1336,32 +1342,48 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont return testSubjects.exists('lnsEmptySizeRatioButtonGroup'); }, - getAutoApplyToggleExists() { - return testSubjects.exists('lensToggleAutoApply'); + settingsMenuOpen() { + return testSubjects.exists('lnsApp__settingsMenu'); }, - enableAutoApply() { - return testSubjects.setEuiSwitch('lensToggleAutoApply', 'check'); + async openSettingsMenu() { + if (await this.settingsMenuOpen()) return; + + await testSubjects.click('lnsApp_settingsButton'); }, - disableAutoApply() { - return testSubjects.setEuiSwitch('lensToggleAutoApply', 'uncheck'); + async closeSettingsMenu() { + if (!(await this.settingsMenuOpen())) return; + + await testSubjects.click('lnsApp_settingsButton'); }, - getAutoApplyEnabled() { - return testSubjects.isEuiSwitchChecked('lensToggleAutoApply'); + async enableAutoApply() { + await this.openSettingsMenu(); + + return testSubjects.setEuiSwitch('lnsToggleAutoApply', 'check'); }, - async applyChanges(throughSuggestions = false) { - const applyButtonSelector = throughSuggestions - ? 'lnsSuggestionApplyChanges' - : 'lensApplyChanges'; - await testSubjects.waitForEnabled(applyButtonSelector); - await testSubjects.click(applyButtonSelector); + async disableAutoApply() { + await this.openSettingsMenu(); + + return testSubjects.setEuiSwitch('lnsToggleAutoApply', 'uncheck'); + }, + + async getAutoApplyEnabled() { + await this.openSettingsMenu(); + + return testSubjects.isEuiSwitchChecked('lnsToggleAutoApply'); }, - async getAreSuggestionsPromptingToApply() { - return testSubjects.exists('lnsSuggestionApplyChanges'); + applyChangesExists(whichButton: 'toolbar' | 'suggestions' | 'workspace') { + return testSubjects.exists(`lnsApplyChanges__${whichButton}`); + }, + + async applyChanges(whichButton: 'toolbar' | 'suggestions' | 'workspace') { + const applyButtonSelector = `lnsApplyChanges__${whichButton}`; + await testSubjects.waitForEnabled(applyButtonSelector); + await testSubjects.click(applyButtonSelector); }, }); } From d579ea73b9a58f1c3d61c31d26a8a9e571bdef7b Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Tue, 29 Mar 2022 23:10:14 +0200 Subject: [PATCH 033/108] [Security Solution][Endpoint] Fix event filter creation success toast (#128810) * Show creation success toast on adding first event filter fixes elastic/kibana/issues/128444 * clean up redundant type imports refs elastic/kibana/pull/100310/ --- .../pages/event_filters/store/middleware.ts | 7 +++---- .../pages/event_filters/view/translations.ts | 13 +++---------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts index 83526c907e73f..a8bf725e61b2a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts @@ -89,10 +89,6 @@ const eventFiltersCreate: MiddlewareActionHandler = async (store, eventFiltersSe const exception = await eventFiltersService.addEventFilters(updatedCommentsEntry); - store.dispatch({ - type: 'eventFiltersCreateSuccess', - }); - store.dispatch({ type: 'eventFiltersFormStateChanged', payload: { @@ -100,6 +96,9 @@ const eventFiltersCreate: MiddlewareActionHandler = async (store, eventFiltersSe data: exception, }, }); + store.dispatch({ + type: 'eventFiltersCreateSuccess', + }); } catch (error) { store.dispatch({ type: 'eventFiltersFormStateChanged', diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts index ae8012711fbf1..6177fb7822c92 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts @@ -7,24 +7,17 @@ import { i18n } from '@kbn/i18n'; -import type { - CreateExceptionListItemSchema, - UpdateExceptionListItemSchema, -} from '@kbn/securitysolution-io-ts-list-types'; import { ServerApiError } from '../../../../common/types'; +import { EventFiltersForm } from '../types'; -export const getCreationSuccessMessage = ( - entry: CreateExceptionListItemSchema | UpdateExceptionListItemSchema | undefined -) => { +export const getCreationSuccessMessage = (entry: EventFiltersForm['entry']) => { return i18n.translate('xpack.securitySolution.eventFilter.form.creationSuccessToastTitle', { defaultMessage: '"{name}" has been added to the event filters list.', values: { name: entry?.name }, }); }; -export const getUpdateSuccessMessage = ( - entry: CreateExceptionListItemSchema | UpdateExceptionListItemSchema | undefined -) => { +export const getUpdateSuccessMessage = (entry: EventFiltersForm['entry']) => { return i18n.translate('xpack.securitySolution.eventFilter.form.updateSuccessToastTitle', { defaultMessage: '"{name}" has been updated successfully.', values: { name: entry?.name }, From ac5603b267eb5b69734758b5412f6cf10a35d514 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 29 Mar 2022 22:12:19 +0100 Subject: [PATCH 034/108] fix(NA): do not declare @types packages as prod dependencies when generating the pkg.json (#128805) --- package.json | 10 +++++----- packages/kbn-generate/src/commands/package_command.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 1906f3af232b7..adc45d9afadd7 100644 --- a/package.json +++ b/package.json @@ -200,11 +200,6 @@ "@turf/helpers": "6.0.1", "@turf/length": "^6.0.2", "@types/jsonwebtoken": "^8.5.6", - "@types/kbn__plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery/npm_module_types", - "@types/kbn__shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components/npm_module_types", - "@types/kbn__shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types", - "@types/kbn__shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook/npm_module_types", - "@types/kbn__shared-ux-utility": "link:bazel-bin/packages/kbn-shared-ux-utility/npm_module_types", "@types/moment-duration-format": "^2.2.3", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", @@ -608,6 +603,7 @@ "@types/kbn__mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types", "@types/kbn__monaco": "link:bazel-bin/packages/kbn-monaco/npm_module_types", "@types/kbn__optimizer": "link:bazel-bin/packages/kbn-optimizer/npm_module_types", + "@types/kbn__plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery/npm_module_types", "@types/kbn__plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator/npm_module_types", "@types/kbn__plugin-helpers": "link:bazel-bin/packages/kbn-plugin-helpers/npm_module_types", "@types/kbn__react-field": "link:bazel-bin/packages/kbn-react-field/npm_module_types", @@ -628,6 +624,10 @@ "@types/kbn__securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module_types", "@types/kbn__server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools/npm_module_types", "@types/kbn__server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository/npm_module_types", + "@types/kbn__shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components/npm_module_types", + "@types/kbn__shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types", + "@types/kbn__shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook/npm_module_types", + "@types/kbn__shared-ux-utility": "link:bazel-bin/packages/kbn-shared-ux-utility/npm_module_types", "@types/kbn__std": "link:bazel-bin/packages/kbn-std/npm_module_types", "@types/kbn__storybook": "link:bazel-bin/packages/kbn-storybook/npm_module_types", "@types/kbn__telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools/npm_module_types", diff --git a/packages/kbn-generate/src/commands/package_command.ts b/packages/kbn-generate/src/commands/package_command.ts index abf4434b78bcd..074f48d26114b 100644 --- a/packages/kbn-generate/src/commands/package_command.ts +++ b/packages/kbn-generate/src/commands/package_command.ts @@ -133,9 +133,13 @@ ${BAZEL_PACKAGE_DIRS.map((rel) => ` ${rel}\n`).join('')} : [packageJson.dependencies, packageJson.devDependencies]; addDeps[name] = `link:bazel-bin/${normalizedRepoRelativeDir}`; - addDeps[typePkgName] = `link:bazel-bin/${normalizedRepoRelativeDir}/npm_module_types`; delete removeDeps[name]; - delete removeDeps[typePkgName]; + + // for @types packages always remove from deps and add to devDeps + packageJson.devDependencies[ + typePkgName + ] = `link:bazel-bin/${normalizedRepoRelativeDir}/npm_module_types`; + delete packageJson.dependencies[typePkgName]; await Fsp.writeFile(packageJsonPath, sortPackageJson(JSON.stringify(packageJson))); log.info('Updated package.json file'); From 18c7f4d0328c290c92b135d1b452fb4636d8c04b Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 29 Mar 2022 14:20:34 -0700 Subject: [PATCH 035/108] Revert "Add a tour showing new rules search capabilities (#128759)" This reverts commit 02a146f7e4fd069c51d964da37460d63d980e829. --- .../security_solution/common/constants.ts | 2 +- .../detection_engine/rules/api.test.ts | 6 +- .../detection_engine/rules/utils.test.ts | 6 +- .../detection_engine/rules/utils.ts | 2 - .../rules_feature_tour_context.tsx | 74 +++++----- .../rules/all/feature_tour/translations.ts | 15 -- .../detection_engine/rules/all/index.test.tsx | 7 +- .../rules_table_filters.test.tsx | 9 +- .../rules_table_filters.tsx | 32 ++--- .../pages/detection_engine/rules/index.tsx | 135 +++++++++--------- .../detection_engine/rules/translations.ts | 3 +- 11 files changed, 127 insertions(+), 164 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index f7bdc889f9c33..591c7d68e17cb 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -443,7 +443,7 @@ export const RULES_TABLE_PAGE_SIZE_OPTIONS = [5, 10, 20, 50, RULES_TABLE_MAX_PAG * we will need to update this constant with the corresponding version. */ export const RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY = - 'securitySolution.rulesManagementPage.newFeaturesTour.v8.2'; + 'securitySolution.rulesManagementPage.newFeaturesTour.v8.1'; export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY = 'securitySolution.ruleDetails.ruleExecutionLog.showMetrics.v8.2'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 3c534ca7294a5..c8d8b5bb6ffd0 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -143,7 +143,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - '(alert.attributes.name: "hello world" OR alert.attributes.params.index: "hello world" OR alert.attributes.params.threat.tactic.id: "hello world" OR alert.attributes.params.threat.tactic.name: "hello world" OR alert.attributes.params.threat.technique.id: "hello world" OR alert.attributes.params.threat.technique.name: "hello world" OR alert.attributes.params.threat.technique.subtechnique.id: "hello world" OR alert.attributes.params.threat.technique.subtechnique.name: "hello world")', + '(alert.attributes.name: "hello world" OR alert.attributes.params.index: "hello world" OR alert.attributes.params.threat.tactic.id: "hello world" OR alert.attributes.params.threat.tactic.name: "hello world" OR alert.attributes.params.threat.technique.id: "hello world" OR alert.attributes.params.threat.technique.name: "hello world")', page: 1, per_page: 20, sort_field: 'enabled', @@ -172,7 +172,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - '(alert.attributes.name: "\\" OR (foo:bar)" OR alert.attributes.params.index: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.subtechnique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.subtechnique.name: "\\" OR (foo:bar)")', + '(alert.attributes.name: "\\" OR (foo:bar)" OR alert.attributes.params.index: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo:bar)")', page: 1, per_page: 20, sort_field: 'enabled', @@ -383,7 +383,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - 'alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags:("hello" AND "world") AND (alert.attributes.name: "ruleName" OR alert.attributes.params.index: "ruleName" OR alert.attributes.params.threat.tactic.id: "ruleName" OR alert.attributes.params.threat.tactic.name: "ruleName" OR alert.attributes.params.threat.technique.id: "ruleName" OR alert.attributes.params.threat.technique.name: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.id: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.name: "ruleName")', + 'alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags:("hello" AND "world") AND (alert.attributes.name: "ruleName" OR alert.attributes.params.index: "ruleName" OR alert.attributes.params.threat.tactic.id: "ruleName" OR alert.attributes.params.threat.tactic.name: "ruleName" OR alert.attributes.params.threat.technique.id: "ruleName" OR alert.attributes.params.threat.technique.name: "ruleName")', page: 1, per_page: 20, sort_field: 'enabled', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts index a26a4aec3ec02..e3d2300972a51 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts @@ -27,7 +27,7 @@ describe('convertRulesFilterToKQL', () => { const kql = convertRulesFilterToKQL({ ...filterOptions, filter: 'foo' }); expect(kql).toBe( - '(alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")' + '(alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo")' ); }); @@ -35,7 +35,7 @@ describe('convertRulesFilterToKQL', () => { const kql = convertRulesFilterToKQL({ ...filterOptions, filter: '" OR (foo: bar)' }); expect(kql).toBe( - '(alert.attributes.name: "\\" OR (foo: bar)" OR alert.attributes.params.index: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.subtechnique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.subtechnique.name: "\\" OR (foo: bar)")' + '(alert.attributes.name: "\\" OR (foo: bar)" OR alert.attributes.params.index: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo: bar)")' ); }); @@ -66,7 +66,7 @@ describe('convertRulesFilterToKQL', () => { }); expect(kql).toBe( - `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags:("tag1" AND "tag2") AND (alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")` + `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags:(\"tag1\" AND \"tag2\") AND (alert.attributes.name: \"foo\" OR alert.attributes.params.index: \"foo\" OR alert.attributes.params.threat.tactic.id: \"foo\" OR alert.attributes.params.threat.tactic.name: \"foo\" OR alert.attributes.params.threat.technique.id: \"foo\" OR alert.attributes.params.threat.technique.name: \"foo\")` ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts index 069746223731c..f5e52fd6362c1 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts @@ -16,8 +16,6 @@ const SEARCHABLE_RULE_PARAMS = [ 'alert.attributes.params.threat.tactic.name', 'alert.attributes.params.threat.technique.id', 'alert.attributes.params.threat.technique.name', - 'alert.attributes.params.threat.technique.subtechnique.id', - 'alert.attributes.params.threat.technique.subtechnique.name', ]; /** diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/rules_feature_tour_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/rules_feature_tour_context.tsx index b0f0b0f17923c..aaa483e49fca7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/rules_feature_tour_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/rules_feature_tour_context.tsx @@ -43,13 +43,23 @@ const tourConfig: EuiTourState = { const stepsConfig: EuiStatelessTourStep[] = [ { step: 1, - title: i18n.SEARCH_CAPABILITIES_TITLE, - content:

{i18n.SEARCH_CAPABILITIES_DESCRIPTION}

, - stepsTotal: 1, + title: 'A new feature', + content:

{'This feature allows for...'}

, + stepsTotal: 2, children: <>, onFinish: noop, maxWidth: TOUR_POPOVER_WIDTH, }, + { + step: 2, + title: 'Another feature', + content:

{'This another feature allows for...'}

, + stepsTotal: 2, + children: <>, + onFinish: noop, + anchorPosition: 'rightUp', + maxWidth: TOUR_POPOVER_WIDTH, + }, ]; const RulesFeatureTourContext = createContext(null); @@ -72,43 +82,39 @@ export const RulesFeatureTourContextProvider: FC = ({ children }) => { const [tourSteps, tourActions, tourState] = useEuiTour(stepsConfig, restoredState); - const enhancedSteps = useMemo( - () => - tourSteps.map((item, index) => ({ + const enhancedSteps = useMemo(() => { + return tourSteps.map((item, index, array) => { + return { ...item, content: ( <> {item.content} - {tourSteps.length > 1 && ( - <> - - - - - - - - - - - )} + + + + + + + + + ), - })), - [tourSteps, tourActions] - ); + }; + }); + }, [tourSteps, tourActions]); const providerValue = useMemo( () => ({ steps: enhancedSteps, actions: tourActions }), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/translations.ts index 88b5489b01eaa..bfcda64bb13dd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/feature_tour/translations.ts @@ -13,18 +13,3 @@ export const TOUR_TITLE = i18n.translate( defaultMessage: "What's new", } ); - -export const SEARCH_CAPABILITIES_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle', - { - defaultMessage: 'Enhanced search capabilities', - } -); - -export const SEARCH_CAPABILITIES_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription', - { - defaultMessage: - 'It is now possible to search rules by index patterns, like "filebeat-*", or by MITRE ATT&CK™ tactics or techniques, like "Defense Evasion" or "TA0005".', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 8e44475a7992e..3b24dda539174 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -12,7 +12,6 @@ import { useKibana } from '../../../../../common/lib/kibana'; import { TestProviders } from '../../../../../common/mock'; import '../../../../../common/mock/formatted_relative'; import '../../../../../common/mock/match_media'; -import { RulesFeatureTourContextProvider } from './feature_tour/rules_feature_tour_context'; import { AllRules } from './index'; jest.mock('../../../../../common/components/link_to'); @@ -68,8 +67,7 @@ describe('AllRules', () => { rulesNotInstalled={0} rulesNotUpdated={0} /> - , - { wrappingComponent: RulesFeatureTourContextProvider } + ); await waitFor(() => { @@ -92,8 +90,7 @@ describe('AllRules', () => { rulesNotInstalled={0} rulesNotUpdated={0} /> - , - { wrappingComponent: RulesFeatureTourContextProvider } + ); await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx index 88b8e952d215a..816ffdfa9dad6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx @@ -10,16 +10,13 @@ import { mount } from 'enzyme'; import { RulesTableFilters } from './rules_table_filters'; import { TestProviders } from '../../../../../../common/mock'; -import { RulesFeatureTourContextProvider } from '../feature_tour/rules_feature_tour_context'; jest.mock('../rules_table/rules_table_context'); describe('RulesTableFilters', () => { it('renders no numbers next to rule type button filter if none exist', async () => { const wrapper = mount( - - - , + , { wrappingComponent: TestProviders } ); @@ -33,9 +30,7 @@ describe('RulesTableFilters', () => { it('renders number of custom and prepackaged rules', async () => { const wrapper = mount( - - - , + , { wrappingComponent: TestProviders } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index 7e3263f6bb26a..b4c81ae5a177d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -11,13 +11,11 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, - EuiTourStep, } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; import React, { useCallback } from 'react'; import styled from 'styled-components'; import * as i18n from '../../translations'; -import { useRulesFeatureTourContext } from '../feature_tour/rules_feature_tour_context'; import { useRulesTableContext } from '../rules_table/rules_table_context'; import { TagsFilterPopover } from './tags_filter_popover'; @@ -25,14 +23,6 @@ const FilterWrapper = styled(EuiFlexGroup)` margin-bottom: ${({ theme }) => theme.eui.euiSizeXS}; `; -const SearchBarWrapper = styled(EuiFlexItem)` - & .euiPopover__anchor { - // This is needed to "cancel" styles passed down from EuiTourStep that - // interfere with EuiFieldSearch and don't allow it to take the full width - display: block; - } -`; - interface RulesTableFiltersProps { rulesCustomInstalled: number | null; rulesInstalled: number | null; @@ -55,8 +45,6 @@ const RulesTableFiltersComponent = ({ const { showCustomRules, showElasticRules, tags: selectedTags } = filterOptions; - const { steps } = useRulesFeatureTourContext(); - const handleOnSearch = useCallback( (filterString) => setFilterOptions({ filter: filterString.trim() }), [setFilterOptions] @@ -81,17 +69,15 @@ const RulesTableFiltersComponent = ({ return ( - - - - - + + + { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); @@ -178,79 +177,77 @@ const RulesPageComponent: React.FC = () => { showCheckBox /> - - - - - - {loadPrebuiltRulesAndTemplatesButton && ( - {loadPrebuiltRulesAndTemplatesButton} - )} - {reloadPrebuiltRulesAndTemplatesButton && ( - {reloadPrebuiltRulesAndTemplatesButton} - )} - - - - {i18n.UPLOAD_VALUE_LISTS} - - - - + + + + + {loadPrebuiltRulesAndTemplatesButton && ( + {loadPrebuiltRulesAndTemplatesButton} + )} + {reloadPrebuiltRulesAndTemplatesButton && ( + {reloadPrebuiltRulesAndTemplatesButton} + )} + + - {i18n.IMPORT_RULE} + {i18n.UPLOAD_VALUE_LISTS} - - - - {i18n.ADD_NEW_RULE} - - - - - {(prePackagedRuleStatus === 'ruleNeedUpdate' || - prePackagedTimelineStatus === 'timelineNeedUpdate') && ( - - )} - + + + + {i18n.IMPORT_RULE} + + + + + {i18n.ADD_NEW_RULE} + + + + + {(prePackagedRuleStatus === 'ruleNeedUpdate' || + prePackagedTimelineStatus === 'timelineNeedUpdate') && ( + - - - + )} + + + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 09949cc5c1a09..f99ebc2c72c26 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -408,8 +408,7 @@ export const SEARCH_RULES = i18n.translate( export const SEARCH_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.searchPlaceholder', { - defaultMessage: - 'Rule name, index pattern (e.g., "filebeat-*"), or MITRE ATT&CK™ tactic or technique (e.g., "Defense Evasion" or "TA0005")', + defaultMessage: 'Search by rule name, index pattern, or MITRE ATT&CK tactic or technique', } ); From 0b26abb5a84f9bdd29a4192e29197bfbcf0aeeb2 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 29 Mar 2022 17:39:39 -0400 Subject: [PATCH 036/108] Security Solution: Improve client side perf: `getAllBrowserFields` (#128818) The `getAllBrowserFields` function is called a lot when loading the overview and alerts pages. This commit optimizes the function. --- .../source/__snapshots__/index.test.tsx.snap | 544 ++++++++++++++++++ .../common/containers/source/index.test.tsx | 7 +- .../public/common/containers/source/index.tsx | 17 +- 3 files changed, 559 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/containers/source/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/security_solution/public/common/containers/source/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/containers/source/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..3924f81c001ba --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/source/__snapshots__/index.test.tsx.snap @@ -0,0 +1,544 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`source/index.tsx getAllBrowserFields it returns an array of all fields in the BrowserFields argument 1`] = ` +Array [ + Object { + "aggregatable": true, + "category": "agent", + "description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but \`agent.id\` does not.", + "example": "8a4f500f", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.ephemeral_id", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "agent", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.hostname", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "agent", + "description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.", + "example": "8a4f500d", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.id", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "agent", + "description": "Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.", + "example": "foo", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.name", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a0", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a1", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a2", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "base", + "description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.", + "example": "2016-05-23T08:05:34.853Z", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "@timestamp", + "readFromDocValues": true, + "searchable": true, + "type": "date", + }, + Object { + "aggregatable": false, + "category": "base", + "description": "Each document has an _id that uniquely identifies it", + "example": "Y-6TfmcB0WOhS6qyMv3s", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "_id", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": false, + "category": "base", + "description": "For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.", + "example": "Hello World", + "format": "string", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "message", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "client", + "description": "Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.address", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "client", + "description": "Bytes sent from the client to the server.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.bytes", + "searchable": true, + "type": "number", + }, + Object { + "aggregatable": true, + "category": "client", + "description": "Client domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.domain", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "client", + "description": "Country ISO code.", + "example": "CA", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.geo.country_iso_code", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "cloud", + "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.", + "example": "666777888999", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.account.id", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "cloud", + "description": "Availability zone in which this host is running.", + "example": "us-east-1c", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.availability_zone", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "container", + "description": "Unique container id.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.id", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "container", + "description": "Name of the image the container was built on.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.name", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "container", + "description": "Container image tag.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.tag", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "destination", + "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.address", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "destination", + "description": "Bytes sent from the destination to the source.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.bytes", + "searchable": true, + "type": "number", + }, + Object { + "aggregatable": true, + "category": "destination", + "description": "Destination domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.domain", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "destination", + "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.ip", + "searchable": true, + "type": "ip", + }, + Object { + "aggregatable": true, + "category": "destination", + "description": "Port of the destination.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.port", + "searchable": true, + "type": "long", + }, + Object { + "aggregatable": true, + "category": "event", + "description": "event.end contains the date when the event ended or when the activity was last observed.", + "example": null, + "format": "", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.end", + "searchable": true, + "type": "date", + }, + Object { + "aggregatable": true, + "category": "event", + "description": "The action captured by the event. This describes the information in the event. It is more specific than \`event.category\`. Examples are \`group-add\`, \`process-started\`, \`file-created\`. The value is normally defined by the implementer.", + "example": "user-password-change", + "format": "string", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.action", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "event", + "description": "This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy. \`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. This field is an array. This will allow proper categorization of some events that fall in multiple categories.", + "example": "authentication", + "format": "string", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.category", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "event", + "description": "The numeric severity of the event according to your event source. What the different severity values mean can be different between sources and use cases. It's up to the implementer to make sure severities are consistent across events from the same source. The Syslog severity belongs in \`log.syslog.severity.code\`. \`event.severity\` is meant to represent the severity according to the event source (e.g. firewall, IDS). If the event source does not publish its own severity, you may optionally copy the \`log.syslog.severity.code\` to \`event.severity\`.", + "example": 7, + "format": "number", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.severity", + "searchable": true, + "type": "number", + }, + Object { + "aggregatable": true, + "category": "host", + "description": "Name of the host. It 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.", + "format": "string", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "host.name", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": true, + "category": "source", + "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.ip", + "searchable": true, + "type": "ip", + }, + Object { + "aggregatable": true, + "category": "source", + "description": "Port of the source.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.port", + "searchable": true, + "type": "long", + }, + Object { + "aggregatable": true, + "category": "user", + "description": "Short name or login of the user.", + "example": "albert", + "format": "string", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "user.name", + "searchable": true, + "type": "string", + }, + Object { + "aggregatable": false, + "category": "nestedField", + "description": "", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "nestedField.firstAttributes", + "searchable": true, + "subType": Object { + "nested": Object { + "path": "nestedField", + }, + }, + "type": "string", + }, + Object { + "aggregatable": false, + "category": "nestedField", + "description": "", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "nestedField.secondAttributes", + "searchable": true, + "subType": Object { + "nested": Object { + "path": "nestedField", + }, + }, + "type": "string", + }, +] +`; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index fd3f35059c6fb..8b1902cbc5e6b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -6,7 +6,7 @@ */ import { IndexField } from '../../../../common/search_strategy/index_fields'; -import { getBrowserFields } from '.'; +import { getBrowserFields, getAllBrowserFields } from '.'; import { IndexFieldSearch, useDataView } from './use_data_view'; import { mockBrowserFields, mocksSource } from './mock'; import { mockGlobalState, TestProviders } from '../../mock'; @@ -25,6 +25,11 @@ jest.mock('react-redux', () => { jest.mock('../../lib/kibana'); describe('source/index.tsx', () => { + describe('getAllBrowserFields', () => { + test('it returns an array of all fields in the BrowserFields argument', () => { + expect(getAllBrowserFields(mockBrowserFields)).toMatchSnapshot(); + }); + }); describe('getBrowserFields', () => { test('it returns an empty object given an empty array', () => { const fields = getBrowserFields('title 1', []); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index f833c2f8c4fc0..7cc37b5f7e6e3 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -26,14 +26,15 @@ import { useAppToasts } from '../../hooks/use_app_toasts'; export type { BrowserField, BrowserFields, DocValueFields }; -export const getAllBrowserFields = (browserFields: BrowserFields): Array> => - Object.values(browserFields).reduce>>( - (acc, namespace) => [ - ...acc, - ...Object.values(namespace.fields != null ? namespace.fields : {}), - ], - [] - ); +export function getAllBrowserFields(browserFields: BrowserFields): Array> { + const result: Array> = []; + for (const namespace of Object.values(browserFields)) { + if (namespace.fields) { + result.push(...Object.values(namespace.fields)); + } + } + return result; +} export const getAllFieldsByName = ( browserFields: BrowserFields From 71cd640fe2941ffd41521f9e6ac43629aa15b0bb Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Tue, 29 Mar 2022 17:41:50 -0400 Subject: [PATCH 037/108] [APM] Fixes editing JSON in Service map storybook example (#128792) (#128822) --- .../cytoscape_example_data.stories.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx index 24e2dceeafdc8..b3615c7fcc42f 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx @@ -129,18 +129,30 @@ export const MapFromJSON: Story<{}> = () => { const [error, setError] = useState(); const [elements, setElements] = useState([]); - useEffect(() => { + + const [uniqueKeyCounter, setUniqueKeyCounter] = useState(0); + const updateRenderedElements = () => { try { setElements(JSON.parse(json).elements); + setUniqueKeyCounter((key) => key + 1); + setError(undefined); } catch (e) { setError(e.message); } + }; + + useEffect(() => { + updateRenderedElements(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (
- + @@ -150,6 +162,10 @@ export const MapFromJSON: Story<{}> = () => { languageId="json" value={json} options={{ fontFamily: 'monospace' }} + onChange={(value) => { + setJson(value); + setSessionJson(value); + }} /> @@ -177,13 +193,7 @@ export const MapFromJSON: Story<{}> = () => { { - try { - setElements(JSON.parse(json).elements); - setSessionJson(json); - setError(undefined); - } catch (e) { - setError(e.message); - } + updateRenderedElements(); }} > Render JSON From f782f8bf338e7f50974bfc2ed5b0b23b7a1bc4bf Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 29 Mar 2022 14:52:15 -0700 Subject: [PATCH 038/108] Revert "Upgrade EUI to v52.2.0 (#128313)" This reverts commit dccd8290bbfabc245d759e0873bb2cd9ea69c70e. --- package.json | 2 +- .../header/__snapshots__/header.test.tsx.snap | 5 +- src/dev/license_checker/config.ts | 2 +- .../url/__snapshots__/url.test.tsx.snap | 10 ++- .../table/__snapshots__/table.test.tsx.snap | 5 +- .../__snapshots__/table_header.test.tsx.snap | 5 +- .../__snapshots__/field_name.test.tsx.snap | 20 +++--- .../not_found_errors.test.tsx.snap | 20 +++--- .../components/not_found_errors.test.tsx | 8 +-- .../link_preview.test.tsx | 6 +- .../custom_link_toolbar.test.tsx | 4 +- .../dropdown_filter.stories.storyshot | 20 ++---- .../time_filter.stories.storyshot | 60 +++++------------ .../extended_template.stories.storyshot | 8 +-- .../date_format.stories.storyshot | 12 +--- .../number_format.stories.storyshot | 12 +--- .../__snapshots__/asset.stories.storyshot | 32 +++------- .../asset_manager.stories.storyshot | 52 ++++----------- .../__snapshots__/color_dot.stories.storyshot | 16 ++--- .../color_manager.stories.storyshot | 32 +++------- .../color_palette.stories.storyshot | 12 +--- .../color_picker.stories.storyshot | 40 +++--------- .../custom_element_modal.stories.storyshot | 44 ++++--------- .../datasource_component.stories.storyshot | 16 ++--- .../element_card.stories.storyshot | 12 +--- .../file_upload.stories.storyshot | 4 +- .../home/__snapshots__/home.stories.storyshot | 4 +- .../empty_prompt.stories.storyshot | 4 +- .../workpad_table.stories.storyshot | 52 ++++----------- .../__snapshots__/item_grid.stories.storyshot | 24 ++----- .../element_controls.stories.storyshot | 8 +-- .../element_grid.stories.storyshot | 24 ++----- .../saved_elements_modal.stories.storyshot | 64 +++++-------------- .../sidebar_header.stories.storyshot | 16 ++--- .../__snapshots__/tag.stories.storyshot | 8 +-- .../__snapshots__/tag_list.stories.storyshot | 12 +--- .../text_style_picker.stories.storyshot | 56 ++++------------ .../delete_var.stories.storyshot | 8 +-- .../__snapshots__/edit_var.stories.storyshot | 44 ++++--------- .../var_config.stories.storyshot | 8 +-- .../filters_group.component.stories.storyshot | 8 +-- ...orkpad_filters.component.stories.storyshot | 40 +++--------- .../editor_menu.stories.storyshot | 8 +-- .../element_menu.stories.storyshot | 4 +- .../extended_template.stories.storyshot | 24 ++----- .../extended_template.stories.storyshot | 16 ++--- .../simple_template.stories.storyshot | 5 +- .../__snapshots__/canvas.stories.storyshot | 48 ++++---------- .../__snapshots__/footer.stories.storyshot | 32 +++------- .../page_controls.stories.storyshot | 24 ++----- .../__snapshots__/title.stories.storyshot | 12 +--- .../autoplay_settings.stories.storyshot | 24 ++----- .../__snapshots__/settings.stories.storyshot | 8 +-- .../toolbar_settings.stories.storyshot | 24 ++----- .../connectors_dropdown.test.tsx | 2 +- .../markdown_editor/renderer.test.tsx | 2 +- .../components/table/table.test.tsx | 2 +- .../index_setup_dataset_filter.tsx | 1 + .../__jest__/test_pipeline.test.tsx | 14 ++-- .../components/table_basic.test.tsx | 4 +- .../field_item.test.tsx | 5 +- .../request_trial_extension.test.js.snap | 8 +-- .../revert_to_basic.test.js.snap | 6 +- .../__snapshots__/start_trial.test.js.snap | 8 +-- .../__snapshots__/exporters.test.js.snap | 15 ++--- .../__snapshots__/reason_found.test.js.snap | 15 ++--- .../list/remote_clusters_list.test.js | 6 +- .../feature_table_cell.test.tsx | 2 +- .../privilege_summary/__fixtures__/index.ts | 2 +- .../exception_item/exception_entries.test.tsx | 2 +- .../common/components/inspect/modal.test.tsx | 6 +- .../common/components/links/index.test.tsx | 10 +-- .../markdown_editor/renderer.test.tsx | 2 +- .../rules/all/optional_eui_tour_step.tsx | 28 -------- .../network/components/port/index.test.tsx | 6 +- .../source_destination/index.test.tsx | 4 +- .../source_destination_ip.test.tsx | 8 +-- .../certificate_fingerprint/index.test.tsx | 2 +- .../components/ja3_fingerprint/index.test.tsx | 2 +- .../components/netflow/index.test.tsx | 10 +-- .../__snapshots__/index.test.tsx.snap | 5 +- .../body/renderers/get_row_renderer.test.tsx | 12 +--- .../suricata/suricata_details.test.tsx | 6 +- .../suricata/suricata_row_renderer.test.tsx | 7 +- .../system/generic_row_renderer.test.tsx | 26 +++----- .../body/renderers/zeek/zeek_details.test.tsx | 16 ++--- .../renderers/zeek/zeek_row_renderer.test.tsx | 7 +- .../renderers/zeek/zeek_signature.test.tsx | 6 +- .../__jest__/client_integration/home.test.ts | 2 +- .../public/components/inspect/modal.test.tsx | 6 +- .../components/health_check.test.tsx | 14 ++-- .../rule_details/components/rule.test.tsx | 9 +-- .../drilldown_table/drilldown_table.test.tsx | 2 +- .../__snapshots__/expanded_row.test.tsx.snap | 5 +- .../monitor_status.bar.test.tsx.snap | 5 +- .../network_requests_total.test.tsx | 4 +- .../components/waterfall_marker_icon.test.tsx | 4 +- .../ux_metrics/key_ux_metrics.test.tsx | 12 ++-- yarn.lock | 8 +-- 99 files changed, 406 insertions(+), 990 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/optional_eui_tour_step.tsx diff --git a/package.json b/package.json index adc45d9afadd7..f70bde1a2108f 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.1", "@elastic/ems-client": "8.2.0", - "@elastic/eui": "52.2.0", + "@elastic/eui": "51.1.0", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index ad0f27bbf08ce..d2b1078641437 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -3108,13 +3108,12 @@ exports[`Header renders 1`] = ` type="logoElastic" > - Elastic Logo - + /> - External link - + /> @@ -244,11 +243,10 @@ exports[`UrlFormatEditor should render normally 1`] = ` Label template help - External link - + /> diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap index c054b42f51ac7..2b6cf62baf221 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -320,11 +320,10 @@ exports[`Table should render the boolean template (false) 1`] = ``; exports[`Table should render the boolean template (true) 1`] = ` - Is searchable - +/> `; exports[`Table should render timestamp field name 1`] = ` diff --git a/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap index 612973fe37a48..3f72349f3e2a0 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap +++ b/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap @@ -20,11 +20,10 @@ exports[`TableHeader with time column renders correctly 1`] = ` class="euiToolTipAnchor" > - Primary time field. - + /> - Geo point field - + />
,
- Number field - + />
,
- String field - + />
,
- Unknown field - + />
,
- External link - + /> - External link - + /> - External link - + /> - External link - + /> { const callOut = mounted.find('EuiCallOut'); expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectThe saved search associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIsExternal link(opens in a new tab or window) to fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectThe saved search associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); @@ -34,7 +34,7 @@ describe('NotFoundErrors component', () => { const callOut = mounted.find('EuiCallOut'); expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectThe data view associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIsExternal link(opens in a new tab or window) to fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectThe data view associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); @@ -43,7 +43,7 @@ describe('NotFoundErrors component', () => { const callOut = mounted.find('EuiCallOut'); expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectA field associated with this object no longer exists in the data view.If you know what this error means, you can use the Saved objects APIsExternal link(opens in a new tab or window) to fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectA field associated with this object no longer exists in the data view.If you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); @@ -52,7 +52,7 @@ describe('NotFoundErrors component', () => { const callOut = mounted.find('EuiCallOut'); expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectIf you know what this error means, you can use the Saved objects APIsExternal link(opens in a new tab or window) to fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectIf you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); }); diff --git a/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx index 4c8a5bc00285e..f44b4d1c1205d 100644 --- a/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx @@ -56,7 +56,7 @@ describe('LinkPreview', () => { removeExternalLinkText( (getByTestId(container, 'preview-link') as HTMLAnchorElement).text ) - ).toContain('https://baz.co'); + ).toEqual('https://baz.co'); }); }); @@ -74,7 +74,7 @@ describe('LinkPreview', () => { removeExternalLinkText( (getByTestId(container, 'preview-link') as HTMLAnchorElement).text ) - ).toContain('https://baz.co?service.name={{invalid}'); + ).toEqual('https://baz.co?service.name={{invalid}'); expect(getByTestId(container, 'preview-warning')).toBeInTheDocument(); }); }); @@ -94,7 +94,7 @@ describe('LinkPreview', () => { removeExternalLinkText( (getByTestId(container, 'preview-link') as HTMLAnchorElement).text ) - ).toContain('https://baz.co?transaction=0'); + ).toEqual('https://baz.co?transaction=0'); }); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx index 42ca08cc3d225..4d92f5a1ae34a 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx @@ -45,7 +45,7 @@ describe('CustomLinkToolbar', () => { wrapper: Wrapper, }); expect( - component.getByText('Custom links settings page') + component.getByLabelText('Custom links settings page') ).toBeInTheDocument(); expectTextsInDocument(component, ['Create']); }); @@ -56,7 +56,7 @@ describe('CustomLinkToolbar', () => { { wrapper: Wrapper } ); expect( - component.getByText('Custom links settings page') + component.getByLabelText('Custom links settings page') ).toBeInTheDocument(); expectTextsNotInDocument(component, ['Create']); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot index 0ded42439fb95..52694d3b04089 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot @@ -35,9 +35,7 @@ exports[`Storyshots renderers/DropdownFilter default 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + />
@@ -98,9 +96,7 @@ exports[`Storyshots renderers/DropdownFilter with choices 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - -
+ />
@@ -161,9 +157,7 @@ exports[`Storyshots renderers/DropdownFilter with choices and new value 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - -
+ />
@@ -224,9 +218,7 @@ exports[`Storyshots renderers/DropdownFilter with choices and value 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - -
+ />
@@ -269,9 +261,7 @@ exports[`Storyshots renderers/DropdownFilter with new value 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - -
+ />
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot index e82b6bf082b05..5abd1e9fd05b6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot @@ -42,17 +42,13 @@ exports[`Storyshots renderers/TimeFilter default 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - > - -
+ /> - - + /> @@ -95,9 +91,7 @@ exports[`Storyshots renderers/TimeFilter default 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - > - - + /> @@ -156,17 +150,13 @@ exports[`Storyshots renderers/TimeFilter with absolute time bounds 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - > - - + /> - - + /> @@ -245,9 +235,7 @@ exports[`Storyshots renderers/TimeFilter with absolute time bounds 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - > - - + /> @@ -306,17 +294,13 @@ exports[`Storyshots renderers/TimeFilter with commonlyUsedRanges 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - > - - + /> - - + /> @@ -359,9 +343,7 @@ exports[`Storyshots renderers/TimeFilter with commonlyUsedRanges 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - > - - + /> @@ -420,17 +402,13 @@ exports[`Storyshots renderers/TimeFilter with dateFormat 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - > - - + /> - - + /> @@ -473,9 +451,7 @@ exports[`Storyshots renderers/TimeFilter with dateFormat 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - > - - + /> @@ -534,17 +510,13 @@ exports[`Storyshots renderers/TimeFilter with relative time bounds 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - > - - + /> - - + /> @@ -623,9 +595,7 @@ exports[`Storyshots renderers/TimeFilter with relative time bounds 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot index 7c0a2ad18c3dc..e3badfa833090 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot @@ -65,9 +65,7 @@ exports[`Storyshots arguments/AxisConfig extended 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + /> @@ -145,9 +143,7 @@ exports[`Storyshots arguments/AxisConfig/components extended 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + /> diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot index 9755e1b53b868..238fe7c259c6e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot @@ -47,9 +47,7 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + /> @@ -122,9 +120,7 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + /> @@ -196,9 +192,7 @@ exports[`Storyshots arguments/DateFormat with preset format 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + /> diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot index ecd8e53ce1d25..2159e49e2bcf1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot @@ -57,9 +57,7 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + /> @@ -142,9 +140,7 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + /> @@ -226,9 +222,7 @@ exports[`Storyshots arguments/NumberFormat with preset format 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot index 587b07ca4f932..6db24bd0b984c 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot @@ -81,9 +81,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - > - - + /> @@ -113,9 +111,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - > - - + /> @@ -146,9 +142,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - > - - + /> @@ -175,9 +169,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> @@ -268,9 +260,7 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - > - - + /> @@ -300,9 +290,7 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - > - - + /> @@ -333,9 +321,7 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - > - - + /> @@ -362,9 +348,7 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot index 5409f9c444df0..dd650e9f4c697 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot @@ -26,9 +26,7 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - > - - + />
- - + />
@@ -126,9 +122,7 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` color="subdued" data-euiicon-type="importAction" size="xxl" - > - - + />
- - + />
- - + />
@@ -390,9 +380,7 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - > - - + />
@@ -422,9 +410,7 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - > - - + />
@@ -455,9 +441,7 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - > - - + />
@@ -484,9 +468,7 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + />
@@ -566,9 +548,7 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - > - - + /> @@ -598,9 +578,7 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - > - - + /> @@ -631,9 +609,7 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - > - - + /> @@ -660,9 +636,7 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot b/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot index 5d83b2718f916..056b87294f245 100644 --- a/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot @@ -129,9 +129,7 @@ Array [ - - + /> ,
- - + />
,
- - + />
,
- - + />
, ] diff --git a/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot index 057bd37b71c20..cb3598430c7ef 100644 --- a/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot @@ -394,9 +394,7 @@ exports[`Storyshots components/Color/ColorManager interactive 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - > - - + /> @@ -809,9 +805,7 @@ Array [ color="inherit" data-euiicon-type="plusInCircle" size="m" - > - - + /> , @@ -894,9 +886,7 @@ Array [ color="inherit" data-euiicon-type="plusInCircle" size="m" - > - - + /> , @@ -977,9 +965,7 @@ Array [ color="inherit" data-euiicon-type="plusInCircle" size="m" - > - - + /> , diff --git a/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot b/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot index 53651c8fe33f2..a0d27eafb23dc 100644 --- a/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot @@ -393,9 +393,7 @@ Array [ className="selected-color" color="#333" data-euiicon-type="check" - > - - + /> @@ -760,9 +758,7 @@ exports[`Storyshots components/Color/ColorPalette six colors, wrap at 4 1`] = ` className="selected-color" color="#333" data-euiicon-type="check" - > - - + /> @@ -1044,9 +1040,7 @@ Array [ className="selected-color" color="#333" data-euiicon-type="check" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot index 557f94c26fac9..6ef3eec47e701 100644 --- a/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot @@ -237,9 +237,7 @@ exports[`Storyshots components/Color/ColorPicker interactive 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - > - - + /> @@ -322,9 +318,7 @@ exports[`Storyshots components/Color/ColorPicker six colors 1`] = ` className="selected-color" color="#333" data-euiicon-type="check" - > - - + /> @@ -532,9 +526,7 @@ exports[`Storyshots components/Color/ColorPicker six colors 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - > - - + /> @@ -796,9 +786,7 @@ exports[`Storyshots components/Color/ColorPicker six colors, value missing 1`] = color="inherit" data-euiicon-type="plusInCircle" size="m" - > - - + /> @@ -860,9 +846,7 @@ exports[`Storyshots components/Color/ColorPicker three colors 1`] = ` className="selected-color" color="#333" data-euiicon-type="check" - > - - + /> @@ -986,9 +970,7 @@ exports[`Storyshots components/Color/ColorPicker three colors 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot index feb04e68ca1d3..d8c660923e3d7 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot @@ -27,9 +27,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] color="inherit" data-euiicon-type="cross" size="m" - > - - + />
- - + />
@@ -228,9 +224,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - > - - + />
- - + />
- - + />
@@ -656,9 +646,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - > - - + />
- - + />
@@ -857,9 +843,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - > - - + />
- - + />
- - + />
@@ -1166,9 +1146,7 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = ` className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - > - - + />
- - + /> - - + /> Test Datasource @@ -74,18 +70,14 @@ exports[`Storyshots components/datasource/DatasourceComponent simple datasource color="inherit" data-euiicon-type="arrowRight" size="m" - > - - + /> - - + /> Test Datasource diff --git a/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot b/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot index 05cec59522ae7..14640fe266839 100644 --- a/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot @@ -19,9 +19,7 @@ exports[`Storyshots components/Elements/ElementCard with click handler 1`] = ` className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - > - - + />
- - + />
- - + />
- - + />
diff --git a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot index 0863fd13af607..d3ab369dcc32c 100644 --- a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot @@ -59,9 +59,7 @@ exports[`Storyshots Home Home Page 1`] = ` color="inherit" data-euiicon-type="plusInCircleFilled" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot index fa3789124ce81..8f00060a1dd1c 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot @@ -28,9 +28,7 @@ exports[`Storyshots Home/Components/Empty Prompt Empty Prompt 1`] = ` color="subdued" data-euiicon-type="importAction" size="xxl" - > - - + />
- - + />
@@ -75,9 +73,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiFilePicker__icon" data-euiicon-type="importAction" size="m" - > - - + />
@@ -154,9 +150,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - > - - + /> @@ -302,9 +296,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiTableSortIcon" data-euiicon-type="sortDown" size="m" - > - - + /> @@ -468,9 +460,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="exportAction" size="m" - > - - + />
@@ -496,9 +486,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="copy" size="m" - > - - + />
@@ -644,9 +632,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="exportAction" size="m" - > - - + />
@@ -672,9 +658,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="copy" size="m" - > - - + />
@@ -820,9 +804,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="exportAction" size="m" - > - - + />
@@ -848,9 +830,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="copy" size="m" - > - - + />
@@ -893,9 +873,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - > - - + /> @@ -937,9 +915,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - > - - + />
    - - + />
diff --git a/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot index e96302525aea4..dbb591582e909 100644 --- a/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot @@ -73,9 +73,7 @@ exports[`Storyshots components/ItemGrid complex grid 1`] = ` - - + />
- - + />
- - + />
@@ -131,19 +125,13 @@ exports[`Storyshots components/ItemGrid icon grid 1`] = ` > - - + /> - - + /> - - + /> `; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot index 9f462d9a4d6cd..6f139df7c8773 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot @@ -34,9 +34,7 @@ exports[`Storyshots components/SavedElementsModal/ElementControls has two button color="inherit" data-euiicon-type="pencil" size="m" - > - - + /> @@ -63,9 +61,7 @@ exports[`Storyshots components/SavedElementsModal/ElementControls has two button color="inherit" data-euiicon-type="trash" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot index fbab31e5c8c5b..70ee9f543d768 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot @@ -85,9 +85,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - > - - + /> @@ -114,9 +112,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> @@ -196,9 +192,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - > - - + /> @@ -225,9 +219,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> @@ -307,9 +299,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - > - - + /> @@ -336,9 +326,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot index e0b7f40657cf8..fd6f29178aa91 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot @@ -26,9 +26,7 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - > - - + />
- - + />
@@ -103,9 +99,7 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` color="subdued" data-euiicon-type="vector" size="xxl" - > - - + />
- - + />
- - + />
@@ -337,9 +327,7 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - > - - + /> @@ -366,9 +354,7 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> @@ -448,9 +434,7 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - > - - + /> @@ -477,9 +461,7 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> @@ -559,9 +541,7 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - > - - + /> @@ -588,9 +568,7 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> @@ -656,9 +634,7 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - > - - + />
- - + />
- - + />
@@ -815,9 +787,7 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - > - - + /> @@ -844,9 +814,7 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot index d5e5af856909b..6bf2535131afc 100644 --- a/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot @@ -72,9 +72,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="sortUp" size="m" - > - - + /> @@ -100,9 +98,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="arrowUp" size="m" - > - - + /> @@ -128,9 +124,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="m" - > - - + /> @@ -156,9 +150,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot index 2a1e12c1e0b74..f21ffcf1a70ea 100644 --- a/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot @@ -57,9 +57,7 @@ exports[`Storyshots components/Tags/Tag as health 1`] = ` - - + />
- - + />
- - + />
- - + />
- - + />
- - + />
@@ -246,9 +244,7 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorBold" size="m" - > - - + /> @@ -278,9 +274,7 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorItalic" size="m" - > - - + /> @@ -310,9 +304,7 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorUnderline" size="m" - > - - + /> @@ -356,9 +348,7 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorAlignLeft" size="m" - > - - + /> @@ -394,9 +384,7 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorAlignCenter" size="m" - > - - + /> @@ -432,9 +420,7 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorAlignRight" size="m" - > - - + /> @@ -612,9 +598,7 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - > - - + /> @@ -706,9 +690,7 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorBold" size="m" - > - - + /> @@ -738,9 +720,7 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorItalic" size="m" - > - - + /> @@ -770,9 +750,7 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorUnderline" size="m" - > - - + /> @@ -816,9 +794,7 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorAlignLeft" size="m" - > - - + /> @@ -854,9 +830,7 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorAlignCenter" size="m" - > - - + /> @@ -892,9 +866,7 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorAlignRight" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot index 0d8a5c0cf4e5d..f5351b0d8ea5f 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot @@ -20,9 +20,7 @@ Array [ "verticalAlign": "top", } } - > - - + /> - - + /> diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot index 72e1b4d6ef909..6c70364f9679c 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot @@ -20,9 +20,7 @@ Array [ "verticalAlign": "top", } } - > - - + /> - - + /> @@ -259,9 +255,7 @@ Array [ color="inherit" data-euiicon-type="save" size="m" - > - - + /> @@ -316,9 +310,7 @@ Array [ "verticalAlign": "top", } } - > - - + /> - - + /> @@ -496,9 +486,7 @@ Array [ color="inherit" data-euiicon-type="save" size="m" - > - - + /> @@ -553,9 +541,7 @@ Array [ "verticalAlign": "top", } } - > - - + /> - - + /> @@ -733,9 +717,7 @@ Array [ color="inherit" data-euiicon-type="save" size="m" - > - - + /> @@ -790,9 +772,7 @@ Array [ "verticalAlign": "top", } } - > - - + /> - - + /> diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot index ac27b0443585a..7d43840e431ab 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot @@ -28,9 +28,7 @@ exports[`Storyshots components/Variables/VarConfig default 1`] = ` color="inherit" data-euiicon-type="arrowRight" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot index 57fbd4c2109cd..b6d842ac44e21 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot @@ -33,9 +33,7 @@ exports[`Storyshots components/WorkpadFilters/FiltersGroupComponent default 1`] color="inherit" data-euiicon-type="arrowRight" size="m" - > - - + />
- - + />
@@ -1473,9 +1467,7 @@ exports[`Storyshots shareables/Canvas component 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - > - - + /> @@ -2817,9 +2809,7 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` - - + />
- - + />
- - + />
@@ -2963,9 +2949,7 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - > - - + /> @@ -3123,9 +3107,7 @@ exports[`Storyshots shareables/Canvas contextual: hello 1`] = ` - - + />
- - + />
- - + />
@@ -3269,9 +3247,7 @@ exports[`Storyshots shareables/Canvas contextual: hello 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot index 6a8d67a70ad1a..90ebc1900d731 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot @@ -1280,9 +1280,7 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` - - + />
- - + />
- - + />
@@ -1426,9 +1420,7 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - > - - + /> @@ -1540,9 +1532,7 @@ exports[`Storyshots shareables/Footer contextual: hello 1`] = ` - - + />
- - + />
- - + />
@@ -1686,9 +1672,7 @@ exports[`Storyshots shareables/Footer contextual: hello 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot index f2b92754b6d6f..9edb6f1fda62f 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot @@ -34,9 +34,7 @@ exports[`Storyshots shareables/Footer/PageControls component 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - > - - + />
- - + />
@@ -135,9 +131,7 @@ exports[`Storyshots shareables/Footer/PageControls contextual: austin 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - > - - + />
- - + />
@@ -236,9 +228,7 @@ exports[`Storyshots shareables/Footer/PageControls contextual: hello 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - > - - + />
- - + />
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot index ea19100f6da87..2b326fd0ec51a 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot @@ -34,9 +34,7 @@ exports[`Storyshots shareables/Footer/Title component 1`] = ` - - + />
- - + />
- - + />
- - + /> - - + /> @@ -216,16 +212,12 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: on, 5 className="euiSwitch__icon" data-euiicon-type="cross" size="m" - > - - + /> - - + /> @@ -384,16 +376,12 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings contextual 1`] = className="euiSwitch__icon" data-euiicon-type="cross" size="m" - > - - + /> - - + /> diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot index 3c3f26bce7e9e..265cbe460607d 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot @@ -39,9 +39,7 @@ exports[`Storyshots shareables/Footer/Settings component 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - > - - + />
@@ -89,9 +87,7 @@ exports[`Storyshots shareables/Footer/Settings contextual 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - > - - + /> diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot index d07e5a9edc8ad..1aafb9cc6b664 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot @@ -59,16 +59,12 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: off 1` className="euiSwitch__icon" data-euiicon-type="cross" size="m" - > - - + /> - - + /> @@ -151,16 +147,12 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: on 1`] className="euiSwitch__icon" data-euiicon-type="cross" size="m" - > - - + /> - - + /> @@ -243,16 +235,12 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings contextual 1`] = className="euiSwitch__icon" data-euiicon-type="cross" size="m" - > - - + /> - - + /> diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index 63fc2e2695a3a..4fd56525541a6 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -264,7 +264,7 @@ describe('ConnectorsDropdown', () => { wrapper: ({ children }) => {children}, }); - const tooltips = screen.getAllByText( + const tooltips = screen.getAllByLabelText( 'This connector is deprecated. Update it, or create a new one.' ); expect(tooltips[0]).toBeInTheDocument(); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx index 8cb8b7f23b439..af803cfc14e05 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx @@ -25,7 +25,7 @@ describe('Markdown', () => { test('it renders the expected link text', () => { const result = appMockRender.render({markdownWithLink}); - expect(removeExternalLinkText(result.getByTestId('markdown-link').textContent)).toContain( + expect(removeExternalLinkText(result.getByTestId('markdown-link').textContent)).toEqual( 'External Site' ); }); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx index eb1f82cc01e37..863e5e85d9ef3 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -138,7 +138,7 @@ describe('Background Search Session Management Table', () => { expect(table.find('tbody td').map((node) => node.text())).toMatchInlineSnapshot(` Array [ "App", - "Namevery background search Info", + "Namevery background search ", "# Searches0", "StatusExpired", "Created2 Dec, 2020, 00:19:32", diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx index d44625b1641ac..0626a946f8848 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx @@ -65,6 +65,7 @@ export const IndexSetupDatasetFilter: React.FC<{ isSelected={isVisible} onClick={show} iconType="arrowDown" + size="s" > { // Dropdown should be visible and processor status should equal "success" expect(exists('documentsDropdown')).toBe(true); - const initialProcessorStatusLabel = find('processors>0.processorStatusIcon').props() - .children; + const initialProcessorStatusLabel = find('processors>0.processorStatusIcon').props()[ + 'aria-label' + ]; expect(initialProcessorStatusLabel).toEqual('Success'); // Open flyout and click clear all button @@ -319,8 +320,9 @@ describe('Test pipeline', () => { // Verify documents and processors were reset expect(exists('documentsDropdown')).toBe(false); expect(exists('addDocumentsButton')).toBe(true); - const resetProcessorStatusIconLabel = find('processors>0.processorStatusIcon').props() - .children; + const resetProcessorStatusIconLabel = find('processors>0.processorStatusIcon').props()[ + 'aria-label' + ]; expect(resetProcessorStatusIconLabel).toEqual('Not run'); }); }); @@ -330,7 +332,7 @@ describe('Test pipeline', () => { it('should show "inactive" processor status by default', async () => { const { find } = testBed; - const statusIconLabel = find('processors>0.processorStatusIcon').props().children; + const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; expect(statusIconLabel).toEqual('Not run'); }); @@ -350,7 +352,7 @@ describe('Test pipeline', () => { actions.closeTestPipelineFlyout(); // Verify status - const statusIconLabel = find('processors>0.processorStatusIcon').props().children; + const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; expect(statusIconLabel).toEqual('Success'); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 2ad20bf0a43e2..36fd1581cb9b6 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -617,9 +617,7 @@ describe('DatatableComponent', () => { wrapper.setProps({ data: newData }); wrapper.update(); - // Using .toContain over .toEqual because this element includes text from - // which can't be seen, but shows in the text content - expect(wrapper.find('[data-test-subj="dataGridHeader"]').children().first().text()).toContain( + expect(wrapper.find('[data-test-subj="dataGridHeader"]').children().first().text()).toEqual( 'new a' ); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index c20f1c37c6c67..c86fdcc33b15f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -120,10 +120,7 @@ describe('IndexPattern Field Item', () => { it('should display displayName of a field', () => { const wrapper = mountWithIntl(); - - // Using .toContain over .toEqual because this element includes text from - // which can't be seen, but shows in the text content - expect(wrapper.find('[data-test-subj="lnsFieldListPanelField"]').first().text()).toContain( + expect(wrapper.find('[data-test-subj="lnsFieldListPanelField"]').first().text()).toEqual( 'bytesLabel' ); }); diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index 0fd589e4886e3..fda479f2888ce 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap index cf977731ee452..4fa45c4bec5ce 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription featuresExternal link(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription featuresExternal link(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription featuresExternal link(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap index 0880eddcc1683..622bff86ead16 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap index 41501a7eedb62..c5b5e5e65ab38 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap @@ -87,11 +87,10 @@ Array [ > Elasticsearch Service Console - External link - + /> @@ -107,11 +106,10 @@ Array [ > Logs and metrics - External link - + /> @@ -127,11 +125,10 @@ Array [ > the documentation page. - External link - + /> diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap index faab608e7af14..dda853a28239f 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap @@ -158,11 +158,10 @@ Array [ > Elasticsearch Service Console - External link - + /> @@ -178,11 +177,10 @@ Array [ > Logs and metrics - External link - + /> @@ -198,11 +196,10 @@ Array [ > the documentation page. - External link - + /> diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js index 26af30ba17c04..a6987fa19d1ee 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js @@ -252,7 +252,7 @@ describe('', () => { ], [ '', - remoteCluster2.name.concat('Info'), //Tests include the word "info" to account for the rendered text coming from EuiIcon + remoteCluster2.name, 'Not connected', PROXY_MODE, remoteCluster2.proxyAddress, @@ -261,7 +261,7 @@ describe('', () => { ], [ '', - remoteCluster3.name.concat('Info'), //Tests include the word "info" to account for the rendered text coming from EuiIcon + remoteCluster3.name, 'Not connected', PROXY_MODE, remoteCluster2.proxyAddress, @@ -360,7 +360,7 @@ describe('', () => { ({ rows } = table.getMetaData('remoteClusterListTable')); expect(rows.length).toBe(2); - expect(rows[0].columns[1].value).toContain(remoteCluster2.name); + expect(rows[0].columns[1].value).toEqual(remoteCluster2.name); }); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx index 006ae053940d8..7052f724cd1cc 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx @@ -40,7 +40,7 @@ describe('FeatureTableCell', () => { ); - expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature Info"`); + expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature "`); expect(wrapper.find(EuiIconTip).props().content).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts index f375263c960c3..3a70ff5713bd9 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts @@ -60,7 +60,7 @@ export function getDisplayedFeaturePrivileges( acc[feature.id][key] = { ...acc[feature.id][key], - primaryFeaturePrivilege: primary.text().replaceAll('Info', '').trim(), // Removing the word "info" to account for the rendered text coming from EuiIcon + primaryFeaturePrivilege: primary.text().trim(), hasCustomizedSubFeaturePrivileges: findTestSubject(primary, 'additionalPrivilegesGranted').length > 0, }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx index a53be08380698..6070924523f63 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx @@ -158,7 +158,7 @@ describe('ExceptionEntries', () => { expect(parentValue.text()).toEqual(getEmptyValue()); expect(nestedField.exists('.euiToolTipAnchor')).toBeTruthy(); - expect(nestedField.text()).toContain('host.name'); + expect(nestedField.text()).toEqual('host.name'); expect(nestedOperator.text()).toEqual('is'); expect(nestedValue.text()).toEqual('some name'); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx index 9796ae2624a73..7a9c36a986afd 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx @@ -58,7 +58,7 @@ describe('Modal Inspect', () => { expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="index-pattern-title"]').text() - ).toContain('Index pattern '); + ).toBe('Index pattern '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="index-pattern-description"]') @@ -66,7 +66,7 @@ describe('Modal Inspect', () => { ).toBe('auditbeat-*, filebeat-*, packetbeat-*, winlogbeat-*'); expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="query-time-title"]').text() - ).toContain('Query time '); + ).toBe('Query time '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="query-time-description"]') @@ -76,7 +76,7 @@ describe('Modal Inspect', () => { wrapper .find('.euiDescriptionList__title span[data-test-subj="request-timestamp-title"]') .text() - ).toContain('Request timestamp '); + ).toBe('Request timestamp '); }); test('Click on request Tab', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index adab4db904d6a..97f93b9732c02 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -105,7 +105,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders props passed in as link', () => { @@ -463,7 +463,7 @@ describe('Custom Links', () => { describe('WhoisLink', () => { test('it renders ip passed in as domain', () => { const wrapper = mountWithIntl({'Example Link'}); - expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders correct href', () => { @@ -488,7 +488,7 @@ describe('Custom Links', () => { {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders correct href', () => { @@ -519,7 +519,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders correct href', () => { @@ -548,7 +548,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders correct href when port is a number', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx index 68588c9338b4c..da3785648de62 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx @@ -20,7 +20,7 @@ describe('Markdown', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="markdown-link"]').first().text()) - ).toContain('External Site'); + ).toEqual('External Site'); }); test('it renders the expected href', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/optional_eui_tour_step.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/optional_eui_tour_step.tsx deleted file mode 100644 index e08389ba250a3..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/optional_eui_tour_step.tsx +++ /dev/null @@ -1,28 +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, { FC } from 'react'; - -import { EuiTourStepProps, EuiTourStep, DistributiveOmit } from '@elastic/eui'; - -/** - * This component can be used for tour steps, when tour step is optional - * If stepProps are not supplied, step will not be rendered, only children component will be - */ -export const OptionalEuiTourStep: FC<{ - stepProps: DistributiveOmit | undefined; -}> = ({ children, stepProps }) => { - if (!stepProps) { - return <>{children}; - } - - return ( - - <>{children} - - ); -}; diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx index ec56dd6934463..480d200c6756f 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx @@ -54,9 +54,9 @@ describe('Port', () => { ); - expect( - removeExternalLinkText(wrapper.find('[data-test-subj="port"]').first().text()) - ).toContain('443'); + expect(removeExternalLinkText(wrapper.find('[data-test-subj="port"]').first().text())).toEqual( + '443' + ); }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx index bb8b4683c9d30..3332111d14f8b 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx @@ -205,7 +205,7 @@ describe('SourceDestination', () => { removeExternalLinkText( wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() ) - ).toContain('10.1.2.3:80'); + ).toEqual('10.1.2.3:80'); }); test('it renders destination.packets', () => { @@ -329,7 +329,7 @@ describe('SourceDestination', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) - ).toContain('192.168.1.2:9987'); + ).toEqual('192.168.1.2:9987'); }); test('it renders source.packets', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx index 6168d98765253..f16cd7dbb109f 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx @@ -984,7 +984,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() ) - ).toContain('9987'); + ).toEqual('9987'); }); test('it renders the expected destination port when type is `destination`, and both destinationIp and destinationPort are populated', () => { @@ -1038,7 +1038,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() ) - ).toContain('80'); + ).toEqual('80'); }); test('it renders the expected source port when type is `source`, but only sourcePort is populated', () => { @@ -1092,7 +1092,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() ) - ).toContain('9987'); + ).toEqual('9987'); }); test('it renders the expected destination port when type is `destination`, and only destinationPort is populated', () => { @@ -1147,7 +1147,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() ) - ).toContain('80'); + ).toEqual('80'); }); test('it does NOT render the badge when type is `source`, but both sourceIp and sourcePort are undefined', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx index 8b3f0bfdb107a..4ebb804eab8a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx @@ -51,7 +51,7 @@ describe('CertificateFingerprint', () => { removeExternalLinkText( wrapper.find('[data-test-subj="certificate-fingerprint-link"]').first().text() ) - ).toContain('3f4c57934e089f02ae7511200aee2d7e7aabd272'); + ).toEqual('3f4c57934e089f02ae7511200aee2d7e7aabd272'); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx index ddbba7f2bc9f3..31f2fec942490 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx @@ -48,7 +48,7 @@ describe('Ja3Fingerprint', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) - ).toContain('fff799d91b7c01ae3fe6787cfc895552'); + ).toEqual('fff799d91b7c01ae3fe6787cfc895552'); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx index 9ccabf2f47d44..8a88a7182af03 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx @@ -204,7 +204,7 @@ describe('Netflow', () => { removeExternalLinkText( wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() ) - ).toContain('10.1.2.3:80'); + ).toEqual('10.1.2.3:80'); }); test('it renders destination.packets', () => { @@ -340,7 +340,7 @@ describe('Netflow', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) - ).toContain('192.168.1.2:9987'); + ).toEqual('192.168.1.2:9987'); }); test('it renders source.packets', () => { @@ -374,7 +374,7 @@ describe('Netflow', () => { .first() .text() ) - ).toContain('tls.client_certificate.fingerprint.sha1-value'); + ).toEqual('tls.client_certificate.fingerprint.sha1-value'); }); test('it hyperlinks tls.fingerprints.ja3.hash site to compare the fingerprint against a known set of signatures', () => { @@ -390,7 +390,7 @@ describe('Netflow', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) - ).toContain('tls.fingerprints.ja3.hash-value'); + ).toEqual('tls.fingerprints.ja3.hash-value'); }); test('it hyperlinks tls.server_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { @@ -418,7 +418,7 @@ describe('Netflow', () => { .first() .text() ) - ).toContain('tls.server_certificate.fingerprint.sha1-value'); + ).toEqual('tls.server_certificate.fingerprint.sha1-value'); }); test('it renders network.transport', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index ebb807a590124..6ea24e5ca57f6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -2686,12 +2686,11 @@ exports[`Details Panel Component DetailsPanel:NetworkDetails: rendering it shoul type="popout" > - External link - + /> { - return str.replaceAll('External link', ''); -}; - jest.mock('../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { @@ -96,7 +90,7 @@ describe('get_column_renderer', () => { {row} ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toContain( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -115,7 +109,7 @@ describe('get_column_renderer', () => { {row} ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toContain( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -134,7 +128,7 @@ describe('get_column_renderer', () => { {row} ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toContain( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx index f8693d4a4f8ea..2d06c040c5b00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -53,11 +53,7 @@ describe('SuricataDetails', () => { /> ); - const removeEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( - 'External link', - '' - ); - expect(removeEuiIconText).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index 2022904e548aa..61ea659964e4d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -72,12 +72,7 @@ describe('suricata_row_renderer', () => { {children} ); - - const extractEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( - 'External link', - '' - ); - expect(extractEuiIconText).toContain( + expect(removeExternalLinkText(wrapper.text())).toContain( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index 4b93c5accb590..ae2caa8ce8401 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -83,12 +83,6 @@ import { import * as i18n from './translations'; import { RowRenderer } from '../../../../../../../common/types'; -// EuiIcons coming from .testenv render the icon's aria-label as a span -// extractEuiIcon removes the aria-label before checking for equality -const extractEuiIconText = (str: string) => { - return str.replaceAll('External link', ''); -}; - jest.mock('../../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { @@ -1136,7 +1130,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'NETWORK SERVICE\\NT AUTHORITY@windows-endpoint-1accepted a connection viasvchost.exe(328)with resultsuccessEndpoint network eventincomingtcpSource10.1.2.3:64557North AmericaUnited States🇺🇸USNorth CarolinaConcordDestination10.50.60.70:3389' ); }); @@ -1220,7 +1214,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'NETWORK SERVICE\\NT AUTHORITY@win2019-endpoint-1made a http request viasvchost.exe(2232)Endpoint network eventoutgoinghttptcpSource10.1.2.3:51570Destination10.11.12.13:80North AmericaUnited States🇺🇸USArizonaPhoenix' ); }); @@ -1249,7 +1243,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' ); }); @@ -1278,7 +1272,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' ); }); @@ -1304,7 +1298,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'NETWORK SERVICE\\NT AUTHORITY@windows-endpoint-1disconnected viasvchost.exe(328)Endpoint network eventincomingtcpSource10.20.30.40:64557North AmericaUnited States🇺🇸USNorth CarolinaConcord(42.47%)1.2KB(57.53%)1.6KBDestination10.11.12.13:3389' ); }); @@ -1333,7 +1327,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' ); }); @@ -1362,7 +1356,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' ); }); @@ -1391,7 +1385,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' ); }); @@ -1420,7 +1414,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' ); }); @@ -1728,7 +1722,7 @@ describe('GenericRowRenderer', () => { ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 9af22fca0c707..62836cbffb2b5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -14,12 +14,6 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ZeekDetails } from './zeek_details'; -// EuiIcons coming from .testenv render the icon's aria-label as a span -// extractEuiIcon removes the aria-label before checking for equality -const extractEuiIconText = (str: string) => { - return str.replaceAll('External link', ''); -}; - jest.mock('../../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { @@ -59,7 +53,7 @@ describe('ZeekDetails', () => { /> ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); @@ -74,7 +68,7 @@ describe('ZeekDetails', () => { /> ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'CyIrMA1L1JtLqdIuoldnsudpSource206.189.35.240:57475Destination67.207.67.3:53' ); }); @@ -89,7 +83,7 @@ describe('ZeekDetails', () => { /> ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'CZLkpC22NquQJOpkwehttp302Source206.189.35.240:36220Destination192.241.164.26:80' ); }); @@ -104,7 +98,7 @@ describe('ZeekDetails', () => { /> ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'noticeDropped:falseScan::Port_Scan8.42.77.171 scanned at least 15 unique ports of host 207.154.238.205 in 0m0sSource8.42.77.171' ); }); @@ -119,7 +113,7 @@ describe('ZeekDetails', () => { /> ); - expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'CmTxzt2OVXZLkGDaResslTLSv12TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256Source188.166.66.184:34514Destination91.189.95.15:443' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index fda83c0ade12b..b60a2965bfd70 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -71,12 +71,7 @@ describe('zeek_row_renderer', () => { {children} ); - - const extractEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( - 'External link', - '' - ); - expect(extractEuiIconText).toContain( + expect(removeExternalLinkText(wrapper.text())).toContain( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index 726716c7f53ab..3f27b80359131 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -101,11 +101,7 @@ describe('ZeekSignature', () => { test('should render value', () => { const wrapper = mount(); - const extractEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( - 'External link', - '' - ); - expect(extractEuiIconText).toEqual('abc'); + expect(removeExternalLinkText(wrapper.text())).toEqual('abc'); }); test('should render value and link', () => { diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index a99a6fdb81167..60fe9d2bd7128 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -769,7 +769,7 @@ describe('', () => { const stateMessage = find('snapshotDetail.state.value').text(); try { - expect(stateMessage).toContain(expectedMessage); // Messages may include the word "Info" to account for the rendered text coming from EuiIcon + expect(stateMessage).toBe(expectedMessage); } catch { throw new Error( `Expected snapshot state message "${expectedMessage}" for state "${state}, but got "${stateMessage}".` diff --git a/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx b/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx index f3846cd784ccc..5ac75f92ea45f 100644 --- a/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx +++ b/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx @@ -110,7 +110,7 @@ describe('Modal Inspect', () => { expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="index-pattern-title"]').text() - ).toContain('Index pattern '); + ).toBe('Index pattern '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="index-pattern-description"]') @@ -118,7 +118,7 @@ describe('Modal Inspect', () => { ).toBe('auditbeat-*, filebeat-*, packetbeat-*, winlogbeat-*'); expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="query-time-title"]').text() - ).toContain('Query time '); + ).toBe('Query time '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="query-time-description"]') @@ -128,7 +128,7 @@ describe('Modal Inspect', () => { wrapper .find('.euiDescriptionList__title span[data-test-subj="request-timestamp-title"]') .text() - ).toContain('Request timestamp '); + ).toBe('Request timestamp '); }); test('Click on request Tab', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index 6942a7708db78..d23f1cfacf94b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -107,12 +107,10 @@ describe('health check', () => { const [action] = queryAllByText(/Learn more/i); expect(description.textContent).toMatchInlineSnapshot( - `"You must enable API keys to use Alerting. Learn more.External link(opens in a new tab or window)"` + `"You must enable API keys to use Alerting. Learn more.(opens in a new tab or window)"` ); - expect(action.textContent).toMatchInlineSnapshot( - `"Learn more.External link(opens in a new tab or window)"` - ); + expect(action.textContent).toMatchInlineSnapshot(`"Learn more.(opens in a new tab or window)"`); expect(action.getAttribute('href')).toMatchInlineSnapshot( `"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-settings.html#api-key-service-settings"` @@ -143,12 +141,12 @@ describe('health check', () => { const description = queryByRole(/banner/i); expect(description!.textContent).toMatchInlineSnapshot( - `"You must configure an encryption key to use Alerting. Learn more.External link(opens in a new tab or window)"` + `"You must configure an encryption key to use Alerting. Learn more.(opens in a new tab or window)"` ); const action = queryByText(/Learn/i); expect(action!.textContent).toMatchInlineSnapshot( - `"Learn more.External link(opens in a new tab or window)"` + `"Learn more.(opens in a new tab or window)"` ); expect(action!.getAttribute('href')).toMatchInlineSnapshot( `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alert-action-settings-kb.html#general-alert-action-settings"` @@ -181,12 +179,12 @@ describe('health check', () => { const description = queryByText(/You must enable/i); expect(description!.textContent).toMatchInlineSnapshot( - `"You must enable API keys and configure an encryption key to use Alerting. Learn more.External link(opens in a new tab or window)"` + `"You must enable API keys and configure an encryption key to use Alerting. Learn more.(opens in a new tab or window)"` ); const action = queryByText(/Learn/i); expect(action!.textContent).toMatchInlineSnapshot( - `"Learn more.External link(opens in a new tab or window)"` + `"Learn more.(opens in a new tab or window)"` ); expect(action!.getAttribute('href')).toMatchInlineSnapshot( `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alerting-setup.html#alerting-prerequisites"` diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx index e7cafb23ee0fa..737501f444300 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx @@ -373,12 +373,9 @@ describe('execution duration overview', () => { const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]'); expect(avgExecutionDurationPanel.exists()).toBeTruthy(); expect(avgExecutionDurationPanel.first().prop('color')).toEqual('warning'); - - const avgExecutionDurationStat = wrapper - .find('EuiStat[data-test-subj="avgExecutionDurationStat"]') - .text() - .replaceAll('Info', ''); - expect(avgExecutionDurationStat).toEqual('Average duration16:44:44.345'); + expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual( + 'Average duration16:44:44.345' + ); expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeTruthy(); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx index 3576d7e34fd0b..ee485f8aee0c0 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx @@ -63,5 +63,5 @@ test('Can delete drilldowns', () => { test('Error is displayed', () => { const screen = render(); - expect(screen.getByText('an error')).toBeInTheDocument(); + expect(screen.getByLabelText('an error')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap index bf25513a6bc2c..51753d2ce8bb3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap @@ -179,11 +179,10 @@ exports[`PingListExpandedRow renders link to docs if body is not recorded but it > docs - External link - + /> diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap index 29d1ba922de8f..80b751d8e243b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap @@ -72,11 +72,10 @@ Array [ > Set tags - External link - + /> diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx index 671371093c819..63b4d2945a51c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx @@ -11,7 +11,7 @@ import { render } from '../../../../../lib/helper/rtl_helpers'; describe('NetworkRequestsTotal', () => { it('message in case total is greater than fetched', () => { - const { getByText } = render( + const { getByText, getByLabelText } = render( { ); expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); - expect(getByText('Info')).toBeInTheDocument(); + expect(getByLabelText('Info')).toBeInTheDocument(); }); it('message in case total is equal to fetched requests', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx index 4241a7238ecd6..7558a82e45df4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx @@ -13,8 +13,8 @@ import { TestWrapper } from './waterfall_marker_test_helper'; describe('', () => { it('renders a dot icon when `field` is an empty string', () => { - const { getByText } = render(); - expect(getByText('An icon indicating that this marker has no field associated with it')); + const { getByLabelText } = render(); + expect(getByLabelText('An icon indicating that this marker has no field associated with it')); }); it('renders an embeddable when opened', async () => { diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx index d232b12f3a47b..2b899aad783d7 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx @@ -45,22 +45,20 @@ describe('KeyUXMetrics', () => { }; }; - // Tests include the word "info" between the task and time to account for the rendered text coming from - // the EuiIcon (tooltip) embedded within each stat description expect( - getAllByText(checkText('Longest long task durationInfo271 ms'))[0] + getAllByText(checkText('Longest long task duration271 ms'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('Total long tasks durationInfo520 ms'))[0] + getAllByText(checkText('Total long tasks duration520 ms'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('No. of long tasksInfo3'))[0] + getAllByText(checkText('No. of long tasks3'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('Total blocking timeInfo271 ms'))[0] + getAllByText(checkText('Total blocking time271 ms'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('First contentful paintInfo1.27 s'))[0] + getAllByText(checkText('First contentful paint1.27 s'))[0] ).toBeInTheDocument(); }); }); diff --git a/yarn.lock b/yarn.lock index d78a3567a18c0..0b6f13bc96b94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1516,10 +1516,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@52.2.0": - version "52.2.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-52.2.0.tgz#761101a29b96a4b5270ef93541dab7bb27f5ca50" - integrity sha512-XboYerntCOTHWHYMWJGzJtu5JYO6pk5IWh0ZHJEQ4SEjmLbTV2bFrVBTO/8uaU7GhV9/RNIo7BU5wHRyYP7z1g== +"@elastic/eui@51.1.0": + version "51.1.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-51.1.0.tgz#338b710ae7a819bb7c3b8e1916080610e0b8e691" + integrity sha512-pjbBSkfDPAjXBRCMk4zsyZ3sPpf70XVcbOzr4BzT0MW38uKjEgEh6nu1aCdnOi+jVSHRtziJkX9rD8BRDWfsnw== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" From 40ba9bf53bfa69e9aa8902bbef154360efdb5fe8 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 29 Mar 2022 16:12:14 -0600 Subject: [PATCH 039/108] [axe-config] extract module to it's own package (#128815) --- package.json | 2 + packages/BUILD.bazel | 2 + packages/kbn-axe-config/BUILD.bazel | 123 ++++++++++++++++++ packages/kbn-axe-config/README.md | 3 + packages/kbn-axe-config/package.json | 11 ++ .../config.ts => kbn-axe-config/src/index.ts} | 2 +- packages/kbn-axe-config/tsconfig.json | 17 +++ packages/kbn-test-jest-helpers/BUILD.bazel | 4 +- .../kbn-test-jest-helpers/src/axe_helpers.ts | 2 +- packages/kbn-test/src/index.ts | 2 - test/accessibility/services/a11y/a11y.ts | 2 +- .../apm/ftr_e2e/cypress/support/commands.ts | 11 +- .../applications/shared/cypress/commands.ts | 2 +- yarn.lock | 8 ++ 14 files changed, 176 insertions(+), 15 deletions(-) create mode 100644 packages/kbn-axe-config/BUILD.bazel create mode 100644 packages/kbn-axe-config/README.md create mode 100644 packages/kbn-axe-config/package.json rename packages/{kbn-test/src/a11y/config.ts => kbn-axe-config/src/index.ts} (97%) create mode 100644 packages/kbn-axe-config/tsconfig.json diff --git a/package.json b/package.json index f70bde1a2108f..9f2e28774d0de 100644 --- a/package.json +++ b/package.json @@ -464,6 +464,7 @@ "@istanbuljs/schema": "^0.1.2", "@jest/console": "^26.6.2", "@jest/reporters": "^26.6.2", + "@kbn/axe-config": "link:bazel-bin/packages/kbn-axe-config", "@kbn/babel-code-parser": "link:bazel-bin/packages/kbn-babel-code-parser", "@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset", "@kbn/bazel-packages": "link:bazel-bin/packages/kbn-bazel-packages", @@ -582,6 +583,7 @@ "@types/kbn__analytics": "link:bazel-bin/packages/kbn-analytics/npm_module_types", "@types/kbn__apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader/npm_module_types", "@types/kbn__apm-utils": "link:bazel-bin/packages/kbn-apm-utils/npm_module_types", + "@types/kbn__axe-config": "link:bazel-bin/packages/kbn-axe-config/npm_module_types", "@types/kbn__bazel-packages": "link:bazel-bin/packages/kbn-bazel-packages/npm_module_types", "@types/kbn__cli-dev-mode": "link:bazel-bin/packages/kbn-cli-dev-mode/npm_module_types", "@types/kbn__config": "link:bazel-bin/packages/kbn-config/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index f2ca181877883..5a4ee479c5c41 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -18,6 +18,7 @@ filegroup( "//packages/kbn-analytics:build", "//packages/kbn-apm-config-loader:build", "//packages/kbn-apm-utils:build", + "//packages/kbn-axe-config:build", "//packages/kbn-babel-code-parser:build", "//packages/kbn-babel-preset:build", "//packages/kbn-bazel-packages:build", @@ -103,6 +104,7 @@ filegroup( "//packages/kbn-analytics:build_types", "//packages/kbn-apm-config-loader:build_types", "//packages/kbn-apm-utils:build_types", + "//packages/kbn-axe-config:build_types", "//packages/kbn-bazel-packages:build_types", "//packages/kbn-cli-dev-mode:build_types", "//packages/kbn-config-schema:build_types", diff --git a/packages/kbn-axe-config/BUILD.bazel b/packages/kbn-axe-config/BUILD.bazel new file mode 100644 index 0000000000000..d6498ed1546e3 --- /dev/null +++ b/packages/kbn-axe-config/BUILD.bazel @@ -0,0 +1,123 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "kbn-axe-config" +PKG_REQUIRE_NAME = "@kbn/axe-config" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//axe-core", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-axe-config/README.md b/packages/kbn-axe-config/README.md new file mode 100644 index 0000000000000..8f3556fefeb62 --- /dev/null +++ b/packages/kbn-axe-config/README.md @@ -0,0 +1,3 @@ +# @kbn/axe-config + +This is package shares [axe](https://www.deque.com/axe/) rule configuration and options between various axe tests and test runners (e.g., Kibana FTR, Cypress, jest). The API remains the same between each axe runner, and should ideally be shared between each to maintain consistency across Kibana. diff --git a/packages/kbn-axe-config/package.json b/packages/kbn-axe-config/package.json new file mode 100644 index 0000000000000..c5b929478c3de --- /dev/null +++ b/packages/kbn-axe-config/package.json @@ -0,0 +1,11 @@ +{ + "name": "@kbn/axe-config", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "kibana": { + "devOnly": true + } +} diff --git a/packages/kbn-test/src/a11y/config.ts b/packages/kbn-axe-config/src/index.ts similarity index 97% rename from packages/kbn-test/src/a11y/config.ts rename to packages/kbn-axe-config/src/index.ts index e5f6773f03502..df175dbf08173 100644 --- a/packages/kbn-test/src/a11y/config.ts +++ b/packages/kbn-axe-config/src/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ReporterVersion } from 'axe-core'; +import type { ReporterVersion } from 'axe-core'; export const AXE_CONFIG = { rules: [ diff --git a/packages/kbn-axe-config/tsconfig.json b/packages/kbn-axe-config/tsconfig.json new file mode 100644 index 0000000000000..a8cfc2cceb08b --- /dev/null +++ b/packages/kbn-axe-config/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-test-jest-helpers/BUILD.bazel b/packages/kbn-test-jest-helpers/BUILD.bazel index c97859e8baab1..dc8b83495494c 100644 --- a/packages/kbn-test-jest-helpers/BUILD.bazel +++ b/packages/kbn-test-jest-helpers/BUILD.bazel @@ -34,7 +34,7 @@ NPM_MODULE_EXTRA_FILES = [ RUNTIME_DEPS = [ "//packages/kbn-dev-utils", "//packages/kbn-i18n-react", - "//packages/kbn-test", + "//packages/kbn-axe-config", "//packages/kbn-std", "//packages/kbn-utils", "@npm//@elastic/elasticsearch", @@ -78,7 +78,7 @@ TYPES_DEPS = [ "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-i18n-react:npm_module_types", "//packages/kbn-std:npm_module_types", - "//packages/kbn-test:npm_module_types", + "//packages/kbn-axe-config:npm_module_types", "//packages/kbn-utils:npm_module_types", "@npm//@elastic/elasticsearch", "@npm//axios", diff --git a/packages/kbn-test-jest-helpers/src/axe_helpers.ts b/packages/kbn-test-jest-helpers/src/axe_helpers.ts index 215209546f956..6b04bed95c95a 100644 --- a/packages/kbn-test-jest-helpers/src/axe_helpers.ts +++ b/packages/kbn-test-jest-helpers/src/axe_helpers.ts @@ -8,7 +8,7 @@ import { configureAxe } from 'jest-axe'; import { Result } from 'axe-core'; -import { AXE_OPTIONS, AXE_CONFIG } from '@kbn/test'; +import { AXE_OPTIONS, AXE_CONFIG } from '@kbn/axe-config'; import { ReactWrapper } from './testbed/types'; const axeRunner = configureAxe({ globalOptions: { ...AXE_CONFIG } }); diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index a0e45f9d7b752..c9f0e67c558f1 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -69,5 +69,3 @@ export { runJest } from './jest/run'; export * from './kbn_archiver_cli'; export * from './kbn_client'; - -export { AXE_CONFIG, AXE_OPTIONS } from './a11y/config'; diff --git a/test/accessibility/services/a11y/a11y.ts b/test/accessibility/services/a11y/a11y.ts index fd4362c1c82b4..e04e38cb9f72f 100644 --- a/test/accessibility/services/a11y/a11y.ts +++ b/test/accessibility/services/a11y/a11y.ts @@ -8,7 +8,7 @@ import chalk from 'chalk'; import testSubjectToCss from '@kbn/test-subj-selector'; -import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/test'; +import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/axe-config'; import { FtrService } from '../../ftr_provider_context'; import { AxeReport, printResult } from './axe_report'; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 3d8d86145cdac..98bad66b1cc76 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -8,8 +8,7 @@ import 'cypress-real-events/support'; import { Interception } from 'cypress/types/net-stubbing'; import 'cypress-axe'; import moment from 'moment'; -// Commenting this out since it's breaking the tests. It was caused by https://github.com/elastic/kibana/commit/bef90a58663b6c4b668a7fe0ce45a002fb68c474#diff-8a4659c6955a712376fe5ca0d81636164d1b783a63fe9d1a23da4850bd0dfce3R10 -// import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/test'; +import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/axe-config'; Cypress.Commands.add('loginAsReadOnlyUser', () => { cy.loginAs({ username: 'apm_read_user', password: 'changeme' }); @@ -88,13 +87,11 @@ Cypress.Commands.add( // A11y configuration const axeConfig = { - // See comment on line 11 - // ...AXE_CONFIG, + ...AXE_CONFIG, }; const axeOptions = { - // See comment on line 11 - // ...AXE_OPTIONS, - // runOnly: [...AXE_OPTIONS.runOnly, 'best-practice'], + ...AXE_OPTIONS, + runOnly: [...AXE_OPTIONS.runOnly, 'best-practice'], }; export const checkA11y = ({ skipFailures }: { skipFailures: boolean }) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cypress/commands.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cypress/commands.ts index 083d55e6f7029..c5773ec5f44b9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/cypress/commands.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cypress/commands.ts @@ -42,7 +42,7 @@ export const login = ({ // eslint-disable-next-line import/no-extraneous-dependencies import 'cypress-axe'; // eslint-disable-next-line import/no-extraneous-dependencies -import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/test'; +import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/axe-config'; const axeConfig = { ...AXE_CONFIG, diff --git a/yarn.lock b/yarn.lock index 0b6f13bc96b94..c13acd20ed888 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2932,6 +2932,10 @@ version "0.0.0" uid "" +"@kbn/axe-config@link:bazel-bin/packages/kbn-axe-config": + version "0.0.0" + uid "" + "@kbn/babel-code-parser@link:bazel-bin/packages/kbn-babel-code-parser": version "0.0.0" uid "" @@ -5954,6 +5958,10 @@ version "0.0.0" uid "" +"@types/kbn__axe-config@link:bazel-bin/packages/kbn-axe-config/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__bazel-packages@link:bazel-bin/packages/kbn-bazel-packages/npm_module_types": version "0.0.0" uid "" From de894d1ba6fe75d6c954abf68c9bd16c81271dbd Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 29 Mar 2022 16:32:18 -0600 Subject: [PATCH 040/108] [Security Solution] Adds event log telemetry specific for security solution rules (#128216) ## Summary Adds event log telemetry specific for security solution rules. This adds a new section to the telemetry underneath `detectionMetrics` called `detection_rule_status`.
<-- click this text to see a full JSON sample document

```json { "detection_rule_status": { "all_rules": { "eql": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "indicator": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "mlRule": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "query": { "failed": 4, "top_failed": { "1": { "message": "an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default", "count": 4 }, "2": { "message": "an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default", "count": 2 }, "3": { "message": "an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default", "count": 2 }, "4": { "message": "hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default", "count": 2 }, "5": { "message": "hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default", "count": 1 }, "6": { "message": "hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default", "count": 1 } }, "partial_failure": 2, "top_partial_failure": { "1": { "message": "This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled", "count": 189 }, "2": { "message": "This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent", "count": 187 } }, "succeeded": 2, "index_duration": { "max": 228568, "avg": 2292.8852459016393, "min": 0 }, "search_duration": { "max": 324, "avg": 21.661202185792348, "min": 1 }, "gap_duration": { "max": 5651, "avg": 3929.4166666666665, "min": 2811 }, "gap_count": 10 }, "savedQuery": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "threshold": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "total": { "failed": 4, "partial_failure": 2, "succeeded": 2 } }, "elastic_rules": { "eql": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "indicator": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "mlRule": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "query": { "failed": 2, "top_failed": { "1": { "message": "an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default", "count": 2 }, "2": { "message": "an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default", "count": 2 }, "3": { "message": "hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default", "count": 1 }, "4": { "message": "hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default", "count": 1 } }, "partial_failure": 1, "top_partial_failure": { "1": { "message": "This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent", "count": 187 } }, "succeeded": 1, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 278, "avg": 8.165745856353592, "min": 1 }, "gap_duration": { "max": 5474, "avg": 3831, "min": 2811 }, "gap_count": 6 }, "savedQuery": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "threshold": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "total": { "failed": 2, "partial_failure": 1, "succeeded": 1 } }, "custom_rules": { "eql": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "indicator": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "mlRule": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "query": { "failed": 2, "top_failed": { "1": { "message": "an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default", "count": 4 }, "2": { "message": "hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default", "count": 2 } }, "partial_failure": 1, "top_partial_failure": { "1": { "message": "This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled", "count": 189 } }, "succeeded": 1, "index_duration": { "max": 228568, "avg": 4536.1945945945945, "min": 0 }, "search_duration": { "max": 324, "avg": 34.86486486486486, "min": 8 }, "gap_duration": { "max": 5651, "avg": 4027.8333333333335, "min": 3051 }, "gap_count": 4 }, "savedQuery": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "threshold": { "failed": 0, "top_failed": {}, "partial_failure": 0, "top_partial_failure": {}, "succeeded": 0, "index_duration": { "max": 0, "avg": 0, "min": 0 }, "search_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_duration": { "max": 0, "avg": 0, "min": 0 }, "gap_count": 0 }, "total": { "failed": 2, "partial_failure": 1, "succeeded": 1 } } } } ```

**manual testing** Add some alerts and malfunction them into partial errors by having them not have their indexes and malfunction them by shutting down Kibana for a while and then starting it back up to have Kibana miss some alerts. Then go to Advanced Settings -> scroll to the bottom and click cluster data. Should see data like this after scrolling down for a while: Screen Shot 2022-03-21 at 4 18 58 PM ### Checklist Delete any items that are not applicable to this PR. - [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 --- .../security_solution/server/plugin.ts | 1 + .../server/usage/collector.ts | 1600 +++++++++ .../usage/detections/get_initial_usage.ts | 3 +- .../usage/detections/get_metrics.test.ts | 23 + .../server/usage/detections/get_metrics.ts | 12 +- .../detections/rules/get_initial_usage.ts | 69 +- .../detections/rules/get_metrics.mocks.ts | 2908 ++++++++++++++++- .../usage/detections/rules/get_metrics.ts | 28 +- .../server/usage/detections/rules/types.ts | 70 + .../get_event_log_by_type_and_status.ts | 148 + .../usage/queries/utils/count_totals.test.ts | 81 + .../usage/queries/utils/count_totals.ts | 35 + .../get_event_log_agg_by_rule_type.test.ts | 80 + .../utils/get_event_log_agg_by_rule_type.ts | 68 + ...event_log_agg_by_rule_type_metrics.test.ts | 74 + .../get_event_log_agg_by_rule_type_metrics.ts | 79 + .../get_event_log_agg_by_rule_types.test.ts | 471 +++ .../utils/get_event_log_agg_by_rule_types.ts | 34 + ...vent_log_agg_by_rule_types_metrics.test.ts | 270 ++ ...get_event_log_agg_by_rule_types_metrics.ts | 19 + .../utils/get_event_log_agg_by_status.test.ts | 380 +++ .../utils/get_event_log_agg_by_status.ts | 39 + .../get_event_log_agg_by_statuses.test.ts | 486 +++ .../utils/get_event_log_agg_by_statuses.ts | 57 + .../get_event_log_by_type_and_status.test.ts | 74 + .../utils/get_search_for_all_rules.test.ts | 63 + .../queries/utils/get_search_for_all_rules.ts | 51 + .../utils/get_search_for_custom_rules.test.ts | 71 + .../utils/get_search_for_custom_rules.ts | 64 + .../get_search_for_elastic_rules.test.ts | 69 + .../utils/get_search_for_elastic_rules.ts | 62 + .../utils/transform_categories.test.ts | 149 + .../queries/utils/transform_categories.ts | 21 + .../utils/transform_category_bucket.test.ts | 22 + .../utils/transform_category_bucket.ts | 20 + .../transform_event_log_type_status.test.ts | 47 + .../utils/transform_event_log_type_status.ts | 138 + .../transform_single_rule_metric.test.ts | 137 + .../utils/transform_single_rule_metric.ts | 62 + .../security_solution/server/usage/types.ts | 89 + .../schema/xpack_plugins.json | 2458 ++++++++++++++ .../usage_collector/detection_rules.ts | 142 + 42 files changed, 10762 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/usage/queries/get_event_log_by_type_and_status.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/count_totals.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/count_totals.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_status.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_status.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_by_type_and_status.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/transform_categories.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/transform_categories.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/transform_category_bucket.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/transform_category_bucket.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/transform_event_log_type_status.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/transform_event_log_type_status.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts create mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 3f14af0d8affc..e22886ccd53ff 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -168,6 +168,7 @@ export class Plugin implements ISecuritySolutionPlugin { initUsageCollectors({ core, + eventLogIndex: eventLogService.getIndexPattern(), signalsIndex: DEFAULT_ALERTS_INDEX, ml: plugins.ml, usageCollection: plugins.usageCollection, diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index dc98b68f9f186..1169b04a868e6 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -18,6 +18,7 @@ export interface UsageData { export const registerCollector: RegisterCollector = ({ core, + eventLogIndex, signalsIndex, ml, usageCollection, @@ -308,6 +309,1604 @@ export const registerCollector: RegisterCollector = ({ }, }, }, + detection_rule_status: { + all_rules: { + eql: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threat_match: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + machine_learning: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + saved_query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threshold: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + total: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of succeeded rules' }, + }, + }, + }, + elastic_rules: { + eql: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threat_match: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + machine_learning: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + saved_query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threshold: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + total: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of succeeded rules' }, + }, + }, + }, + custom_rules: { + eql: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threat_match: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + machine_learning: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + saved_query: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + threshold: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + top_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + top_partial_failures: { + type: 'array', + items: { + message: { + type: 'keyword', + _meta: { description: 'Failed rule message' }, + }, + count: { + type: 'long', + _meta: { description: 'Number of times the message occurred' }, + }, + }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of successful rules' }, + }, + index_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + search_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, + gap_count: { + type: 'long', + _meta: { description: 'The count of gaps' }, + }, + }, + total: { + failures: { + type: 'long', + _meta: { description: 'The number of failed rules' }, + }, + partial_failures: { + type: 'long', + _meta: { description: 'The number of partial failure rules' }, + }, + succeeded: { + type: 'long', + _meta: { description: 'The number of succeeded rules' }, + }, + }, + }, + }, }, ml_jobs: { ml_job_usage: { @@ -515,6 +2114,7 @@ export const registerCollector: RegisterCollector = ({ fetch: async ({ esClient }: CollectorFetchContext): Promise => { const savedObjectsClient = await getInternalSavedObjectsClient(core); const detectionMetrics = await getDetectionsMetrics({ + eventLogIndex, signalsIndex, esClient, savedObjectsClient, diff --git a/x-pack/plugins/security_solution/server/usage/detections/get_initial_usage.ts b/x-pack/plugins/security_solution/server/usage/detections/get_initial_usage.ts index 0d885aa3b142c..6252b865c0ec9 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/get_initial_usage.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/get_initial_usage.ts @@ -8,7 +8,7 @@ import type { DetectionMetrics } from './types'; import { getInitialMlJobUsage } from './ml_jobs/get_initial_usage'; -import { getInitialRulesUsage } from './rules/get_initial_usage'; +import { getInitialEventLogUsage, getInitialRulesUsage } from './rules/get_initial_usage'; /** * Initial detection metrics initialized. @@ -21,5 +21,6 @@ export const getInitialDetectionMetrics = (): DetectionMetrics => ({ detection_rules: { detection_rule_detail: [], detection_rule_usage: getInitialRulesUsage(), + detection_rule_status: getInitialEventLogUsage(), }, }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/get_metrics.test.ts b/x-pack/plugins/security_solution/server/usage/detections/get_metrics.test.ts index 65929039bc104..a044c1a22e91e 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/get_metrics.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/get_metrics.test.ts @@ -25,6 +25,10 @@ import { getMockRuleAlertsResponse, getMockAlertCaseCommentsResponse, getEmptySavedObjectResponse, + getEventLogAllRules, + getEventLogElasticRules, + getElasticLogCustomRules, + getAllEventLogTransform, } from './rules/get_metrics.mocks'; import { getInitialDetectionMetrics } from './get_initial_usage'; import { getDetectionsMetrics } from './get_metrics'; @@ -45,6 +49,7 @@ describe('Detections Usage and Metrics', () => { it('returns zeroed counts if calls are empty', async () => { const logger = loggingSystemMock.createLogger(); const result = await getDetectionsMetrics({ + eventLogIndex: '', signalsIndex: '', esClient, savedObjectsClient, @@ -55,13 +60,18 @@ describe('Detections Usage and Metrics', () => { }); it('returns information with rule, alerts and cases', async () => { + esClient.search.mockResponseOnce(getEventLogAllRules()); + esClient.search.mockResponseOnce(getEventLogElasticRules()); + esClient.search.mockResponseOnce(getElasticLogCustomRules()); esClient.search.mockResponseOnce(getMockRuleAlertsResponse(3400)); savedObjectsClient.find.mockResolvedValueOnce(getMockRuleSearchResponse()); savedObjectsClient.find.mockResolvedValueOnce(getMockAlertCaseCommentsResponse()); // Get empty saved object for legacy notification system. savedObjectsClient.find.mockResolvedValueOnce(getEmptySavedObjectResponse()); + const logger = loggingSystemMock.createLogger(); const result = await getDetectionsMetrics({ + eventLogIndex: '', signalsIndex: '', esClient, savedObjectsClient, @@ -72,6 +82,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + detection_rule_status: getAllEventLogTransform(), detection_rule_detail: [ { alert_count_daily: 3400, @@ -116,6 +127,9 @@ describe('Detections Usage and Metrics', () => { }); it('returns information with on non elastic prebuilt rule', async () => { + esClient.search.mockResponseOnce(getEventLogAllRules()); + esClient.search.mockResponseOnce(getEventLogElasticRules()); + esClient.search.mockResponseOnce(getElasticLogCustomRules()); esClient.search.mockResponseOnce(getMockRuleAlertsResponse(800)); savedObjectsClient.find.mockResolvedValueOnce(getMockRuleSearchResponse('not_immutable')); savedObjectsClient.find.mockResolvedValueOnce(getMockAlertCaseCommentsResponse()); @@ -123,6 +137,7 @@ describe('Detections Usage and Metrics', () => { savedObjectsClient.find.mockResolvedValueOnce(getEmptySavedObjectResponse()); const logger = loggingSystemMock.createLogger(); const result = await getDetectionsMetrics({ + eventLogIndex: '', signalsIndex: '', esClient, savedObjectsClient, @@ -133,6 +148,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + detection_rule_status: getAllEventLogTransform(), detection_rule_detail: [], // *should not* contain custom detection rule details detection_rule_usage: { ...getInitialRulesUsage(), @@ -162,6 +178,9 @@ describe('Detections Usage and Metrics', () => { }); it('returns information with rule, no alerts and no cases', async () => { + esClient.search.mockResponseOnce(getEventLogAllRules()); + esClient.search.mockResponseOnce(getEventLogElasticRules()); + esClient.search.mockResponseOnce(getElasticLogCustomRules()); esClient.search.mockResponseOnce(getMockRuleAlertsResponse(0)); savedObjectsClient.find.mockResolvedValueOnce(getMockRuleSearchResponse()); savedObjectsClient.find.mockResolvedValueOnce(getMockAlertCaseCommentsResponse()); @@ -170,6 +189,7 @@ describe('Detections Usage and Metrics', () => { const logger = loggingSystemMock.createLogger(); const result = await getDetectionsMetrics({ + eventLogIndex: '', signalsIndex: '', esClient, savedObjectsClient, @@ -180,6 +200,7 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { + detection_rule_status: getAllEventLogTransform(), detection_rule_detail: [ { alert_count_daily: 0, @@ -239,6 +260,7 @@ describe('Detections Usage and Metrics', () => { } as unknown as ReturnType); const logger = loggingSystemMock.createLogger(); const result = await getDetectionsMetrics({ + eventLogIndex: '', signalsIndex: '', esClient, savedObjectsClient, @@ -271,6 +293,7 @@ describe('Detections Usage and Metrics', () => { } as unknown as ReturnType); const result = await getDetectionsMetrics({ + eventLogIndex: '', signalsIndex: '', esClient, savedObjectsClient, diff --git a/x-pack/plugins/security_solution/server/usage/detections/get_metrics.ts b/x-pack/plugins/security_solution/server/usage/detections/get_metrics.ts index 258945fba662a..2d44928026859 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/get_metrics.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/get_metrics.ts @@ -11,7 +11,7 @@ import type { DetectionMetrics } from './types'; import { getMlJobMetrics } from './ml_jobs/get_metrics'; import { getRuleMetrics } from './rules/get_metrics'; -import { getInitialRulesUsage } from './rules/get_initial_usage'; +import { getInitialEventLogUsage, getInitialRulesUsage } from './rules/get_initial_usage'; import { getInitialMlJobUsage } from './ml_jobs/get_initial_usage'; export interface GetDetectionsMetricsOptions { @@ -20,9 +20,11 @@ export interface GetDetectionsMetricsOptions { savedObjectsClient: SavedObjectsClientContract; logger: Logger; mlClient: MlPluginSetup | undefined; + eventLogIndex: string; } export const getDetectionsMetrics = async ({ + eventLogIndex, signalsIndex, esClient, savedObjectsClient, @@ -31,7 +33,7 @@ export const getDetectionsMetrics = async ({ }: GetDetectionsMetricsOptions): Promise => { const [mlJobMetrics, detectionRuleMetrics] = await Promise.allSettled([ getMlJobMetrics({ mlClient, savedObjectsClient, logger }), - getRuleMetrics({ signalsIndex, esClient, savedObjectsClient, logger }), + getRuleMetrics({ signalsIndex, eventLogIndex, esClient, savedObjectsClient, logger }), ]); return { @@ -42,6 +44,10 @@ export const getDetectionsMetrics = async ({ detection_rules: detectionRuleMetrics.status === 'fulfilled' ? detectionRuleMetrics.value - : { detection_rule_detail: [], detection_rule_usage: getInitialRulesUsage() }, + : { + detection_rule_detail: [], + detection_rule_usage: getInitialRulesUsage(), + detection_rule_status: getInitialEventLogUsage(), + }, }; }; diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts index 81ea7aec800e3..b4902e40db822 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts @@ -5,7 +5,13 @@ * 2.0. */ -import type { RulesTypeUsage } from './types'; +import type { + EventLogStatusMetric, + MaxAvgMin, + RulesTypeUsage, + SingleEventLogStatusMetric, + SingleEventMetric, +} from './types'; /** * Default detection rule usage count, split by type + elastic/custom @@ -82,3 +88,64 @@ export const getInitialRulesUsage = (): RulesTypeUsage => ({ notifications_disabled: 0, }, }); + +/** + * Returns the initial usage of event logs specific to rules. + * This returns them for all rules, custom rules, and "elastic_rules"/"immutable rules"/pre-packaged rules + * @returns The initial event log usage + */ +export const getInitialEventLogUsage = (): EventLogStatusMetric => ({ + all_rules: getInitialSingleEventLogUsage(), + custom_rules: getInitialSingleEventLogUsage(), + elastic_rules: getInitialSingleEventLogUsage(), +}); + +/** + * Returns the initial single event metric for a particular event log. + * This returns the initial single event metric for either rules, custom rules, or "elastic_rules"/"immutable rules"/pre-packaged rules + * @see getInitialEventLogUsage + * @returns The initial event log usage for a single event metric. + */ +export const getInitialSingleEventLogUsage = (): SingleEventLogStatusMetric => ({ + eql: getInitialSingleEventMetric(), + threat_match: getInitialSingleEventMetric(), + machine_learning: getInitialSingleEventMetric(), + query: getInitialSingleEventMetric(), + saved_query: getInitialSingleEventMetric(), + threshold: getInitialSingleEventMetric(), + total: { + failures: 0, + partial_failures: 0, + succeeded: 0, + }, +}); + +/** + * Returns the initial single event metric. + * This returns the initial single event metric for either rules, custom rules, or "elastic_rules"/"immutable rules"/pre-packaged rules + * @see getInitialEventLogUsage + * @returns The initial event log usage for a single event metric. + */ +export const getInitialSingleEventMetric = (): SingleEventMetric => ({ + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: getInitialMaxAvgMin(), + search_duration: getInitialMaxAvgMin(), + gap_duration: getInitialMaxAvgMin(), + gap_count: 0, +}); + +/** + * Returns the max, avg, or min for an event. + * This returns the max, avg, or min for a single event metric. + * @see getInitialEventLogUsage + * @returns The max, avg, or min. + */ +export const getInitialMaxAvgMin = (): MaxAvgMin => ({ + max: 0.0, + avg: 0.0, + min: 0.0, +}); diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts index 1801d5bd67782..8cedbeaec03d7 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts @@ -7,8 +7,9 @@ import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsFindResponse } from 'kibana/server'; -import type { AlertAggs } from '../../types'; +import type { AlertAggs, EventLogTypeStatusAggs } from '../../types'; import { CommentAttributes, CommentType } from '../../../../../cases/common/api/cases/comment'; +import type { EventLogStatusMetric, SingleEventLogStatusMetric } from './types'; export const getMockRuleAlertsResponse = (docCount: number): SearchResponse => ({ took: 7, @@ -97,3 +98,2908 @@ export const getEmptySavedObjectResponse = (): SavedObjectsFindResponse => ({ + took: 495, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + eventActionExecutionMetrics: { + doc_count: 325, + 'siem.queryRule': { + doc_count: 325, + maxTotalIndexDuration: { + value: 228568, + }, + avgGapDuration: { + value: 4246.375, + }, + maxTotalSearchDuration: { + value: 324, + }, + gapCount: { + value: 6, + }, + avgTotalIndexDuration: { + value: 2610.1356466876973, + }, + minTotalIndexDuration: { + value: 0, + }, + minGapDuration: { + value: 2811, + }, + avgTotalSearchDuration: { + value: 23.42902208201893, + }, + minTotalSearchDuration: { + value: 1, + }, + maxGapDuration: { + value: 5651, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.mlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + }, + eventActionStatusChange: { + doc_count: 1297, + 'partial failure': { + doc_count: 325, + 'siem.queryRule': { + doc_count: 325, + categories: { + buckets: [ + { + doc_count: 163, + key: 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled', + }, + { + doc_count: 162, + key: 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent', + }, + ], + }, + cardinality: { + value: 2, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + failed: { + doc_count: 8, + 'siem.queryRule': { + doc_count: 8, + categories: { + buckets: [ + { + doc_count: 2, + key: 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default', + }, + { + doc_count: 2, + key: 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default', + }, + { + doc_count: 1, + key: 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default', + }, + { + doc_count: 1, + key: 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default', + }, + { + doc_count: 1, + key: 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default', + }, + { + doc_count: 1, + key: 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default', + }, + ], + }, + cardinality: { + value: 4, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + succeeded: { + doc_count: 317, + 'siem.queryRule': { + doc_count: 317, + cardinality: { + value: 2, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + }, + }, + }, +}); + +/** + * Returns empty event log all rules for testing when you get all the rules. + * See "getEventLogAllRulesResult" for the transform results for use in tests + * @see get_event_log_by_type_and_status + * @see getEventLogAllRulesResult + * @returns The Elasticsearch aggregation for all the rules + */ +export const getEmptyEventLogAllRules = (): SearchResponse => ({ + took: 495, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + eventActionExecutionMetrics: { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.mlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + }, + eventActionStatusChange: { + doc_count: 0, + 'partial failure': { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + failed: { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + succeeded: { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + }, + }, + }, +}); + +/** + * Returns the event log total rules for testing when you get elastic rules specifically. + * See "getEventLogElasticRulesResult" for the transform results for use in tests + * @see get_event_log_by_type_and_status + * @see getEventLogElasticRulesResult + * @returns The Elasticsearch aggregation for "elastic rules"/"immutable"/"pre-built rules" + */ +export const getEventLogElasticRules = (): SearchResponse => ({ + took: 488, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + eventActionExecutionMetrics: { + doc_count: 160, + 'siem.queryRule': { + doc_count: 160, + maxTotalIndexDuration: { + value: 0, + }, + avgGapDuration: { + value: 4141.75, + }, + maxTotalSearchDuration: { + value: 278, + }, + gapCount: { + value: 4, + }, + avgTotalIndexDuration: { + value: 0, + }, + minTotalIndexDuration: { + value: 0, + }, + minGapDuration: { + value: 2811, + }, + avgTotalSearchDuration: { + value: 9.185897435897436, + }, + minTotalSearchDuration: { + value: 1, + }, + maxGapDuration: { + value: 5474, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.mlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + }, + eventActionStatusChange: { + doc_count: 642, + 'partial failure': { + doc_count: 162, + 'siem.queryRule': { + doc_count: 162, + categories: { + buckets: [ + { + doc_count: 162, + key: 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent', + }, + ], + }, + cardinality: { + value: 1, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + failed: { + doc_count: 4, + 'siem.queryRule': { + doc_count: 4, + categories: { + buckets: [ + { + doc_count: 1, + key: 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default', + }, + { + doc_count: 1, + key: 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default', + }, + { + doc_count: 1, + key: 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default', + }, + { + doc_count: 1, + key: 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default', + }, + ], + }, + cardinality: { + value: 2, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + succeeded: { + doc_count: 156, + 'siem.queryRule': { + doc_count: 156, + cardinality: { + value: 1, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + }, + }, + }, +}); + +/** + * Returns empty event log total rules for testing when you get elastic rules specifically. + * See "getEventLogElasticRulesResult" for the transform results for use in tests + * @see get_event_log_by_type_and_status + * @see getEventLogElasticRulesResult + * @returns The Elasticsearch aggregation for "elastic rules"/"immutable"/"pre-built rules" + */ +export const getEmptyEventLogElasticRules = (): SearchResponse => ({ + took: 488, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + eventActionExecutionMetrics: { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.mlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + }, + eventActionStatusChange: { + doc_count: 0, + 'partial failure': { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + failed: { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + succeeded: { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + }, + }, + }, +}); + +/** + * Returns the event log custom rules for testing when you get custom rules specifically. + * See "getEventLogCustomRulesResult" for the transform results for use in tests + * @see get_event_log_by_type_and_status + * @see getEventLogCustomRulesResult + * @returns The Elasticsearch aggregation for "custom rules" + */ +export const getElasticLogCustomRules = (): SearchResponse => ({ + took: 487, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + eventActionExecutionMetrics: { + doc_count: 165, + 'siem.queryRule': { + doc_count: 165, + maxTotalIndexDuration: { + value: 228568, + }, + avgGapDuration: { + value: 4351, + }, + maxTotalSearchDuration: { + value: 324, + }, + gapCount: { + value: 2, + }, + avgTotalIndexDuration: { + value: 5139.211180124224, + }, + minTotalIndexDuration: { + value: 0, + }, + minGapDuration: { + value: 3051, + }, + avgTotalSearchDuration: { + value: 37.22981366459627, + }, + minTotalSearchDuration: { + value: 8, + }, + maxGapDuration: { + value: 5651, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.mlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + }, + eventActionStatusChange: { + doc_count: 655, + 'partial failure': { + doc_count: 163, + 'siem.queryRule': { + doc_count: 163, + categories: { + buckets: [ + { + doc_count: 163, + key: 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled', + }, + ], + }, + cardinality: { + value: 1, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + failed: { + doc_count: 4, + 'siem.queryRule': { + doc_count: 4, + categories: { + buckets: [ + { + doc_count: 2, + key: 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default', + }, + { + doc_count: 2, + key: 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default', + }, + ], + }, + cardinality: { + value: 2, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + succeeded: { + doc_count: 161, + 'siem.queryRule': { + doc_count: 161, + cardinality: { + value: 1, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + }, + }, + }, +}); + +/** + * Returns the empty event log total rules for testing when you get custom rules specifically. + * See "getEventLogCustomRulesResult" for the transform results for use in tests + * @see get_event_log_by_type_and_status + * @see getEventLogCustomRulesResult + * @returns The Elasticsearch aggregation for "custom rules" + */ +export const getEmptyElasticLogCustomRules = (): SearchResponse => ({ + took: 487, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + eventActionExecutionMetrics: { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.mlRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + maxTotalIndexDuration: { + value: null, + }, + avgGapDuration: { + value: null, + }, + maxTotalSearchDuration: { + value: null, + }, + gapCount: { + value: 0, + }, + avgTotalIndexDuration: { + value: null, + }, + minTotalIndexDuration: { + value: null, + }, + minGapDuration: { + value: null, + }, + avgTotalSearchDuration: { + value: null, + }, + minTotalSearchDuration: { + value: null, + }, + maxGapDuration: { + value: null, + }, + }, + }, + eventActionStatusChange: { + doc_count: 0, + 'partial failure': { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + failed: { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + categories: { + buckets: [], + }, + cardinality: { + value: 0, + }, + }, + }, + succeeded: { + doc_count: 0, + 'siem.queryRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.savedQueryRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.eqlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.thresholdRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.mlRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + 'siem.indicatorRule': { + doc_count: 0, + cardinality: { + value: 0, + }, + }, + }, + }, + }, +}); + +/** + * Gets the all rule results for tests. + * @see getEventLogAllRules + * @returns The transform of "getEventLogAllRules" + */ +export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ + eql: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + threat_match: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + machine_learning: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + query: { + failures: 4, + top_failures: [ + { + message: + 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default', + count: 2, + }, + { + message: + 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default', + count: 2, + }, + { + message: + 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default', + count: 1, + }, + { + message: + 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default', + count: 1, + }, + { + message: + 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default', + count: 1, + }, + { + message: + 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default', + count: 1, + }, + ], + partial_failures: 2, + top_partial_failures: [ + { + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled', + count: 163, + }, + { + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent', + count: 162, + }, + ], + succeeded: 2, + index_duration: { + max: 228568, + avg: 2610.1356466876973, + min: 0, + }, + search_duration: { + max: 324, + avg: 23.42902208201893, + min: 1, + }, + gap_duration: { + max: 5651, + avg: 4246.375, + min: 2811, + }, + gap_count: 6, + }, + saved_query: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + threshold: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + total: { + failures: 4, + partial_failures: 2, + succeeded: 2, + }, +}); + +/** + * Gets the elastic rule results for tests. + * @see getEventLogElasticRules + * @returns The transform of "getEventLogElasticRules" + */ +export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ({ + eql: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + threat_match: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + machine_learning: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + query: { + failures: 2, + top_failures: [ + { + message: + 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default', + count: 1, + }, + { + message: + 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default', + count: 1, + }, + { + message: + 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Endpoint Security id rule id execution id space ID default', + count: 1, + }, + { + message: + 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name Telnet Port Activity id rule id execution id space ID default', + count: 1, + }, + ], + partial_failures: 1, + top_partial_failures: [ + { + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent', + count: 162, + }, + ], + succeeded: 1, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 278, + avg: 9.185897435897436, + min: 1, + }, + gap_duration: { + max: 5474, + avg: 4141.75, + min: 2811, + }, + gap_count: 4, + }, + saved_query: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + threshold: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + total: { + failures: 2, + partial_failures: 1, + succeeded: 1, + }, +}); + +/** + * Gets the custom rule results for tests. + * @see getEventLogCustomRulesResult + * @returns The transform of "getEventLogCustomRulesResult" + */ +export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ + eql: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + threat_match: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + machine_learning: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + query: { + failures: 2, + top_failures: [ + { + message: + 'an hour were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default', + count: 2, + }, + { + message: + 'hours were not queried between this rule execution and the last execution so signals may have been missed Consider increasing your look behind time or adding more Kibana instances name * id rule id execution id space ID default', + count: 2, + }, + ], + partial_failures: 1, + top_partial_failures: [ + { + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled', + count: 163, + }, + ], + succeeded: 1, + index_duration: { + max: 228568, + avg: 5139.211180124224, + min: 0, + }, + search_duration: { + max: 324, + avg: 37.22981366459627, + min: 8, + }, + gap_duration: { + max: 5651, + avg: 4351, + min: 3051, + }, + gap_count: 2, + }, + saved_query: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + threshold: { + failures: 0, + top_failures: [], + partial_failures: 0, + top_partial_failures: [], + succeeded: 0, + index_duration: { + max: 0, + avg: 0, + min: 0, + }, + search_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_duration: { + max: 0, + avg: 0, + min: 0, + }, + gap_count: 0, + }, + total: { + failures: 2, + partial_failures: 1, + succeeded: 1, + }, +}); + +/** + * Gets all rule results for tests. + * @returns The transform of all the rule results + */ +export const getAllEventLogTransform = (): EventLogStatusMetric => ({ + all_rules: getEventLogAllRulesResult(), + elastic_rules: getEventLogElasticRulesResult(), + custom_rules: getEventLogCustomRulesResult(), +}); diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.ts index b202ea964301c..a6d653632a564 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.ts @@ -12,12 +12,13 @@ import { updateRuleUsage } from './update_usage'; import { getDetectionRules } from '../../queries/get_detection_rules'; import { getAlerts } from '../../queries/get_alerts'; import { MAX_PER_PAGE, MAX_RESULTS_WINDOW } from '../../constants'; -import { getInitialRulesUsage } from './get_initial_usage'; +import { getInitialEventLogUsage, getInitialRulesUsage } from './get_initial_usage'; import { getCaseComments } from '../../queries/get_case_comments'; import { getRuleIdToCasesMap } from './transform_utils/get_rule_id_to_cases_map'; import { getAlertIdToCountMap } from './transform_utils/get_alert_id_to_count_map'; import { getRuleIdToEnabledMap } from './transform_utils/get_rule_id_to_enabled_map'; import { getRuleObjectCorrelations } from './transform_utils/get_rule_object_correlations'; +import { getEventLogByTypeAndStatus } from '../../queries/get_event_log_by_type_and_status'; // eslint-disable-next-line no-restricted-imports import { legacyGetRuleActions } from '../../queries/legacy_get_rule_actions'; @@ -27,6 +28,7 @@ export interface GetRuleMetricsOptions { esClient: ElasticsearchClient; savedObjectsClient: SavedObjectsClientContract; logger: Logger; + eventLogIndex: string; } export const getRuleMetrics = async ({ @@ -34,6 +36,7 @@ export const getRuleMetrics = async ({ esClient, savedObjectsClient, logger, + eventLogIndex, }: GetRuleMetricsOptions): Promise => { try { // gets rule saved objects @@ -49,6 +52,7 @@ export const getRuleMetrics = async ({ return { detection_rule_detail: [], detection_rule_usage: getInitialRulesUsage(), + detection_rule_status: getInitialEventLogUsage(), }; } @@ -77,11 +81,21 @@ export const getRuleMetrics = async ({ logger, }); - const [detectionAlertsResp, caseComments, legacyRuleActions] = await Promise.all([ - detectionAlertsRespPromise, - caseCommentsPromise, - legacyRuleActionsPromise, - ]); + // gets the event log information by type and status + const eventLogMetricsTypeStatusPromise = getEventLogByTypeAndStatus({ + esClient, + logger, + eventLogIndex, + ruleResults, + }); + + const [detectionAlertsResp, caseComments, legacyRuleActions, eventLogMetricsTypeStatus] = + await Promise.all([ + detectionAlertsRespPromise, + caseCommentsPromise, + legacyRuleActionsPromise, + eventLogMetricsTypeStatusPromise, + ]); // create in-memory maps for correlation const legacyNotificationRuleIds = getRuleIdToEnabledMap(legacyRuleActions); @@ -108,6 +122,7 @@ export const getRuleMetrics = async ({ return { detection_rule_detail: elasticRuleObjects, detection_rule_usage: rulesUsage, + detection_rule_status: eventLogMetricsTypeStatus, }; } catch (e) { // ignore failure, usage will be zeroed. We use debug mode to not unnecessarily worry users as this will not effect them. @@ -117,6 +132,7 @@ export const getRuleMetrics = async ({ return { detection_rule_detail: [], detection_rule_usage: getInitialRulesUsage(), + detection_rule_status: getInitialEventLogUsage(), }; } }; diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts index 54b3e6d6a0084..4b8f62ad01ed3 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts @@ -29,6 +29,7 @@ export interface RulesTypeUsage { export interface RuleAdoption { detection_rule_detail: RuleMetric[]; detection_rule_usage: RulesTypeUsage; + detection_rule_status: EventLogStatusMetric; } export interface RuleMetric { @@ -45,3 +46,72 @@ export interface RuleMetric { has_legacy_notification: boolean; has_notification: boolean; } + +/** + * All the metrics for + * - all_rules, All the rules which includes "custom" and "elastic rules"/"immutable"/"pre-packaged" + * - custom_rules, All the rules which are _not_ "elastic rules"/"immutable"/"pre-packaged", thus custom rules + * - elastic_rules, All the "elastic rules"/"immutable"/"pre-packaged" + * @see get_event_log_by_type_and_status + */ +export interface EventLogStatusMetric { + all_rules: SingleEventLogStatusMetric; + custom_rules: SingleEventLogStatusMetric; + elastic_rules: SingleEventLogStatusMetric; +} + +/** + * Simple max, avg, min interface. + * @see SingleEventMetric + * @see EventLogStatusMetric + */ +export interface MaxAvgMin { + max: number; + avg: number; + min: number; +} + +/** + * Single event metric and how many failures, succeeded, index, durations. + * @see SingleEventLogStatusMetric + * @see EventLogStatusMetric + */ +export interface SingleEventMetric { + failures: number; + top_failures: FailureMessage[]; + partial_failures: number; + top_partial_failures: FailureMessage[]; + succeeded: number; + index_duration: MaxAvgMin; + search_duration: MaxAvgMin; + gap_duration: MaxAvgMin; + gap_count: number; +} + +/** + * This contains the single event log status metric + * @see EventLogStatusMetric + */ +export interface SingleEventLogStatusMetric { + eql: SingleEventMetric; + threat_match: SingleEventMetric; + machine_learning: SingleEventMetric; + query: SingleEventMetric; + saved_query: SingleEventMetric; + threshold: SingleEventMetric; + total: { + failures: number; + partial_failures: number; + succeeded: number; + }; +} + +/** + * This is the format for a failure message which is the message + * and a count of how many rules had that failure message. + * @see EventLogStatusMetric + */ +export interface FailureMessage { + message: string; + count: number; +} diff --git a/x-pack/plugins/security_solution/server/usage/queries/get_event_log_by_type_and_status.ts b/x-pack/plugins/security_solution/server/usage/queries/get_event_log_by_type_and_status.ts new file mode 100644 index 0000000000000..4e6d982bf1d7d --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/get_event_log_by_type_and_status.ts @@ -0,0 +1,148 @@ +/* + * 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 { ElasticsearchClient, Logger, SavedObjectsFindResult } from 'kibana/server'; +import { + EQL_RULE_TYPE_ID, + INDICATOR_RULE_TYPE_ID, + ML_RULE_TYPE_ID, + QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, + SAVED_QUERY_RULE_TYPE_ID, +} from '@kbn/securitysolution-rules'; +import type { EventLogTypeStatusAggs, RuleSearchResult } from '../types'; +import type { EventLogStatusMetric } from '../detections/rules/types'; +import { getEventLogAggByStatuses } from './utils/get_event_log_agg_by_statuses'; +import { transformEventLogTypeStatus } from './utils/transform_event_log_type_status'; +import { getInitialEventLogUsage } from '../detections/rules/get_initial_usage'; +import { getSearchForAllRules } from './utils/get_search_for_all_rules'; +import { getSearchForElasticRules } from './utils/get_search_for_elastic_rules'; +import { getSearchForCustomRules } from './utils/get_search_for_custom_rules'; + +export interface GetEventLogByTypeAndStatusOptions { + esClient: ElasticsearchClient; + eventLogIndex: string; + logger: Logger; + ruleResults: Array>; +} + +/** + * Gets the event logs by their rule type and rule status. Returns the structure + * transformed. If it malfunctions or times out then it will not malfunction other + * parts of telemetry. + * NOTE: This takes in "ruleResults" to filter against elastic rules and custom rules. + * If the event log recorded information about which rules were elastic vs. custom this + * would not need to be passed down. + * @param esClient the elastic client which should be a system based client + * @param eventLogIndex the index of the event log such as ".kibana-event-log-8.2.0" + * @param logger The kibana logger + * @param ruleResults The elastic and custom rules to filter against each. + * @returns The event log transformed + */ +export const getEventLogByTypeAndStatus = async ({ + esClient, + eventLogIndex, + logger, + ruleResults, +}: GetEventLogByTypeAndStatusOptions): Promise => { + try { + const typeAndStatus = await _getEventLogByTypeAndStatus({ + esClient, + eventLogIndex, + logger, + ruleResults, + }); + return typeAndStatus; + } catch (error) { + logger.debug( + `Error trying to get event log by type and status. Error message is: "${error.message}". Error is: "${error}". Returning empty initialized object.` + ); + return getInitialEventLogUsage(); + } +}; + +/** + * Non-try-catch version. Gets the event logs by their rule type and rule status. Returns the structure + * transformed. + * NOTE: This takes in "ruleResults" to filter against elastic rules and custom rules. + * If the event log recorded information about which rules were elastic vs. custom this + * would not need to be passed down. + * @param esClient the elastic client which should be a system based client + * @param eventLogIndex the index of the event log such as ".kibana-event-log-8.2.0" + * @param logger The kibana logger + * @param ruleResults The elastic and custom rules to filter against each. + * @returns The event log transformed + */ +const _getEventLogByTypeAndStatus = async ({ + esClient, + eventLogIndex, + logger, + ruleResults, +}: GetEventLogByTypeAndStatusOptions): Promise => { + const aggs = getEventLogAggByStatuses({ + ruleStatuses: ['succeeded', 'failed', 'partial failure'], + ruleTypes: [ + EQL_RULE_TYPE_ID, + INDICATOR_RULE_TYPE_ID, + ML_RULE_TYPE_ID, + QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, + SAVED_QUERY_RULE_TYPE_ID, + ], + }); + + const elasticRuleIds = ruleResults + .filter((ruleResult) => ruleResult.attributes.params.immutable) + .map((ruleResult) => ruleResult.id); + + const queryForTotal = getSearchForAllRules({ eventLogIndex, aggs }); + const queryForElasticRules = getSearchForElasticRules({ eventLogIndex, aggs, elasticRuleIds }); + const queryForCustomRules = getSearchForCustomRules({ eventLogIndex, aggs, elasticRuleIds }); + logger.debug( + `Getting event logs by type and status with query for total: ${JSON.stringify( + queryForTotal + )}, elastic_rules: ${JSON.stringify(queryForElasticRules)} custom_rules: ${JSON.stringify( + queryForCustomRules + )}` + ); + + const [totalRules, elasticRules, customRules] = await Promise.all([ + esClient.search(queryForTotal), + esClient.search(queryForElasticRules), + esClient.search(queryForCustomRules), + ]); + + logger.debug( + `Raw search results of event logs by type and status for total: ${JSON.stringify( + totalRules + )} elastic_rules: ${JSON.stringify(elasticRules)}, custom_rules: ${JSON.stringify(customRules)}` + ); + + const totalRulesTransformed = transformEventLogTypeStatus({ + aggs: totalRules.aggregations, + logger, + }); + const elasticRulesTransformed = transformEventLogTypeStatus({ + aggs: elasticRules.aggregations, + logger, + }); + const customRulesTransformed = transformEventLogTypeStatus({ + aggs: customRules.aggregations, + logger, + }); + + const logStatusMetric: EventLogStatusMetric = { + all_rules: totalRulesTransformed, + elastic_rules: elasticRulesTransformed, + custom_rules: customRulesTransformed, + }; + logger.debug( + `Metrics transformed for event logs of type and status are: ${JSON.stringify(logStatusMetric)}` + ); + + return logStatusMetric; +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/count_totals.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/count_totals.test.ts new file mode 100644 index 0000000000000..85cb4389cdbd7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/count_totals.test.ts @@ -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 { countTotals } from './count_totals'; + +describe('count_totals', () => { + test('returns 0 if given an empty array', () => { + const result = countTotals([]); + expect(result).toEqual(0); + }); + + test('returns 0 if given a single cardinality with a null value', () => { + const result = countTotals([ + { + doc_count: 0, + cardinality: { value: null }, + }, + ]); + expect(result).toEqual(0); + }); + + test('it counts a single cardinality by returning that single number', () => { + const result = countTotals([ + { + doc_count: 8, + cardinality: { value: 5 }, + }, + ]); + expect(result).toEqual(5); + }); + + test('it can count 2 cardinalities by adding their sum up correctly', () => { + const result = countTotals([ + { + doc_count: 8, + cardinality: { value: 5 }, + }, + { + doc_count: 8, + cardinality: { value: 3 }, + }, + ]); + expect(result).toEqual(8); + }); + + test('it can will skip a single cardinality value if that value is null', () => { + const result = countTotals([ + { + doc_count: 8, + cardinality: { value: 5 }, + }, + { + doc_count: 0, + cardinality: { value: null }, + }, + ]); + expect(result).toEqual(5); + }); + + test('it can will skip a single cardinality value if that value is null but add the 3rd one', () => { + const result = countTotals([ + { + doc_count: 8, + cardinality: { value: 5 }, + }, + { + doc_count: 0, + cardinality: { value: null }, + }, + { + doc_count: 0, + cardinality: { value: 3 }, + }, + ]); + expect(result).toEqual(8); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/count_totals.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/count_totals.ts new file mode 100644 index 0000000000000..698d654e1e38d --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/count_totals.ts @@ -0,0 +1,35 @@ +/* + * 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 { CountCardinality } from '../../types'; + +/** + * Given an array of cardinalities this will count them and return the total. + * You can use this to count failures, partial failures, successes, etc... + * @example + * ```ts + * const failed = countTotals([ + * eqlFailure, + * indicatorFailure, + * mlFailure, + * queryFailure, + * savedQueryFailure, + * thresholdFailure, + * ]), + * ``` + * @param countCardinalities Array of cardinalities to count. + * @returns The count or zero if the cardinalities do not exist or it is an empty array. + */ +export const countTotals = (countCardinalities: CountCardinality[]) => { + return countCardinalities.reduce((accum, countCardinality) => { + if (countCardinality.cardinality.value != null) { + return countCardinality.cardinality.value + accum; + } else { + return accum; + } + }, 0); +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type.test.ts new file mode 100644 index 0000000000000..3ecbdd0d03554 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getEventLogAggByRuleType } from './get_event_log_agg_by_rule_type'; + +describe('get_event_log_agg_by_rule_type', () => { + test('returns aggregation that does NOT have "categorize_text" when status is "succeeded"', () => { + const result = getEventLogAggByRuleType({ ruleType: 'siem.eqlRule', ruleStatus: 'succeeded' }); + expect(result).toEqual({ + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }); + }); + + test('returns aggregation that has "categorize_text" when status is "failed"', () => { + const result = getEventLogAggByRuleType({ ruleType: 'siem.queryRule', ruleStatus: 'failed' }); + expect(result).toEqual({ + filter: { + term: { + 'rule.category': 'siem.queryRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }); + }); + + test('returns aggregation that has "categorize_text" when status is "partial failure"', () => { + const result = getEventLogAggByRuleType({ + ruleType: 'siem.indicatorRule', + ruleStatus: 'partial failure', + }); + expect(result).toEqual({ + filter: { + term: { + 'rule.category': 'siem.indicatorRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type.ts new file mode 100644 index 0000000000000..49dd76aef2bce --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type.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. + */ + +import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { RuleTypeId } from '@kbn/securitysolution-rules'; +import type { RuleStatus } from '../../types'; + +export interface GetEventLogAggByRuleTypeOptions { + ruleType: RuleTypeId; + ruleStatus: RuleStatus; +} + +/** + * Given a rule type and rule status this will return an aggregation filter and a + * sub aggregation of categories and count how many rule id's are associated. If the + * rule status is "failed" or "partial failure" you get the added aggregation of a + * categorize text added. This categorize text will give you the top 10 failures based + * on the message field. + * @param ruleType The rule type such as "siem.eqlRule" | "siem.mlRule" etc... + * @param ruleStatus The rule status such as "succeeded" | "partial failure" | "failed" + * @returns The aggregation to put into a search + */ +export const getEventLogAggByRuleType = ({ + ruleType, + ruleStatus, +}: GetEventLogAggByRuleTypeOptions): AggregationsAggregationContainer => { + if (ruleStatus === 'failed' || ruleStatus === 'partial failure') { + return { + filter: { + term: { + 'rule.category': ruleType, + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }; + } else { + return { + filter: { + term: { + 'rule.category': ruleType, + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }; + } +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts new file mode 100644 index 0000000000000..09a988fbf02ef --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getEventLogAggByRuleTypeMetrics } from './get_event_log_agg_by_rule_type_metrics'; + +describe('get_event_log_agg_by_rule_type_metrics', () => { + test('returns expected aggregation when given a rule type', () => { + const result = getEventLogAggByRuleTypeMetrics('siem.eqlRule'); + expect(result).toEqual({ + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts new file mode 100644 index 0000000000000..6fe8103e29a0d --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts @@ -0,0 +1,79 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { RuleTypeId } from '@kbn/securitysolution-rules'; + +/** + * Given a rule type this will return aggregations based on metrics such as "gapCount" and it + * will filter on the rule.category given the rule type to get the metrics based on the rule type + * @param ruleType The rule type such as "siem.eqlRule" | "siem.mlRule" etc... + * @returns The aggregation to put into a search + */ +export const getEventLogAggByRuleTypeMetrics = ( + ruleType: RuleTypeId +): AggregationsAggregationContainer => { + return { + filter: { + term: { + 'rule.category': ruleType, + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types.test.ts new file mode 100644 index 0000000000000..a1dbde6d79841 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types.test.ts @@ -0,0 +1,471 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getEventLogAggByRuleTypes } from './get_event_log_agg_by_rule_types'; + +describe('get_event_log_agg_by_rule_types', () => { + test('it returns empty object if the array is empty', () => { + const result = getEventLogAggByRuleTypes({ ruleTypes: [], ruleStatus: 'succeeded' }); + expect(result).toEqual>({}); + }); + + test('it returns 1 aggregation if the array has a single element', () => { + const result = getEventLogAggByRuleTypes({ + ruleTypes: ['siem.eqlRule'], + ruleStatus: 'succeeded', + }); + expect(result).toEqual>({ + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }); + }); + + test('it returns 2 aggregations if the array has 2 elements', () => { + const result = getEventLogAggByRuleTypes({ + ruleTypes: ['siem.eqlRule', 'siem.mlRule'], + ruleStatus: 'succeeded', + }); + expect(result).toEqual>({ + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.mlRule': { + filter: { + term: { + 'rule.category': 'siem.mlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }); + }); + + test('it returns the same aggregation if the array has the same 2 elements of the same type for some reason.', () => { + const result = getEventLogAggByRuleTypes({ + ruleTypes: ['siem.eqlRule', 'siem.eqlRule'], + ruleStatus: 'succeeded', + }); + expect(result).toEqual>({ + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }); + }); + + test('it returns 6 aggregations if the array has 6 elements', () => { + const result = getEventLogAggByRuleTypes({ + ruleTypes: [ + 'siem.eqlRule', + 'siem.mlRule', + 'siem.indicatorRule', + 'siem.queryRule', + 'siem.savedQueryRule', + 'siem.thresholdRule', + ], + ruleStatus: 'succeeded', + }); + expect(result).toEqual>({ + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.mlRule': { + filter: { + term: { + 'rule.category': 'siem.mlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.indicatorRule': { + filter: { + term: { + 'rule.category': 'siem.indicatorRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.queryRule': { + filter: { + term: { + 'rule.category': 'siem.queryRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.savedQueryRule': { + filter: { + term: { + 'rule.category': 'siem.savedQueryRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.thresholdRule': { + filter: { + term: { + 'rule.category': 'siem.thresholdRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }); + }); + + test('it returns 6 aggregations if the array has 6 elements and will have "categorization" if the ruleStatus is "failed"', () => { + const result = getEventLogAggByRuleTypes({ + ruleTypes: [ + 'siem.eqlRule', + 'siem.mlRule', + 'siem.indicatorRule', + 'siem.queryRule', + 'siem.savedQueryRule', + 'siem.thresholdRule', + ], + ruleStatus: 'failed', + }); + expect(result).toEqual>({ + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.mlRule': { + filter: { + term: { + 'rule.category': 'siem.mlRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.indicatorRule': { + filter: { + term: { + 'rule.category': 'siem.indicatorRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.queryRule': { + filter: { + term: { + 'rule.category': 'siem.queryRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.savedQueryRule': { + filter: { + term: { + 'rule.category': 'siem.savedQueryRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.thresholdRule': { + filter: { + term: { + 'rule.category': 'siem.thresholdRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }); + }); + + test('it returns 6 aggregations if the array has 6 elements and will have "categorization" if the ruleStatus is "partial failure"', () => { + const result = getEventLogAggByRuleTypes({ + ruleTypes: [ + 'siem.eqlRule', + 'siem.mlRule', + 'siem.indicatorRule', + 'siem.queryRule', + 'siem.savedQueryRule', + 'siem.thresholdRule', + ], + ruleStatus: 'partial failure', + }); + expect(result).toEqual>({ + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.mlRule': { + filter: { + term: { + 'rule.category': 'siem.mlRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.indicatorRule': { + filter: { + term: { + 'rule.category': 'siem.indicatorRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.queryRule': { + filter: { + term: { + 'rule.category': 'siem.queryRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.savedQueryRule': { + filter: { + term: { + 'rule.category': 'siem.savedQueryRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.thresholdRule': { + filter: { + term: { + 'rule.category': 'siem.thresholdRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types.ts new file mode 100644 index 0000000000000..e046fcbe75c76 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types.ts @@ -0,0 +1,34 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { RuleTypeId } from '@kbn/securitysolution-rules'; +import type { RuleStatus } from '../../types'; +import { getEventLogAggByRuleType } from './get_event_log_agg_by_rule_type'; + +export interface GetEventLogAggByRuleTypesOptions { + ruleTypes: RuleTypeId[]; + ruleStatus: RuleStatus; +} + +/** + * Given an array of rule types such as "siem.eqlRule" | "siem.mlRule", etc.. and + * a rule status such as "succeeded" | "partial failure" | "failed", this will return + * aggregations for querying and categorizing them. + * @param ruleType The rule type such as "siem.eqlRule" | "siem.mlRule" etc... + * @param ruleStatus The rule status such as "succeeded" | "partial failure" | "failed" + * @returns The aggregation by rule types + */ +export const getEventLogAggByRuleTypes = ({ + ruleTypes, + ruleStatus, +}: GetEventLogAggByRuleTypesOptions): Record => { + return ruleTypes.reduce>((accum, ruleType) => { + accum[ruleType] = getEventLogAggByRuleType({ ruleType, ruleStatus }); + return accum; + }, {}); +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts new file mode 100644 index 0000000000000..22261ac48812c --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts @@ -0,0 +1,270 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getEventLogAggByRuleTypesMetrics } from './get_event_log_agg_by_rule_types_metrics'; + +describe('get_event_log_agg_by_rule_types_metrics', () => { + test('returns empty object when given an empty array', () => { + const result = getEventLogAggByRuleTypesMetrics([]); + expect(result).toEqual>({}); + }); + + test('returns expected aggregation when given a single ruleType ', () => { + const result = getEventLogAggByRuleTypesMetrics(['siem.eqlRule']); + expect(result).toEqual>({ + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }, + }); + }); + + test('returns same aggregation if the same string is repeated in the array', () => { + const result = getEventLogAggByRuleTypesMetrics(['siem.eqlRule', 'siem.eqlRule']); + expect(result).toEqual>({ + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }, + }); + }); + + test('returns 2 expected aggregations when given 2 ruleTypes ', () => { + const result = getEventLogAggByRuleTypesMetrics(['siem.eqlRule', 'siem.indicatorRule']); + expect(result).toEqual>({ + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }, + 'siem.indicatorRule': { + filter: { + term: { + 'rule.category': 'siem.indicatorRule', + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.ts new file mode 100644 index 0000000000000..425a16bffacd0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.ts @@ -0,0 +1,19 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/types'; +import type { RuleTypeId } from '@kbn/securitysolution-rules'; +import { getEventLogAggByRuleTypeMetrics } from './get_event_log_agg_by_rule_type_metrics'; + +export const getEventLogAggByRuleTypesMetrics = ( + ruleTypes: RuleTypeId[] +): Record => { + return ruleTypes.reduce>((accum, ruleType) => { + accum[ruleType] = getEventLogAggByRuleTypeMetrics(ruleType); + return accum; + }, {}); +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_status.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_status.test.ts new file mode 100644 index 0000000000000..fad4d5aa35c34 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_status.test.ts @@ -0,0 +1,380 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getEventLogAggByStatus } from './get_event_log_agg_by_status'; + +describe('get_event_log_agg_by_status', () => { + test('returns empty aggregation if ruleTypes is an empty array', () => { + const result = getEventLogAggByStatus({ ruleStatus: 'succeeded', ruleTypes: [] }); + expect(result).toEqual({ + filter: { + term: { + 'kibana.alert.rule.execution.status': 'succeeded', + }, + }, + aggs: {}, + }); + }); + + test('returns 1 aggregation if ruleTypes has 1 element', () => { + const result = getEventLogAggByStatus({ ruleStatus: 'succeeded', ruleTypes: ['siem.eqlRule'] }); + expect(result).toEqual({ + filter: { + term: { + 'kibana.alert.rule.execution.status': 'succeeded', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }, + }); + }); + + test('returns 1 aggregation merged if ruleTypes has same element twice', () => { + const result = getEventLogAggByStatus({ + ruleStatus: 'succeeded', + ruleTypes: ['siem.eqlRule', 'siem.eqlRule'], + }); + expect(result).toEqual({ + filter: { + term: { + 'kibana.alert.rule.execution.status': 'succeeded', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }, + }); + }); + + test('returns 2 aggregations if ruleTypes has 2 different elements', () => { + const result = getEventLogAggByStatus({ + ruleStatus: 'succeeded', + ruleTypes: ['siem.eqlRule', 'siem.indicatorRule'], + }); + expect(result).toEqual({ + filter: { + term: { + 'kibana.alert.rule.execution.status': 'succeeded', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.indicatorRule': { + filter: { + term: { + 'rule.category': 'siem.indicatorRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }, + }); + }); + + test('returns 2 aggregations if ruleTypes has 2 different elements with "categorization" if ruleStatus is "failed', () => { + const result = getEventLogAggByStatus({ + ruleStatus: 'failed', + ruleTypes: ['siem.eqlRule', 'siem.indicatorRule'], + }); + expect(result).toEqual({ + filter: { + term: { + 'kibana.alert.rule.execution.status': 'failed', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.indicatorRule': { + filter: { + term: { + 'rule.category': 'siem.indicatorRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }, + }); + }); + + test('returns 2 aggregations if ruleTypes has 2 different elements with "categorization" if ruleStatus is "partial failure', () => { + const result = getEventLogAggByStatus({ + ruleStatus: 'partial failure', + ruleTypes: ['siem.eqlRule', 'siem.indicatorRule'], + }); + expect(result).toEqual({ + filter: { + term: { + 'kibana.alert.rule.execution.status': 'partial failure', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.indicatorRule': { + filter: { + term: { + 'rule.category': 'siem.indicatorRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }, + }); + }); + + test('returns 7 aggregations if ruleTypes has 7 different elements with "categorization" if ruleStatus is "failed', () => { + const result = getEventLogAggByStatus({ + ruleStatus: 'partial failure', + ruleTypes: [ + 'siem.eqlRule', + 'siem.indicatorRule', + 'siem.thresholdRule', + 'siem.indicatorRule', + 'siem.mlRule', + 'siem.queryRule', + 'siem.savedQueryRule', + ], + }); + expect(result).toEqual({ + filter: { + term: { + 'kibana.alert.rule.execution.status': 'partial failure', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.indicatorRule': { + filter: { + term: { + 'rule.category': 'siem.indicatorRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.thresholdRule': { + filter: { + term: { + 'rule.category': 'siem.thresholdRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.mlRule': { + filter: { + term: { + 'rule.category': 'siem.mlRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.queryRule': { + filter: { + term: { + 'rule.category': 'siem.queryRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.savedQueryRule': { + filter: { + term: { + 'rule.category': 'siem.savedQueryRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_status.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_status.ts new file mode 100644 index 0000000000000..7d878260ef5be --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_status.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { RuleTypeId } from '@kbn/securitysolution-rules'; +import type { RuleStatus } from '../../types'; +import { getEventLogAggByRuleTypes } from './get_event_log_agg_by_rule_types'; + +interface GetEventLogAggByStatusOptions { + ruleStatus: RuleStatus; + ruleTypes: RuleTypeId[]; +} + +/** + * Given an array of rule types such as "siem.eqlRule" | "siem.mlRule", etc.. and + * a rule status such as "succeeded" | "partial failure" | "failed", this will return + * aggregations for querying and categorizing them. + * @param ruleType The rule type such as "siem.eqlRule" | "siem.mlRule" etc... + * @param ruleStatus The rule status such as "succeeded" | "partial failure" | "failed" + * @returns The aggregation by rule status + */ +export const getEventLogAggByStatus = ({ + ruleStatus, + ruleTypes, +}: GetEventLogAggByStatusOptions): AggregationsAggregationContainer => { + const aggs = getEventLogAggByRuleTypes({ ruleTypes, ruleStatus }); + return { + filter: { + term: { + 'kibana.alert.rule.execution.status': ruleStatus, + }, + }, + aggs, + }; +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts new file mode 100644 index 0000000000000..7d474769bd79f --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts @@ -0,0 +1,486 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getEventLogAggByStatuses } from './get_event_log_agg_by_statuses'; + +describe('get_event_log_agg_by_statuses', () => { + test('returns empty aggregations with empty array', () => { + const result = getEventLogAggByStatuses({ ruleStatuses: [], ruleTypes: [] }); + expect(result).toEqual>({ + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + aggs: {}, + }, + eventActionExecutionMetrics: { + filter: { + term: { + 'event.action': 'execution-metrics', + }, + }, + aggs: {}, + }, + }); + }); + + test('returns partial empty aggregations when ruleStatuses has a value', () => { + const result = getEventLogAggByStatuses({ ruleStatuses: ['failed'], ruleTypes: [] }); + expect(result).toEqual>({ + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + aggs: { + failed: { + filter: { + term: { + 'kibana.alert.rule.execution.status': 'failed', + }, + }, + aggs: {}, + }, + }, + }, + eventActionExecutionMetrics: { + filter: { + term: { + 'event.action': 'execution-metrics', + }, + }, + aggs: {}, + }, + }); + }); + + test('returns partial empty aggregations when ruleTypes has a value', () => { + const result = getEventLogAggByStatuses({ ruleStatuses: [], ruleTypes: ['siem.eqlRule'] }); + expect(result).toEqual>({ + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + aggs: {}, + }, + eventActionExecutionMetrics: { + filter: { + term: { + 'event.action': 'execution-metrics', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }, + }, + }, + }); + }); + + test('returns single aggregation when both ruleStatuses and ruleTypes has a single value', () => { + const result = getEventLogAggByStatuses({ + ruleStatuses: ['succeeded'], + ruleTypes: ['siem.eqlRule'], + }); + expect(result).toEqual>({ + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + aggs: { + succeeded: { + filter: { + term: { + 'kibana.alert.rule.execution.status': 'succeeded', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }, + }, + }, + }, + eventActionExecutionMetrics: { + filter: { + term: { + 'event.action': 'execution-metrics', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }, + }, + }, + }); + }); + + test('returns aggregations when both ruleStatuses and ruleTypes both have multiple values', () => { + const result = getEventLogAggByStatuses({ + ruleStatuses: ['succeeded', 'failed'], + ruleTypes: ['siem.eqlRule', 'siem.thresholdRule'], + }); + expect(result).toEqual>({ + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + aggs: { + succeeded: { + filter: { + term: { + 'kibana.alert.rule.execution.status': 'succeeded', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.thresholdRule': { + filter: { + term: { + 'rule.category': 'siem.thresholdRule', + }, + }, + aggs: { + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }, + }, + failed: { + filter: { + term: { + 'kibana.alert.rule.execution.status': 'failed', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + 'siem.thresholdRule': { + filter: { + term: { + 'rule.category': 'siem.thresholdRule', + }, + }, + aggs: { + categories: { + categorize_text: { + size: 10, + field: 'message', + }, + }, + cardinality: { + cardinality: { + field: 'rule.id', + }, + }, + }, + }, + }, + }, + }, + }, + eventActionExecutionMetrics: { + filter: { + term: { + 'event.action': 'execution-metrics', + }, + }, + aggs: { + 'siem.eqlRule': { + filter: { + term: { + 'rule.category': 'siem.eqlRule', + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }, + 'siem.thresholdRule': { + filter: { + term: { + 'rule.category': 'siem.thresholdRule', + }, + }, + aggs: { + gapCount: { + cardinality: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxGapDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + minGapDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + avgGapDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.execution_gap_duration_s', + }, + }, + maxTotalIndexDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + minTotalIndexDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + avgTotalIndexDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_indexing_duration_ms', + }, + }, + maxTotalSearchDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + minTotalSearchDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + avgTotalSearchDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', + }, + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.ts new file mode 100644 index 0000000000000..c7431eaf77142 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.ts @@ -0,0 +1,57 @@ +/* + * 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { RuleTypeId } from '@kbn/securitysolution-rules'; +import type { RuleStatus } from '../../types'; +import { getEventLogAggByRuleTypesMetrics } from './get_event_log_agg_by_rule_types_metrics'; +import { getEventLogAggByStatus } from './get_event_log_agg_by_status'; + +interface GetEventLogAggByStatusesOptions { + ruleStatuses: RuleStatus[]; + ruleTypes: RuleTypeId[]; +} + +/** + * Given an array of rule types such as "siem.eqlRule" | "siem.mlRule", etc.. and + * a rule status such as "succeeded" | "partial failure" | "failed", this will return + * aggregations for querying and categorizing them. + * @param ruleType The rule type such as "siem.eqlRule" | "siem.mlRule" etc... + * @param ruleStatus The rule status such as "succeeded" | "partial failure" | "failed" + * @returns The aggregation by rule status + */ +export const getEventLogAggByStatuses = ({ + ruleStatuses, + ruleTypes, +}: GetEventLogAggByStatusesOptions): Record => { + const eventActionStatusChangeAggs = ruleStatuses.reduce< + Record + >((accum, ruleStatus) => { + accum[ruleStatus] = getEventLogAggByStatus({ ruleStatus, ruleTypes }); + return accum; + }, {}); + + const actionExecutionAggs = getEventLogAggByRuleTypesMetrics(ruleTypes); + return { + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + aggs: eventActionStatusChangeAggs, + }, + eventActionExecutionMetrics: { + filter: { + term: { + 'event.action': 'execution-metrics', + }, + }, + aggs: actionExecutionAggs, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_by_type_and_status.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_by_type_and_status.test.ts new file mode 100644 index 0000000000000..a1d57387bfb60 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_by_type_and_status.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { EventLogStatusMetric } from '../../detections/rules/types'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import { getInitialEventLogUsage } from '../../detections/rules/get_initial_usage'; +import { + getAllEventLogTransform, + getElasticLogCustomRules, + getEmptyElasticLogCustomRules, + getEmptyEventLogAllRules, + getEmptyEventLogElasticRules, + getEventLogAllRules, + getEventLogElasticRules, +} from '../../detections/rules/get_metrics.mocks'; +import { getEventLogByTypeAndStatus } from '../get_event_log_by_type_and_status'; + +describe('get_event_log_by_type_and_status', () => { + let esClient: ReturnType; + + beforeEach(() => { + esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + }); + + test('returns initial event log usage results if there are no rule results', async () => { + const logger = loggingSystemMock.createLogger(); + esClient.search.mockResponseOnce(getEmptyEventLogAllRules()); + esClient.search.mockResponseOnce(getEmptyEventLogElasticRules()); + esClient.search.mockResponseOnce(getEmptyElasticLogCustomRules()); + + const result = await getEventLogByTypeAndStatus({ + logger, + eventLogIndex: 'test', + esClient, + ruleResults: [], + }); + expect(result).toEqual(getInitialEventLogUsage()); + }); + + test('returns initial event log usage results if an exception is thrown by Elasticsearch', async () => { + const logger = loggingSystemMock.createLogger(); + esClient.search.mockRejectedValue(new Error('Some error')); + + const result = await getEventLogByTypeAndStatus({ + logger, + eventLogIndex: 'test', + esClient, + ruleResults: [], + }); + expect(logger.debug).toHaveBeenCalledWith( + 'Error trying to get event log by type and status. Error message is: "Some error". Error is: "Error: Some error". Returning empty initialized object.' + ); + expect(result).toEqual(getInitialEventLogUsage()); + }); + + test('returns results transformed if given valid input from Elasticsearch', async () => { + const logger = loggingSystemMock.createLogger(); + esClient.search.mockResponseOnce(getEventLogAllRules()); + esClient.search.mockResponseOnce(getEventLogElasticRules()); + esClient.search.mockResponseOnce(getElasticLogCustomRules()); + + const result = await getEventLogByTypeAndStatus({ + logger, + eventLogIndex: 'test', + esClient, + ruleResults: [], + }); + expect(result).toEqual(getAllEventLogTransform()); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.test.ts new file mode 100644 index 0000000000000..a431e7a07990a --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getSearchForAllRules } from './get_search_for_all_rules'; + +describe('get_search_for_all_rules', () => { + test('it returns query merged with an aggregation sent in', () => { + const result = getSearchForAllRules({ + eventLogIndex: 'test-123', + aggs: { + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + }, + }, + }); + expect(result).toEqual({ + index: 'test-123', + size: 0, + track_total_hits: false, + aggs: { + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + }, + }, + query: { + bool: { + filter: { + bool: { + must: [ + { + range: { + '@timestamp': { + gte: 'now-24h', + lte: 'now', + }, + }, + }, + { + term: { + 'event.provider': 'securitySolution.ruleExecution', + }, + }, + ], + }, + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.ts new file mode 100644 index 0000000000000..c74ce1a2262c3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.ts @@ -0,0 +1,51 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { RULE_EXECUTION_LOG_PROVIDER } from '../../../lib/detection_engine/rule_execution_log/event_log/constants'; + +/** + * Given an aggregation of "aggs" this will return a search for all rules within 24 hours. + * @param eventLogIndex The event log index such as ".kibana-event-log-8.2.0*" + * @param aggs The aggregation to break things down by + */ +export const getSearchForAllRules = ({ + eventLogIndex, + aggs, +}: { + eventLogIndex: string; + aggs: Record; +}): SearchRequest => ({ + index: eventLogIndex, + size: 0, + track_total_hits: false, + aggs, + query: { + bool: { + filter: { + bool: { + must: [ + { + range: { + '@timestamp': { + gte: 'now-24h', + lte: 'now', + }, + }, + }, + { + term: { + 'event.provider': RULE_EXECUTION_LOG_PROVIDER, + }, + }, + ], + }, + }, + }, + }, +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.test.ts new file mode 100644 index 0000000000000..8d2aa362ed606 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getSearchForCustomRules } from './get_search_for_custom_rules'; + +describe('get_search_for_custom_rules', () => { + test('it returns query merged with an aggregation sent in and list of elastic ids', () => { + const result = getSearchForCustomRules({ + elasticRuleIds: ['test-123', 'test-456'], + eventLogIndex: 'test-123', + aggs: { + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + }, + }, + }); + expect(result).toEqual({ + index: 'test-123', + size: 0, + track_total_hits: false, + aggs: { + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + }, + }, + query: { + bool: { + filter: { + bool: { + must: [ + { + range: { + '@timestamp': { + gte: 'now-24h', + lte: 'now', + }, + }, + }, + { + term: { + 'event.provider': 'securitySolution.ruleExecution', + }, + }, + ], + must_not: [ + { + terms: { + 'rule.id': ['test-123', 'test-456'], + }, + }, + ], + }, + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.ts new file mode 100644 index 0000000000000..88f70469e57ee --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.ts @@ -0,0 +1,64 @@ +/* + * 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 { + AggregationsAggregationContainer, + SearchRequest, +} from '@elastic/elasticsearch/lib/api/types'; +import { RULE_EXECUTION_LOG_PROVIDER } from '../../../lib/detection_engine/rule_execution_log/event_log/constants'; + +/** + * Given an aggregation of "aggs" this will return a search for rules that are NOT elastic + * rules within 24 hours. + * @param eventLogIndex The event log index such as ".kibana-event-log-8.2.0*" + * @param aggs The aggregation to break things down by + * @param elasticRuleIds Array of elastic rule ids to exclude + */ +export const getSearchForCustomRules = ({ + eventLogIndex, + aggs, + elasticRuleIds, +}: { + eventLogIndex: string; + aggs: Record; + elasticRuleIds: string[]; +}): SearchRequest => ({ + index: eventLogIndex, + size: 0, + track_total_hits: false, + aggs, + query: { + bool: { + filter: { + bool: { + must: [ + { + range: { + '@timestamp': { + gte: 'now-24h', + lte: 'now', + }, + }, + }, + { + term: { + 'event.provider': RULE_EXECUTION_LOG_PROVIDER, + }, + }, + ], + must_not: [ + { + terms: { + 'rule.id': elasticRuleIds, + }, + }, + ], + }, + }, + }, + }, +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.test.ts new file mode 100644 index 0000000000000..0c6e3e1bd5ee4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getSearchForElasticRules } from './get_search_for_elastic_rules'; + +describe('get_search_for_elastic_rules', () => { + test('it returns query merged with an aggregation sent in and list of elastic ids', () => { + const result = getSearchForElasticRules({ + elasticRuleIds: ['test-123', 'test-456'], + eventLogIndex: 'test-123', + aggs: { + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + }, + }, + }); + expect(result).toEqual({ + index: 'test-123', + size: 0, + track_total_hits: false, + aggs: { + eventActionStatusChange: { + filter: { + term: { + 'event.action': 'status-change', + }, + }, + }, + }, + query: { + bool: { + filter: { + bool: { + must: [ + { + range: { + '@timestamp': { + gte: 'now-24h', + lte: 'now', + }, + }, + }, + { + term: { + 'event.provider': 'securitySolution.ruleExecution', + }, + }, + { + terms: { + 'rule.id': ['test-123', 'test-456'], + }, + }, + ], + }, + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.ts new file mode 100644 index 0000000000000..30dc61499b17f --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.ts @@ -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 { + AggregationsAggregationContainer, + SearchRequest, +} from '@elastic/elasticsearch/lib/api/types'; +import { RULE_EXECUTION_LOG_PROVIDER } from '../../../lib/detection_engine/rule_execution_log/event_log/constants'; + +/** + * Given an aggregation of "aggs" this will return a search for rules that are elastic + * rules within 24 hours. + * @param eventLogIndex The event log index such as ".kibana-event-log-8.2.0*" + * @param aggs The aggregation to break things down by + * @param elasticRuleIds Array of elastic rule ids to include + */ +export const getSearchForElasticRules = ({ + eventLogIndex, + aggs, + elasticRuleIds, +}: { + eventLogIndex: string; + aggs: Record; + elasticRuleIds: string[]; +}): SearchRequest => ({ + index: eventLogIndex, + size: 0, + track_total_hits: false, + aggs, + query: { + bool: { + filter: { + bool: { + must: [ + { + range: { + '@timestamp': { + gte: 'now-24h', + lte: 'now', + }, + }, + }, + { + term: { + 'event.provider': RULE_EXECUTION_LOG_PROVIDER, + }, + }, + { + terms: { + 'rule.id': elasticRuleIds, + }, + }, + ], + }, + }, + }, + }, +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_categories.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_categories.test.ts new file mode 100644 index 0000000000000..234bd2a2aa6ec --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_categories.test.ts @@ -0,0 +1,149 @@ +/* + * 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 { FailureMessage } from '../../detections/rules/types'; +import { transformCategories } from './transform_categories'; + +describe('transform_categories', () => { + test('it transforms an empty array into an empty object', () => { + const result = transformCategories({ + buckets: [], + }); + expect(result).toEqual([]); + }); + + test('it transforms a single element into a single output', () => { + const result = transformCategories({ + buckets: [ + { + doc_count: 6, + key: 'category-1', + }, + ], + }); + expect(result).toEqual([ + { + count: 6, + message: 'category-1', + }, + ]); + }); + + test('it transforms 2 elements into 2 outputs', () => { + const result = transformCategories({ + buckets: [ + { + doc_count: 6, + key: 'category-1', + }, + { + doc_count: 5, + key: 'category-2', + }, + ], + }); + expect(result).toEqual([ + { + count: 6, + message: 'category-1', + }, + { + count: 5, + message: 'category-2', + }, + ]); + }); + + test('it transforms 10 elements into 10 outputs', () => { + const result = transformCategories({ + buckets: [ + { + doc_count: 10, + key: 'category-10', + }, + { + doc_count: 9, + key: 'category-9', + }, + { + doc_count: 8, + key: 'category-8', + }, + { + doc_count: 7, + key: 'category-7', + }, + { + doc_count: 6, + key: 'category-6', + }, + { + doc_count: 5, + key: 'category-5', + }, + { + doc_count: 4, + key: 'category-4', + }, + { + doc_count: 3, + key: 'category-3', + }, + { + doc_count: 2, + key: 'category-2', + }, + { + doc_count: 1, + key: 'category-1', + }, + ], + }); + expect(result).toEqual([ + { + message: 'category-10', + count: 10, + }, + { + message: 'category-9', + count: 9, + }, + { + message: 'category-8', + count: 8, + }, + { + message: 'category-7', + count: 7, + }, + { + message: 'category-6', + count: 6, + }, + { + message: 'category-5', + count: 5, + }, + { + message: 'category-4', + count: 4, + }, + { + message: 'category-3', + count: 3, + }, + { + message: 'category-2', + count: 2, + }, + { + message: 'category-1', + count: 1, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_categories.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_categories.ts new file mode 100644 index 0000000000000..9dbbf7ba4bdb7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_categories.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FailureMessage } from '../../detections/rules/types'; +import type { Categories } from '../../types'; +import { transformCategoryBucket } from './transform_category_bucket'; + +/** + * Given a set of categories from a categorization aggregation this will + * return those transformed. + * @param categories The categories to transform + * @see https://www.elastic.co/guide/en/elasticsearch/reference/8.1/search-aggregations-bucket-categorize-text-aggregation.html + * @returns the categories transformed + */ +export const transformCategories = (categories: Categories): FailureMessage[] => { + return categories.buckets.map((bucket) => transformCategoryBucket(bucket)); +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_category_bucket.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_category_bucket.test.ts new file mode 100644 index 0000000000000..dc5edb779bef3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_category_bucket.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { FailureMessage } from '../../detections/rules/types'; +import { transformCategoryBucket } from './transform_category_bucket'; + +describe('transform_category_bucket', () => { + test('it will transform a bucket sent in', () => { + const result = transformCategoryBucket({ + key: 'test-123', + doc_count: 10, + }); + expect(result).toEqual({ + message: 'test-123', + count: 10, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_category_bucket.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_category_bucket.ts new file mode 100644 index 0000000000000..8b1c5e318db38 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_category_bucket.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 type { FailureMessage } from '../../detections/rules/types'; +import type { Categories } from '../../types'; + +/** + * Given a category from a categorization aggregation this will + * return it transformed. + * @param bucket The category bucket to transform + * @see https://www.elastic.co/guide/en/elasticsearch/reference/8.1/search-aggregations-bucket-categorize-text-aggregation.html + * @returns the bucket transformed + */ +export const transformCategoryBucket = (bucket: Categories['buckets'][0]): FailureMessage => { + return { message: bucket.key, count: bucket.doc_count }; +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_event_log_type_status.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_event_log_type_status.test.ts new file mode 100644 index 0000000000000..d7299fdb375b2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_event_log_type_status.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { SingleEventLogStatusMetric } from '../../detections/rules/types'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { + getElasticLogCustomRules, + getEventLogAllRules, + getEventLogAllRulesResult, + getEventLogCustomRulesResult, + getEventLogElasticRules, + getEventLogElasticRulesResult, +} from '../../detections/rules/get_metrics.mocks'; +import { transformEventLogTypeStatus } from './transform_event_log_type_status'; + +describe('transform_event_log_type_status', () => { + test('returns expected transform for all rules results', () => { + const logger = loggingSystemMock.createLogger(); + const result = transformEventLogTypeStatus({ + logger, + aggs: getEventLogAllRules().aggregations, + }); + expect(result).toEqual(getEventLogAllRulesResult()); + }); + + test('returns expected transform for elastic rules results', () => { + const logger = loggingSystemMock.createLogger(); + const result = transformEventLogTypeStatus({ + logger, + aggs: getEventLogElasticRules().aggregations, + }); + expect(result).toEqual(getEventLogElasticRulesResult()); + }); + + test('returns expected transform for custom rules results', () => { + const logger = loggingSystemMock.createLogger(); + const result = transformEventLogTypeStatus({ + logger, + aggs: getElasticLogCustomRules().aggregations, + }); + expect(result).toEqual(getEventLogCustomRulesResult()); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_event_log_type_status.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_event_log_type_status.ts new file mode 100644 index 0000000000000..b9b2cba496dfd --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_event_log_type_status.ts @@ -0,0 +1,138 @@ +/* + * 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 { Logger } from 'kibana/server'; +import type { EventLogTypeStatusAggs } from '../../types'; +import type { SingleEventLogStatusMetric } from '../../detections/rules/types'; +import { getInitialSingleEventLogUsage } from '../../detections/rules/get_initial_usage'; +import { countTotals } from './count_totals'; +import { transformSingleRuleMetric } from './transform_single_rule_metric'; + +export interface TransformEventLogTypeStatusOptions { + logger: Logger; + aggs: EventLogTypeStatusAggs | undefined; +} + +/** + * Given a raw Elasticsearch aggregation against the event log this will transform that + * for telemetry. This expects the aggregation to be broken down by "ruleType" and "ruleStatus". + * @param aggs The Elasticsearch aggregations broken down by "ruleType" and "ruleStatus" + * @params logger The kibana logger + * @returns The single metric from the aggregation broken down + */ +export const transformEventLogTypeStatus = ({ + aggs, + logger, +}: TransformEventLogTypeStatusOptions): SingleEventLogStatusMetric => { + // early return if the aggs are empty/null + if (aggs == null) { + logger.debug( + 'Was expecting aggregations to exist for "transformEventLogTypeStatus", returning empty metrics instead' + ); + return getInitialSingleEventLogUsage(); + } + + // metrics + const eqlMetrics = aggs.eventActionExecutionMetrics['siem.eqlRule']; + const indicatorMetrics = aggs.eventActionExecutionMetrics['siem.indicatorRule']; + const mlMetrics = aggs.eventActionExecutionMetrics['siem.mlRule']; + const queryMetrics = aggs.eventActionExecutionMetrics['siem.queryRule']; + const savedQueryMetrics = aggs.eventActionExecutionMetrics['siem.savedQueryRule']; + const thresholdMetrics = aggs.eventActionExecutionMetrics['siem.thresholdRule']; + + // failure status + const eqlFailure = aggs.eventActionStatusChange.failed['siem.eqlRule']; + const indicatorFailure = aggs.eventActionStatusChange.failed['siem.indicatorRule']; + const mlFailure = aggs.eventActionStatusChange.failed['siem.mlRule']; + const queryFailure = aggs.eventActionStatusChange.failed['siem.queryRule']; + const savedQueryFailure = aggs.eventActionStatusChange.failed['siem.savedQueryRule']; + const thresholdFailure = aggs.eventActionStatusChange.failed['siem.thresholdRule']; + + // partial failure + const eqlPartialFailure = aggs.eventActionStatusChange['partial failure']['siem.eqlRule']; + const indicatorPartialFailure = + aggs.eventActionStatusChange['partial failure']['siem.indicatorRule']; + const mlPartialFailure = aggs.eventActionStatusChange['partial failure']['siem.mlRule']; + const queryPartialFailure = aggs.eventActionStatusChange['partial failure']['siem.queryRule']; + const savedQueryPartialFailure = + aggs.eventActionStatusChange['partial failure']['siem.savedQueryRule']; + const thresholdPartialFailure = + aggs.eventActionStatusChange['partial failure']['siem.thresholdRule']; + + // success + const eqlSuccess = aggs.eventActionStatusChange.succeeded['siem.eqlRule']; + const indicatorSuccess = aggs.eventActionStatusChange.succeeded['siem.indicatorRule']; + const mlSuccess = aggs.eventActionStatusChange.succeeded['siem.mlRule']; + const querySuccess = aggs.eventActionStatusChange.succeeded['siem.queryRule']; + const savedQuerySuccess = aggs.eventActionStatusChange.succeeded['siem.savedQueryRule']; + const thresholdSuccess = aggs.eventActionStatusChange.succeeded['siem.thresholdRule']; + + return { + eql: transformSingleRuleMetric({ + failed: eqlFailure, + partialFailed: eqlPartialFailure, + succeeded: eqlSuccess, + singleMetric: eqlMetrics, + }), + threat_match: transformSingleRuleMetric({ + failed: indicatorFailure, + partialFailed: indicatorPartialFailure, + succeeded: indicatorSuccess, + singleMetric: indicatorMetrics, + }), + machine_learning: transformSingleRuleMetric({ + failed: mlFailure, + partialFailed: mlPartialFailure, + succeeded: mlSuccess, + singleMetric: mlMetrics, + }), + query: transformSingleRuleMetric({ + failed: queryFailure, + partialFailed: queryPartialFailure, + succeeded: querySuccess, + singleMetric: queryMetrics, + }), + saved_query: transformSingleRuleMetric({ + failed: savedQueryFailure, + partialFailed: savedQueryPartialFailure, + succeeded: savedQuerySuccess, + singleMetric: savedQueryMetrics, + }), + threshold: transformSingleRuleMetric({ + failed: thresholdFailure, + partialFailed: thresholdPartialFailure, + succeeded: thresholdSuccess, + singleMetric: thresholdMetrics, + }), + total: { + failures: countTotals([ + eqlFailure, + indicatorFailure, + mlFailure, + queryFailure, + savedQueryFailure, + thresholdFailure, + ]), + partial_failures: countTotals([ + eqlPartialFailure, + indicatorPartialFailure, + mlPartialFailure, + queryPartialFailure, + savedQueryPartialFailure, + thresholdPartialFailure, + ]), + succeeded: countTotals([ + eqlSuccess, + indicatorSuccess, + mlSuccess, + querySuccess, + savedQuerySuccess, + thresholdSuccess, + ]), + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts new file mode 100644 index 0000000000000..c64f0833fe851 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { SingleEventMetric } from '../../detections/rules/types'; +import { transformSingleRuleMetric } from './transform_single_rule_metric'; + +describe('transform_single_rule_metric', () => { + test('it transforms a single metric correctly', () => { + const result = transformSingleRuleMetric({ + failed: { + doc_count: 325, + categories: { + buckets: [ + { + doc_count: 163, + key: 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled', + }, + { + doc_count: 162, + key: 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent', + }, + ], + }, + cardinality: { + value: 2, + }, + }, + partialFailed: { + doc_count: 325, + categories: { + buckets: [ + { + doc_count: 163, + key: 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled', + }, + { + doc_count: 162, + key: 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent', + }, + ], + }, + cardinality: { + value: 2, + }, + }, + succeeded: { + doc_count: 317, + cardinality: { + value: 5, + }, + }, + singleMetric: { + doc_count: 5, + maxTotalIndexDuration: { + value: 5, + }, + avgTotalIndexDuration: { + value: 3, + }, + minTotalIndexDuration: { + value: 2, + }, + gapCount: { + value: 4, + }, + maxGapDuration: { + value: 8, + }, + avgGapDuration: { + value: 2, + }, + minGapDuration: { + value: 9, + }, + maxTotalSearchDuration: { + value: 4, + }, + avgTotalSearchDuration: { + value: 2, + }, + minTotalSearchDuration: { + value: 12, + }, + }, + }); + + expect(result).toEqual({ + failures: 2, + top_failures: [ + { + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled', + count: 163, + }, + { + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent', + count: 162, + }, + ], + partial_failures: 2, + top_partial_failures: [ + { + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching blah frank was found This warning will continue to appear until matching index is created or this rule is disabled', + count: 163, + }, + { + message: + 'This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching logs-endpoint.alerts was found This warning will continue to appear until matching index is created or this rule is disabled If you have recently enrolled agents enabled with Endpoint Security through Fleet this warning should stop once an alert is sent from an agent', + count: 162, + }, + ], + succeeded: 5, + index_duration: { + max: 5, + avg: 3, + min: 2, + }, + search_duration: { + max: 4, + avg: 2, + min: 12, + }, + gap_duration: { + max: 8, + avg: 2, + min: 9, + }, + gap_count: 4, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts new file mode 100644 index 0000000000000..bebd867fb195f --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts @@ -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 type { SingleEventMetric } from '../../detections/rules/types'; +import type { + CountCardinality, + CountCardinalityWithCategories, + SingleExecutionMetricAgg, +} from '../../types'; +import { transformCategories } from './transform_categories'; + +export interface TransformSingleRuleMetricOptions { + failed: CountCardinalityWithCategories; + partialFailed: CountCardinalityWithCategories; + succeeded: CountCardinality; + singleMetric: SingleExecutionMetricAgg; +} + +/** + * Given a different count cardinalities this will return them broken down by various + * metrics such as "failed", "partial failed", "succeeded, and will list a top 10 of each + * of the error message types. + * @param failed The failed counts and top 10 "messages" + * @param partialFailed The partial failed counts and top 10 "messages" + * @param succeeded The succeeded counts + * @param singleMetric The max/min/avg metric + * @returns The single metric from the aggregation broken down + */ +export const transformSingleRuleMetric = ({ + failed, + partialFailed, + succeeded, + singleMetric, +}: TransformSingleRuleMetricOptions): SingleEventMetric => { + return { + failures: failed.cardinality.value ?? 0, + top_failures: transformCategories(failed.categories), + partial_failures: partialFailed.cardinality.value ?? 0, + top_partial_failures: transformCategories(partialFailed.categories), + succeeded: succeeded.cardinality.value ?? 0, + index_duration: { + max: singleMetric.maxTotalIndexDuration.value ?? 0.0, + avg: singleMetric.avgTotalIndexDuration.value ?? 0.0, + min: singleMetric.minTotalIndexDuration.value ?? 0.0, + }, + search_duration: { + max: singleMetric.maxTotalSearchDuration.value ?? 0.0, + avg: singleMetric.avgTotalSearchDuration.value ?? 0.0, + min: singleMetric.minTotalSearchDuration.value ?? 0.0, + }, + gap_duration: { + max: singleMetric.maxGapDuration.value ?? 0.0, + avg: singleMetric.avgGapDuration.value ?? 0.0, + min: singleMetric.minGapDuration.value ?? 0.0, + }, + gap_count: singleMetric.gapCount.value ?? 0.0, + }; +}; diff --git a/x-pack/plugins/security_solution/server/usage/types.ts b/x-pack/plugins/security_solution/server/usage/types.ts index f591ffd8f422e..3bc2235f9c624 100644 --- a/x-pack/plugins/security_solution/server/usage/types.ts +++ b/x-pack/plugins/security_solution/server/usage/types.ts @@ -14,6 +14,7 @@ export type CollectorDependencies = { signalsIndex: string; core: CoreSetup; logger: Logger; + eventLogIndex: string; } & Pick; export interface AlertBucket { @@ -51,3 +52,91 @@ export type RuleSearchResult = Omit< createdAt: string; updatedAt: string; }; + +export type RuleStatus = 'running' | 'succeeded' | 'partial failure' | 'failed'; + +export interface CountCardinality { + doc_count: number; + cardinality: { + value: number | null; + }; +} + +export interface Categories { + buckets: Array<{ doc_count: number; key: string }>; +} +export interface CountCardinalityWithCategories extends CountCardinality { + categories: Categories; +} + +export interface SingleEVentLogTypeStatusAgg { + doc_count: number; + 'siem.queryRule': CountCardinality; + 'siem.savedQueryRule': CountCardinality; + 'siem.eqlRule': CountCardinality; + 'siem.thresholdRule': CountCardinality; + 'siem.mlRule': CountCardinality; + 'siem.indicatorRule': CountCardinality; +} + +export interface SingleEVentLogTypeStatusAggWithCategories { + doc_count: number; + 'siem.queryRule': CountCardinalityWithCategories; + 'siem.savedQueryRule': CountCardinalityWithCategories; + 'siem.eqlRule': CountCardinalityWithCategories; + 'siem.thresholdRule': CountCardinalityWithCategories; + 'siem.mlRule': CountCardinalityWithCategories; + 'siem.indicatorRule': CountCardinalityWithCategories; +} + +export interface SingleExecutionMetricAgg { + doc_count: number; + maxTotalIndexDuration: { + value: number | null; + }; + avgTotalIndexDuration: { + value: number | null; + }; + minTotalIndexDuration: { + value: number | null; + }; + gapCount: { + value: number | null; + }; + maxGapDuration: { + value: number | null; + }; + avgGapDuration: { + value: number | null; + }; + minGapDuration: { + value: number | null; + }; + maxTotalSearchDuration: { + value: number | null; + }; + avgTotalSearchDuration: { + value: number | null; + }; + minTotalSearchDuration: { + value: number | null; + }; +} + +export interface EventLogTypeStatusAggs { + eventActionStatusChange: { + doc_count: number; + 'partial failure': SingleEVentLogTypeStatusAggWithCategories; + failed: SingleEVentLogTypeStatusAggWithCategories; + succeeded: SingleEVentLogTypeStatusAgg; + }; + eventActionExecutionMetrics: { + doc_count: number; + 'siem.queryRule': SingleExecutionMetricAgg; + 'siem.savedQueryRule': SingleExecutionMetricAgg; + 'siem.eqlRule': SingleExecutionMetricAgg; + 'siem.thresholdRule': SingleExecutionMetricAgg; + 'siem.mlRule': SingleExecutionMetricAgg; + 'siem.indicatorRule': SingleExecutionMetricAgg; + }; +} diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 99b11d1d14cf6..1a3e32a3ccd6f 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -8855,6 +8855,2464 @@ } } } + }, + "detection_rule_status": { + "properties": { + "all_rules": { + "properties": { + "eql": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "threat_match": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "machine_learning": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "query": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "saved_query": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "threshold": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "total": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of succeeded rules" + } + } + } + } + } + }, + "elastic_rules": { + "properties": { + "eql": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "threat_match": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "machine_learning": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "query": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "saved_query": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "threshold": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "total": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of succeeded rules" + } + } + } + } + } + }, + "custom_rules": { + "properties": { + "eql": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "threat_match": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "machine_learning": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "query": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "saved_query": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "threshold": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "top_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "top_partial_failures": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "keyword", + "_meta": { + "description": "Failed rule message" + } + }, + "count": { + "type": "long", + "_meta": { + "description": "Number of times the message occurred" + } + } + } + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of successful rules" + } + }, + "index_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "search_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, + "gap_count": { + "type": "long", + "_meta": { + "description": "The count of gaps" + } + } + } + }, + "total": { + "properties": { + "failures": { + "type": "long", + "_meta": { + "description": "The number of failed rules" + } + }, + "partial_failures": { + "type": "long", + "_meta": { + "description": "The number of partial failure rules" + } + }, + "succeeded": { + "type": "long", + "_meta": { + "description": "The number of succeeded rules" + } + } + } + } + } + } + } } } }, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts index b93141a1ffe73..41415e8bafc1e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts @@ -34,6 +34,7 @@ import { updateRule, } from '../../../../utils'; import { getInitialDetectionMetrics } from '../../../../../../plugins/security_solution/server/usage/detections/get_initial_usage'; +import { getInitialEventLogUsage } from '../../../../../../plugins/security_solution/server/usage/detections/rules/get_initial_usage'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -66,6 +67,10 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -102,6 +107,10 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, log, 4, [id]); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -141,6 +150,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -174,6 +187,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -207,6 +224,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -240,6 +261,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -272,6 +297,10 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -308,6 +337,10 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, log, 4, [id]); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -347,6 +380,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -380,6 +417,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -413,6 +454,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -446,6 +491,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -484,6 +533,10 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -526,6 +579,10 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, log, 4, [id]); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -571,6 +628,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -610,6 +671,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -649,6 +714,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -688,6 +757,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -721,6 +794,10 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -756,6 +833,10 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -794,6 +875,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -826,6 +911,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -858,6 +947,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -890,6 +983,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -921,6 +1018,10 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -972,6 +1073,10 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, log, 4, [id]); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -1011,6 +1116,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -1059,6 +1168,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -1092,6 +1205,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -1140,6 +1257,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -1171,6 +1292,10 @@ export default ({ getService }: FtrProviderContext) => { await installPrePackagedRules(supertest, log); await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); expect(stats.detection_rules.detection_rule_usage.elastic_total.disabled).above(0); expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); @@ -1205,6 +1330,9 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id @@ -1247,6 +1375,9 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' @@ -1302,6 +1433,9 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' @@ -1356,6 +1490,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' @@ -1410,6 +1548,10 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); + + // remove "detection_rule_status" from the test by resetting it to initial + stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); + // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' From 8bf8bfda8346e84d8631104efdb1cdb7a561730f Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Tue, 29 Mar 2022 18:34:28 -0400 Subject: [PATCH 041/108] [Security Solution][Rule Preview] Display alert table in rule preview result (#127986) --- .../event_details/alert_summary_view.tsx | 7 +- .../components/event_details/columns.tsx | 72 ++++---- .../event_details/event_details.tsx | 21 ++- .../event_details/event_fields_browser.tsx | 5 +- .../event_details/get_alert_summary_rows.tsx | 3 + .../components/event_details/helpers.tsx | 1 + .../event_details/overview/index.tsx | 130 ++++++++------ .../table/summary_value_cell.tsx | 3 +- .../rule_preview/preview_histogram.test.tsx | 1 + .../rules/rule_preview/preview_histogram.tsx | 159 ++++++++++++++---- .../preview_table_cell_renderer.tsx | 113 +++++++++++++ .../preview_table_control_columns.tsx | 76 +++++++++ .../rules/rule_preview/translations.ts | 27 +++ .../detection_engine/rules/create/index.tsx | 3 + .../event_details/expandable_event.tsx | 3 + .../side_panel/event_details/index.tsx | 51 +++--- .../timelines/components/side_panel/index.tsx | 3 + 17 files changed, 530 insertions(+), 148 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_control_columns.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index 3750753114c36..8df5bd3ce0194 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -22,10 +22,11 @@ const AlertSummaryViewComponent: React.FC<{ timelineId: string; title: string; goToTable: () => void; -}> = ({ browserFields, data, eventId, isDraggable, timelineId, title, goToTable }) => { + isReadOnly?: boolean; +}> = ({ browserFields, data, eventId, isDraggable, timelineId, title, goToTable, isReadOnly }) => { const summaryRows = useMemo( - () => getSummaryRows({ browserFields, data, eventId, isDraggable, timelineId }), - [browserFields, data, eventId, isDraggable, timelineId] + () => getSummaryRows({ browserFields, data, eventId, isDraggable, timelineId, isReadOnly }), + [browserFields, data, eventId, isDraggable, timelineId, isReadOnly] ); return ; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 811c43fdfc5b9..baea88334bb05 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -48,6 +48,7 @@ export const getColumns = ({ toggleColumn, getLinkValue, isDraggable, + isReadOnly, }: { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; @@ -58,40 +59,45 @@ export const getColumns = ({ toggleColumn: (column: ColumnHeaderOptions) => void; getLinkValue: (field: string) => string | null; isDraggable?: boolean; + isReadOnly?: boolean; }) => [ - { - field: 'values', - name: ( - - {i18n.ACTIONS} - - ), - sortable: false, - truncateText: false, - width: '132px', - render: (values: string[] | null | undefined, data: EventFieldsData) => { - const label = data.isObjectArray - ? i18n.NESTED_COLUMN(data.field) - : i18n.VIEW_COLUMN(data.field); - const fieldFromBrowserField = getFieldFromBrowserField( - [data.category, 'fields', data.field], - browserFields - ); - return ( - - ); - }, - }, + ...(!isReadOnly + ? [ + { + field: 'values', + name: ( + + {i18n.ACTIONS} + + ), + sortable: false, + truncateText: false, + width: '132px', + render: (values: string[] | null | undefined, data: EventFieldsData) => { + const label = data.isObjectArray + ? i18n.NESTED_COLUMN(data.field) + : i18n.VIEW_COLUMN(data.field); + const fieldFromBrowserField = getFieldFromBrowserField( + [data.category, 'fields', data.field], + browserFields + ); + return ( + + ); + }, + }, + ] + : []), { field: 'field', className: 'eventFieldsTable__fieldNameCell', diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index f10beb1c9c6ca..c2c88ec3e08c6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -69,6 +69,7 @@ interface Props { timelineId: string; hostRisk: HostRisk | null; handleOnEventClosed: () => void; + isReadOnly?: boolean; } export const Indent = styled.div` @@ -117,6 +118,7 @@ const EventDetailsComponent: React.FC = ({ timelineTabType, hostRisk, handleOnEventClosed, + isReadOnly, }) => { const [selectedTabId, setSelectedTabId] = useState(EventsViewType.summaryView); const handleTabClick = useCallback( @@ -168,6 +170,7 @@ const EventDetailsComponent: React.FC = ({ indexName={indexName} timelineId={timelineId} handleOnEventClosed={handleOnEventClosed} + isReadOnly={isReadOnly} /> @@ -181,6 +184,7 @@ const EventDetailsComponent: React.FC = ({ isDraggable, timelineId, title: i18n.HIGHLIGHTED_FIELDS, + isReadOnly, }} goToTable={goToTableTab} /> @@ -222,12 +226,13 @@ const EventDetailsComponent: React.FC = ({ hostRisk, goToTableTab, handleOnEventClosed, + isReadOnly, ] ); const threatIntelTab = useMemo( () => - isAlert + isAlert && !isReadOnly ? { id: EventsViewType.threatIntelView, 'data-test-subj': 'threatIntelTab', @@ -270,7 +275,16 @@ const EventDetailsComponent: React.FC = ({ ), } : undefined, - [allEnrichments, setRange, range, enrichmentCount, isAlert, eventFields, isEnrichmentsLoading] + [ + allEnrichments, + setRange, + range, + enrichmentCount, + isAlert, + eventFields, + isEnrichmentsLoading, + isReadOnly, + ] ); const tableTab = useMemo( @@ -288,11 +302,12 @@ const EventDetailsComponent: React.FC = ({ isDraggable={isDraggable} timelineId={timelineId} timelineTabType={timelineTabType} + isReadOnly={isReadOnly} /> ), }), - [browserFields, data, id, isDraggable, timelineId, timelineTabType] + [browserFields, data, id, isDraggable, timelineId, timelineTabType, isReadOnly] ); const jsonTab = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 384e9d72b0787..29700d3706b7b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -37,6 +37,7 @@ interface Props { isDraggable?: boolean; timelineId: string; timelineTabType: TimelineTabs | 'flyout'; + isReadOnly?: boolean; } const TableWrapper = styled.div` @@ -137,7 +138,7 @@ const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` /** Renders a table view or JSON view of the `ECS` `data` */ export const EventFieldsBrowser = React.memo( - ({ browserFields, data, eventId, isDraggable, timelineTabType, timelineId }) => { + ({ browserFields, data, eventId, isDraggable, timelineTabType, timelineId, isReadOnly }) => { const containerElement = useRef(null); const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -219,6 +220,7 @@ export const EventFieldsBrowser = React.memo( toggleColumn, getLinkValue, isDraggable, + isReadOnly, }), [ browserFields, @@ -230,6 +232,7 @@ export const EventFieldsBrowser = React.memo( toggleColumn, getLinkValue, isDraggable, + isReadOnly, ] ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx index 8d2de0439967c..f2180ea2565e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx @@ -227,12 +227,14 @@ export const getSummaryRows = ({ timelineId, eventId, isDraggable = false, + isReadOnly = false, }: { data: TimelineEventsDetailsItem[]; browserFields: BrowserFields; timelineId: string; eventId: string; isDraggable?: boolean; + isReadOnly?: boolean; }) => { const eventCategories = getEventCategoriesFromData(data); @@ -280,6 +282,7 @@ export const getSummaryRows = ({ field, }), isDraggable, + isReadOnly, }; if (field.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index a6c9d71e3371c..a58ac79ee25ae 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -56,6 +56,7 @@ export interface AlertSummaryRow { title: string; description: EnrichedFieldInfo & { isDraggable?: boolean; + isReadOnly?: boolean; }; } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx index 3c9c202a982a8..39fbf36ef5cd8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx @@ -26,7 +26,7 @@ import { SIGNAL_STATUS_FIELD_NAME, } from '../../../../timelines/components/timeline/body/renderers/constants'; import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; -import { OverviewCardWithActions } from '../overview/overview_card'; +import { OverviewCardWithActions, OverviewCard } from '../overview/overview_card'; import { StatusPopoverButton } from '../overview/status_popover_button'; import { SeverityBadge } from '../../../../../public/detections/components/rules/severity_badge'; import { useThrottledResizeObserver } from '../../utils'; @@ -44,10 +44,20 @@ interface Props { handleOnEventClosed: () => void; indexName: string; timelineId: string; + isReadOnly?: boolean; } export const Overview = React.memo( - ({ browserFields, contextId, data, eventId, handleOnEventClosed, indexName, timelineId }) => { + ({ + browserFields, + contextId, + data, + eventId, + handleOnEventClosed, + indexName, + timelineId, + isReadOnly, + }) => { const statusData = useMemo(() => { const item = find({ field: SIGNAL_STATUS_FIELD_NAME, category: 'kibana' }, data); return ( @@ -106,71 +116,83 @@ export const Overview = React.memo( ); }, [browserFields, contextId, data, eventId, timelineId]); - const signalCard = hasData(statusData) ? ( - - - + - - - ) : null; + contextId={contextId} + > + + + + ) : null; const severityCard = hasData(severityData) ? ( - - - + {!isReadOnly ? ( + + + + ) : ( + + + + )} ) : null; const riskScoreCard = hasData(riskScoreData) ? ( - - {riskScoreData.values[0]} - + {!isReadOnly ? ( + + {riskScoreData.values[0]} + + ) : ( + {riskScoreData.values[0]} + )} ) : null; - const ruleNameCard = hasData(ruleNameData) ? ( - - - + - - - ) : null; + > + + + + ) : null; const { width, ref } = useThrottledResizeObserver(); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx index 4289e0e73327d..ad9ca84429f00 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx @@ -20,6 +20,7 @@ export const SummaryValueCell: React.FC = ({ linkValue, timelineId, values, + isReadOnly, }) => ( <> = ({ style={{ flexGrow: 0 }} values={values} /> - {timelineId !== TimelineId.active && ( + {timelineId !== TimelineId.active && !isReadOnly && ( { const mockSetQuery = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx index c027c7fc17bc7..3b640592535b6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx @@ -11,6 +11,8 @@ import { Unit } from '@elastic/datemath'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; import styled from 'styled-components'; import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { getHistogramConfig, getThresholdHistogramConfig, isNoisy } from './helpers'; @@ -21,12 +23,31 @@ import { BarChart } from '../../../../common/components/charts/barchart'; import { usePreviewHistogram } from './use_preview_histogram'; import { formatDate } from '../../../../common/components/super_date_picker'; import { FieldValueThreshold } from '../threshold_input'; +import { alertsDefaultModel } from '../../alerts_table/default_config'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers'; +import { TimelineId } from '../../../../../common/types'; +import { APP_ID, APP_UI_ID, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; +import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../../../common/lib/cell_actions/constants'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { DetailsPanel } from '../../../../timelines/components/side_panel'; +import { PreviewRenderCellValue } from './preview_table_cell_renderer'; +import { getPreviewTableControlColumn } from './preview_table_control_columns'; +import { useGlobalFullScreen } from '../../../../common/containers/use_full_screen'; +import { InspectButtonContainer } from '../../../../common/components/inspect'; const LoadingChart = styled(EuiLoadingChart)` display: block; margin: 0 auto; `; +const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` + height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)}; + flex: 1 1 auto; + display: flex; + width: 100%; +`; + export const ID = 'previewHistogram'; interface PreviewHistogramProps { @@ -51,7 +72,7 @@ export const PreviewHistogram = ({ index, }: PreviewHistogramProps) => { const { setQuery, isInitializing } = useGlobalTime(); - + const { timelines: timelinesUi, cases } = useKibana().services; const from = useMemo(() => `now-1${timeFrame}`, [timeFrame]); const to = useMemo(() => 'now', []); const startDate = useMemo(() => formatDate(from), [from]); @@ -69,7 +90,30 @@ export const PreviewHistogram = ({ ruleType, }); + const { + columns, + dataProviders, + deletedEventIds, + kqlMode, + itemsPerPage, + itemsPerPageOptions, + graphEventId, + sort, + } = alertsDefaultModel; + + const { + browserFields, + docValueFields, + indexPattern, + runtimeMappings, + loading: isLoadingIndexPattern, + } = useSourcererDataView(SourcererScopeName.detections); + + const { globalFullScreen } = useGlobalFullScreen(); const previousPreviewId = usePrevious(previewId); + const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled( + 'tGridEventRenderedViewEnabled' + ); useEffect(() => { if (previousPreviewId !== previewId && totalCount > 0) { @@ -121,38 +165,91 @@ export const PreviewHistogram = ({ : i18n.QUERY_PREVIEW_TITLE(totalCount), [isLoading, totalCount, thresholdTotalCount, isThresholdRule] ); + const CasesContext = cases.ui.getCasesContext(); return ( - - - - - - - {isLoading ? ( - - ) : ( - + + + + - )} - - - <> - - -

{i18n.QUERY_PREVIEW_DISCLAIMER_MAX_SIGNALS}

-
- -
-
-
+
+ + {isLoading ? ( + + ) : ( + + )} + + + <> + + +

{i18n.QUERY_PREVIEW_DISCLAIMER_MAX_SIGNALS}

+
+ +
+
+
+ + + + + {timelinesUi.getTGrid<'embedded'>({ + additionalFilters: <>, + appId: APP_UI_ID, + browserFields, + columns, + dataProviders, + deletedEventIds, + disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, + docValueFields, + end: to, + entityType: 'alerts', + filters: [], + globalFullScreen, + graphEventId, + hasAlertsCrud: false, + id: TimelineId.detectionsPage, + indexNames: [`${DEFAULT_PREVIEW_INDEX}-${spaceId}`], + indexPattern, + isLive: false, + isLoadingIndexPattern, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + query: { query: `kibana.alert.rule.uuid:${previewId}`, language: 'kuery' }, + renderCellValue: PreviewRenderCellValue, + rowRenderers: defaultRowRenderers, + runtimeMappings, + setQuery: () => {}, + sort, + start: from, + tGridEventRenderedViewEnabled, + type: 'embedded', + leadingControlColumns: getPreviewTableControlColumn(1.5), + })} + + + + + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx new file mode 100644 index 0000000000000..ab56bd3fbfe0d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { StyledContent } from '../../../../common/lib/cell_actions/expanded_cell_value_actions'; +import { getLinkColumnDefinition } from '../../../../common/lib/cell_actions/helpers'; +import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns'; +import { columnRenderers } from '../../../../timelines/components/timeline/body/renderers'; +import { getColumnRenderer } from '../../../../timelines/components/timeline/body/renderers/get_column_renderer'; +import { CellValueElementProps } from '../../../../../../timelines/common'; + +export const PreviewRenderCellValue: React.FC< + EuiDataGridCellValueElementProps & CellValueElementProps +> = ({ + browserFields, + columnId, + data, + ecsData, + eventId, + globalFilters, + header, + isDetails, + isDraggable, + isExpandable, + isExpanded, + linkValues, + rowIndex, + colIndex, + rowRenderers, + setCellProps, + timelineId, + truncate, +}) => ( + +); + +export const PreviewTableCellRenderer: React.FC = ({ + browserFields, + data, + ecsData, + eventId, + header, + isDetails, + isDraggable, + isTimeline, + linkValues, + rowRenderers, + timelineId, + truncate, +}) => { + const usersEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); + + const asPlainText = useMemo(() => { + return ( + getLinkColumnDefinition(header.id, header.type, undefined, usersEnabled) !== undefined && + !isTimeline + ); + }, [header.id, header.type, isTimeline, usersEnabled]); + + const values = useGetMappedNonEcsValue({ + data, + fieldName: header.id, + }); + const styledContentClassName = isDetails + ? 'eui-textBreakWord' + : 'eui-displayInlineBlock eui-textTruncate'; + return ( + <> + + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + asPlainText, + browserFields, + columnName: header.id, + ecsData, + eventId, + field: header, + isDetails, + isDraggable, + linkValues, + rowRenderers, + timelineId, + truncate, + values, + })} + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_control_columns.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_control_columns.tsx new file mode 100644 index 0000000000000..c8f59149ea1a2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_control_columns.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { ControlColumnProps, ActionProps } from '../../../../../../timelines/common'; +import { + getActionsColumnWidth, + DEFAULT_ACTION_BUTTON_WIDTH, +} from '../../../../../../timelines/public'; +import * as i18n from './translations'; + +const EventsTdContent = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__tdContent ${className != null ? className : ''}`, +}))<{ textAlign?: string; width?: number }>` + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + min-width: 0; + padding: ${({ theme }) => theme.eui.paddingSizes.xs}; + text-align: ${({ textAlign }) => textAlign}; + width: ${({ width }) => + width != null + ? `${width}px` + : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ + + button.euiButtonIcon { + margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`}; + } +`; + +export const getPreviewTableControlColumn = (actionButtonCount: number): ControlColumnProps[] => [ + { + headerCellRender: () => <>{i18n.ACTIONS}, + id: 'default-timeline-control-column', + rowCellRender: PreviewActions, + width: getActionsColumnWidth(actionButtonCount), + }, +]; + +const ActionsContainer = styled.div` + align-items: center; + display: flex; +`; + +const PreviewActionsComponent: React.FC = ({ + ariaRowindex, + columnValues, + onEventDetailsPanelOpened, +}) => { + return ( + +
+ + + + + +
+
+ ); +}; + +PreviewActionsComponent.displayName = 'PreviewActionsComponent'; + +export const PreviewActions = React.memo(PreviewActionsComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts index 58a90fba13dc9..3acb533913cfd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts @@ -174,3 +174,30 @@ export const QUERY_PREVIEW_SEE_ALL_WARNINGS = i18n.translate( defaultMessage: 'See all warnings', } ); + +export const ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.actions', + { + defaultMessage: 'Actions', + } +); + +export const VIEW_DETAILS = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.viewDetailsAriaLabel', + { + defaultMessage: 'View details', + } +); + +export const VIEW_DETAILS_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.securitySolution.detectionEngine.queryPreview.viewDetailsForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: + 'View details for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index e9d8749ee5601..c7043f3725fcf 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -70,6 +70,9 @@ const MyEuiPanel = styled(EuiPanel)<{ display: none; } } + .euiAccordion__childWrapper { + transform: none; /* To circumvent an issue in Eui causing the fullscreen datagrid to break */ + } `; MyEuiPanel.displayName = 'MyEuiPanel'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx index 37c88cc77d110..9955c2c318065 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx @@ -40,6 +40,7 @@ interface Props { timelineId: string; hostRisk: HostRisk | null; handleOnEventClosed: HandleOnEventClosed; + isReadOnly?: boolean; } interface ExpandableEventTitleProps { @@ -109,6 +110,7 @@ export const ExpandableEvent = React.memo( hostRisk, rawEventData, handleOnEventClosed, + isReadOnly, }) => { if (!event.eventId) { return {i18n.EVENT_DETAILS_PLACEHOLDER}; @@ -133,6 +135,7 @@ export const ExpandableEvent = React.memo( timelineTabType={timelineTabType} hostRisk={hostRisk} handleOnEventClosed={handleOnEventClosed} + isReadOnly={isReadOnly} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 0ca853b84f86e..22d8b9bd2f4aa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -67,6 +67,7 @@ interface EventDetailsPanelProps { runtimeMappings: MappingRuntimeFields; tabType: TimelineTabs; timelineId: string; + isReadOnly?: boolean; } const EventDetailsPanelComponent: React.FC = ({ @@ -80,6 +81,7 @@ const EventDetailsPanelComponent: React.FC = ({ runtimeMappings, tabType, timelineId, + isReadOnly, }) => { const [loading, detailsData, rawEventData, ecsData, refetchFlyoutData] = useTimelineEventsDetails( { @@ -234,21 +236,24 @@ const EventDetailsPanelComponent: React.FC = ({ timelineTabType="flyout" hostRisk={hostRisk} handleOnEventClosed={handleOnEventClosed} + isReadOnly={isReadOnly} /> )} - + {!isReadOnly && ( + + )} ) : ( @@ -272,17 +277,19 @@ const EventDetailsPanelComponent: React.FC = ({ hostRisk={hostRisk} handleOnEventClosed={handleOnEventClosed} /> - + {!isReadOnly && ( + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx index 736fa39889340..b943d3251c476 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx @@ -30,6 +30,7 @@ interface DetailsPanelProps { runtimeMappings: MappingRuntimeFields; tabType?: TimelineTabs; timelineId: string; + isReadOnly?: boolean; } /** @@ -47,6 +48,7 @@ export const DetailsPanel = React.memo( runtimeMappings, tabType, timelineId, + isReadOnly, }: DetailsPanelProps) => { const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -90,6 +92,7 @@ export const DetailsPanel = React.memo( runtimeMappings={runtimeMappings} tabType={activeTab} timelineId={timelineId} + isReadOnly={isReadOnly} /> ); } From abd3e9e84d620651f4e90c5d6c6a3e76e98c182a Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Tue, 29 Mar 2022 18:15:42 -0500 Subject: [PATCH 042/108] [Security Solution] silently filter duplicate blocklist values (#128702) --- .../validators/blocklist_validator.ts | 37 +++++++++++++------ .../apis/endpoint_artifacts/blocklists.ts | 29 --------------- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts index e51190467aee4..f6f08d792b115 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { cloneDeep, uniq } from 'lodash'; import { ENDPOINT_BLOCKLISTS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { schema, Type, TypeOf } from '@kbn/config-schema'; import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; @@ -127,24 +128,14 @@ const hashEntriesValidation = (entries: BlocklistConditionEntry[]) => { } const hashesCount: { [key: string]: boolean } = {}; - const duplicatedHashes: string[] = []; const invalidHash: string[] = []; // Check hash entries individually currentHashes.forEach((hash) => { if (!allowedHashes.includes(hash)) invalidHash.push(hash); - if (hashesCount[hash]) { - duplicatedHashes.push(hash); - } else { - hashesCount[hash] = true; - } + hashesCount[hash] = true; }); - // There is more than one entry with the same hash type - if (duplicatedHashes.length) { - return `There are some duplicated hashes: ${duplicatedHashes.join(',')}`; - } - // There is an entry with an invalid hash type if (invalidHash.length) { return `There are some invalid fields for hash type: ${invalidHash.join(',')}`; @@ -159,7 +150,7 @@ const entriesSchemaOptions = { return hashEntriesValidation(entries); } else { if (entries.length > 1) { - return 'Only one entry is allowed when no using hash field type'; + return 'Only one entry is allowed when not using hash field type'; } } }, @@ -202,6 +193,20 @@ const BlocklistDataSchema = schema.object( { unknowns: 'ignore' } ); +function removeDuplicateEntryValues(entries: BlocklistConditionEntry[]): BlocklistConditionEntry[] { + return entries.map((entry) => { + const nextEntry = cloneDeep(entry); + + if (nextEntry.type === 'match_any') { + nextEntry.value = uniq(nextEntry.value); + } else if (nextEntry.type === 'nested') { + removeDuplicateEntryValues(nextEntry.entries); + } + + return nextEntry; + }); +} + export class BlocklistValidator extends BaseValidator { static isBlocklist(item: { listId: string }): boolean { return item.listId === ENDPOINT_BLOCKLISTS_LIST_ID; @@ -211,6 +216,9 @@ export class BlocklistValidator extends BaseValidator { item: CreateExceptionListItemOptions ): Promise { await this.validateCanManageEndpointArtifacts(); + + item.entries = removeDuplicateEntryValues(item.entries as BlocklistConditionEntry[]); + await this.validateBlocklistData(item); await this.validateCanCreateByPolicyArtifacts(item); await this.validateByPolicyItem(item); @@ -249,6 +257,11 @@ export class BlocklistValidator extends BaseValidator { const updatedItem = _updatedItem as ExceptionItemLikeOptions; await this.validateCanManageEndpointArtifacts(); + + _updatedItem.entries = removeDuplicateEntryValues( + _updatedItem.entries as BlocklistConditionEntry[] + ); + await this.validateBlocklistData(updatedItem); try { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/blocklists.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/blocklists.ts index 7e67c38347603..5f58eb40c0956 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/blocklists.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/blocklists.ts @@ -124,35 +124,6 @@ export default function ({ getService }: FtrProviderContext) { .expect(anErrorMessageWith(/types that failed validation:/)); }); - it(`should error on [${blocklistApiCall.method}] if the same hash type is present twice`, async () => { - const body = blocklistApiCall.getBody(); - - body.entries = [ - { - field: 'file.hash.sha256', - value: ['a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'], - type: 'match_any', - operator: 'included', - }, - { - field: 'file.hash.sha256', - value: [ - '2C26B46B68FFC68FF99B453C1D30413413422D706483BFA0F98A5E886266E7AE', - 'FCDE2B2EDBA56BF408601FB721FE9B5C338D10EE429EA04FAE5511B68FBF8FB9', - ], - type: 'match_any', - operator: 'included', - }, - ]; - - await supertest[blocklistApiCall.method](blocklistApiCall.path) - .set('kbn-xsrf', 'true') - .send(body) - .expect(400) - .expect(anEndpointArtifactError) - .expect(anErrorMessageWith(/duplicated/)); - }); - it(`should error on [${blocklistApiCall.method}] if an invalid hash is used`, async () => { const body = blocklistApiCall.getBody(); From db7db0e4ca7c91bf5dc4ce175d77e8f5a1b54544 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 29 Mar 2022 17:53:38 -0600 Subject: [PATCH 043/108] [ci-stats-reporter] prevent `Request body larger than maxBodyLength limit` error (#128840) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts | 4 ++++ packages/kbn-pm/dist/index.js | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index f710f7ec70843..367a1be175266 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -312,6 +312,10 @@ export class CiStatsReporter { data: body, params: query, adapter: httpAdapter, + + // if it can be serialized into a string, send it + maxBodyLength: Infinity, + maxContentLength: Infinity, }); return resp.data; diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 5c1403494e944..0b6ebc9deccb4 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -9304,7 +9304,10 @@ class CiStatsReporter { headers, data: body, params: query, - adapter: _http.default + adapter: _http.default, + // if it can be serialized into a string, send it + maxBodyLength: Infinity, + maxContentLength: Infinity }); return resp.data; } catch (error) { From 2884e56fee10f882c0eb35f38070e19f1027c488 Mon Sep 17 00:00:00 2001 From: "Lucas F. da Costa" Date: Wed, 30 Mar 2022 01:04:37 +0100 Subject: [PATCH 044/108] [Uptime] Use URL from the /allowed route in the action button to signup for Synthetics beta (#128798) * [Uptime] Use URL from the /allowed route in the action button to signup for Synthetics beta * rename betaFormUrl to SignupUrl * Rename service_allowed_wrapper test title to use "allowed" variable instead of enabled Co-authored-by: Dominique Clarke * Rename service_allowed_wrapper test title to use "allowed" variable instead of enabled Co-authored-by: Dominique Clarke * assert on button's HREF on empty state signup form tests Co-authored-by: Dominique Clarke --- .../uptime/common/types/synthetics_monitor.ts | 1 + .../hooks/use_inline_errors.test.tsx | 1 + .../hooks/use_inline_errors_count.test.tsx | 1 + .../hooks/use_locations.test.tsx | 1 + .../monitor_list/monitor_list.test.tsx | 1 + .../public/lib/__mocks__/uptime_store.mock.ts | 1 + .../uptime/public/lib/helper/rtl_helpers.tsx | 15 ++-- .../service_allowed_wrapper.test.tsx | 73 ++++++++++++++----- .../service_allowed_wrapper.tsx | 8 +- .../state/reducers/monitor_management.ts | 6 +- .../synthetics_service/service_api_client.ts | 11 ++- .../synthetics_service.test.ts | 1 + .../synthetics_service/synthetics_service.ts | 10 ++- .../synthetics_service/get_service_allowed.ts | 5 +- 14 files changed, 101 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/uptime/common/types/synthetics_monitor.ts b/x-pack/plugins/uptime/common/types/synthetics_monitor.ts index 2fba9e0c299aa..d59e45e86b819 100644 --- a/x-pack/plugins/uptime/common/types/synthetics_monitor.ts +++ b/x-pack/plugins/uptime/common/types/synthetics_monitor.ts @@ -22,4 +22,5 @@ export type DecryptedSyntheticsMonitorSavedObject = SimpleSavedObject', () => { }, syntheticsService: { loading: false, + signupUrl: null, }, } as MonitorManagementListState, }; diff --git a/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts index 298f3d17575f1..1bb86877f9861 100644 --- a/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts +++ b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts @@ -84,6 +84,7 @@ export const mockState: AppState = { enablement: null, syntheticsService: { loading: false, + signupUrl: null, }, }, ml: { diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index 15a279e1e95f0..ed23cdd52ca0e 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -285,21 +285,26 @@ const getHistoryFromUrl = (url: Url) => { }); }; -// This function allows us to query for the nearest button with test -// no matter whether it has nested tags or not (as EuiButton elements do). -export const forNearestButton = +const forNearestTag = + (tag: string) => (getByText: (f: MatcherFunction) => HTMLElement | null) => (text: string): HTMLElement | null => getByText((_content: string, node: Nullish) => { if (!node) return false; const noOtherButtonHasText = Array.from(node.children).every( - (child) => child && (child.textContent !== text || child.tagName.toLowerCase() !== 'button') + (child) => child && (child.textContent !== text || child.tagName.toLowerCase() !== tag) ); return ( - noOtherButtonHasText && node.textContent === text && node.tagName.toLowerCase() === 'button' + noOtherButtonHasText && node.textContent === text && node.tagName.toLowerCase() === tag ); }); +// This function allows us to query for the nearest button with test +// no matter whether it has nested tags or not (as EuiButton elements do). +export const forNearestButton = forNearestTag('button'); + +export const forNearestAnchor = forNearestTag('a'); + export const makeUptimePermissionsCore = ( permissions: Partial<{ 'alerting:save': boolean; diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.test.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.test.tsx index 77e64f70d48e3..6f05314bc9fc0 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { render } from '../../lib/helper/rtl_helpers'; +import { render, forNearestButton, forNearestAnchor } from '../../lib/helper/rtl_helpers'; import * as allowedHook from '../../components/monitor_management/hooks/use_service_allowed'; import { ServiceAllowedWrapper } from './service_allowed_wrapper'; @@ -22,8 +22,10 @@ describe('ServiceAllowedWrapper', () => { expect(await findByText('Test text')).toBeInTheDocument(); }); - it('renders when enabled state is loading', async () => { - jest.spyOn(allowedHook, 'useSyntheticsServiceAllowed').mockReturnValue({ loading: true }); + it('renders loading state when allowed state is loading', async () => { + jest + .spyOn(allowedHook, 'useSyntheticsServiceAllowed') + .mockReturnValue({ loading: true, signupUrl: null }); const { findByText } = render( @@ -34,31 +36,68 @@ describe('ServiceAllowedWrapper', () => { expect(await findByText('Loading Monitor Management')).toBeInTheDocument(); }); - it('renders when enabled state is false', async () => { + it('renders children when allowed state is true', async () => { jest .spyOn(allowedHook, 'useSyntheticsServiceAllowed') - .mockReturnValue({ loading: false, isAllowed: false }); + .mockReturnValue({ loading: false, isAllowed: true, signupUrl: 'https://example.com' }); - const { findByText } = render( + const { findByText, queryByText } = render(
Test text
); - expect(await findByText('Monitor Management')).toBeInTheDocument(); + expect(await findByText('Test text')).toBeInTheDocument(); + expect(await queryByText('Monitor management')).not.toBeInTheDocument(); }); - it('renders when enabled state is true', async () => { - jest - .spyOn(allowedHook, 'useSyntheticsServiceAllowed') - .mockReturnValue({ loading: false, isAllowed: true }); + describe('when enabled state is false', () => { + it('renders an enabled button if there is a form URL', async () => { + jest + .spyOn(allowedHook, 'useSyntheticsServiceAllowed') + .mockReturnValue({ loading: false, isAllowed: false, signupUrl: 'https://example.com' }); - const { findByText } = render( - -
Test text
-
- ); + const { findByText, getByText } = render( + +
Test text
+
+ ); - expect(await findByText('Test text')).toBeInTheDocument(); + expect(await findByText('Monitor management')).toBeInTheDocument(); + expect(forNearestAnchor(getByText)('Request access')).toBeEnabled(); + expect(forNearestAnchor(getByText)('Request access')).toHaveAttribute( + 'href', + 'https://example.com' + ); + }); + + it('renders a disabled button if there is no form URL', async () => { + jest + .spyOn(allowedHook, 'useSyntheticsServiceAllowed') + .mockReturnValue({ loading: false, isAllowed: false, signupUrl: null }); + + const { findByText, getByText } = render( + +
Test text
+
+ ); + + expect(await findByText('Monitor management')).toBeInTheDocument(); + expect(forNearestButton(getByText)('Request access')).toBeDisabled(); + }); + + it('renders when enabled state is false', async () => { + jest + .spyOn(allowedHook, 'useSyntheticsServiceAllowed') + .mockReturnValue({ loading: false, isAllowed: false, signupUrl: 'https://example.com' }); + + const { findByText } = render( + +
Test text
+
+ ); + + expect(await findByText('Monitor management')).toBeInTheDocument(); + }); }); }); diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.tsx index 8f6cd7d3f0eb5..152bc1cca12ad 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/service_allowed_wrapper.tsx @@ -11,7 +11,7 @@ import { EuiButton, EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; import { useSyntheticsServiceAllowed } from '../../components/monitor_management/hooks/use_service_allowed'; export const ServiceAllowedWrapper: React.FC = ({ children }) => { - const { isAllowed, loading } = useSyntheticsServiceAllowed(); + const { isAllowed, signupUrl, loading } = useSyntheticsServiceAllowed(); if (loading) { return ( @@ -29,7 +29,7 @@ export const ServiceAllowedWrapper: React.FC = ({ children }) => { title={

{MONITOR_MANAGEMENT_LABEL}

} body={

{PUBLIC_BETA_DESCRIPTION}

} actions={[ - + {REQUEST_ACCESS_LABEL} , ]} @@ -45,7 +45,7 @@ const REQUEST_ACCESS_LABEL = i18n.translate('xpack.uptime.monitorManagement.requ }); const MONITOR_MANAGEMENT_LABEL = i18n.translate('xpack.uptime.monitorManagement.label', { - defaultMessage: 'Monitor Management', + defaultMessage: 'Monitor management', }); const LOADING_MONITOR_MANAGEMENT_LABEL = i18n.translate( @@ -59,7 +59,7 @@ const PUBLIC_BETA_DESCRIPTION = i18n.translate( 'xpack.uptime.monitorManagement.publicBetaDescription', { defaultMessage: - 'Monitor Management is available only for selected public beta users. With public\n' + + 'Monitor management is available only for selected public beta users. With public\n' + 'beta access, you will be able to add HTTP, TCP, ICMP and Browser checks which will\n' + "run on Elastic's managed synthetics service nodes.", } diff --git a/x-pack/plugins/uptime/public/state/reducers/monitor_management.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_management.ts index 419c43db20ccf..2f60b9bf1499e 100644 --- a/x-pack/plugins/uptime/public/state/reducers/monitor_management.ts +++ b/x-pack/plugins/uptime/public/state/reducers/monitor_management.ts @@ -40,7 +40,7 @@ export interface MonitorManagementList { list: MonitorManagementListResult; locations: ServiceLocations; enablement: MonitorManagementEnablementResult | null; - syntheticsService: { isAllowed?: boolean; loading: boolean }; + syntheticsService: { isAllowed?: boolean; signupUrl: string | null; loading: boolean }; throttling: ThrottlingOptions; } @@ -64,6 +64,7 @@ export const initialState: MonitorManagementList = { enablement: null, }, syntheticsService: { + signupUrl: null, loading: false, }, throttling: DEFAULT_THROTTLING, @@ -269,6 +270,7 @@ export const monitorManagementListReducer = createReducer(initialState, (builder ...state, syntheticsService: { isAllowed: state.syntheticsService?.isAllowed, + signupUrl: state.syntheticsService?.signupUrl, loading: true, }, }) @@ -282,6 +284,7 @@ export const monitorManagementListReducer = createReducer(initialState, (builder ...state, syntheticsService: { isAllowed: action.payload.serviceAllowed, + signupUrl: action.payload.signupUrl, loading: false, }, }) @@ -292,6 +295,7 @@ export const monitorManagementListReducer = createReducer(initialState, (builder ...state, syntheticsService: { isAllowed: false, + signupUrl: null, loading: false, }, }) diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts index cf27574c09d6f..68d4ebd385f07 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts @@ -85,10 +85,10 @@ export class ServiceAPIClient { return this.callAPI('POST', { ...data, runOnce: true }); } - async checkIfAccountAllowed() { + async checkAccountAccessStatus() { if (this.authorization) { // in case username/password is provided, we assume it's always allowed - return true; + return { allowed: true, signupUrl: null }; } const httpsAgent = this.getHttpsAgent(); @@ -109,12 +109,15 @@ export class ServiceAPIClient { : undefined, httpsAgent, }); - return data.allowed; + + const { allowed, signupUrl } = data; + return { allowed, signupUrl }; } catch (e) { this.logger.error(e); } } - return false; + + return { allowed: false, signupUrl: null }; } async callAPI( diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.test.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.test.ts index 74c4aa0fca7da..f76126d40d5e9 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.test.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.test.ts @@ -30,6 +30,7 @@ describe('SyntheticsService', () => { expect(service.isAllowed).toEqual(false); expect(service.locations).toEqual([]); + expect(service.signupUrl).toEqual(null); }); it('inits properly with basic auth', async () => { diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts index 21d5fa6760983..b48a785f8354f 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts @@ -61,12 +61,14 @@ export class SyntheticsService { private indexTemplateInstalling?: boolean; public isAllowed: boolean; + public signupUrl: string | null; constructor(logger: Logger, server: UptimeServerSetup, config: ServiceConfig) { this.logger = logger; this.server = server; this.config = config; this.isAllowed = false; + this.signupUrl = null; this.apiClient = new ServiceAPIClient(logger, this.config, this.server.kibanaVersion); @@ -78,7 +80,9 @@ export class SyntheticsService { public async init() { await this.registerServiceLocations(); - this.isAllowed = await this.apiClient.checkIfAccountAllowed(); + const { allowed, signupUrl } = await this.apiClient.checkAccountAccessStatus(); + this.isAllowed = allowed; + this.signupUrl = signupUrl; } private setupIndexTemplates() { @@ -140,7 +144,9 @@ export class SyntheticsService { await service.registerServiceLocations(); - service.isAllowed = await service.apiClient.checkIfAccountAllowed(); + const { allowed, signupUrl } = await service.apiClient.checkAccountAccessStatus(); + service.isAllowed = allowed; + service.signupUrl = signupUrl; if (service.isAllowed) { service.setupIndexTemplates(); diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_allowed.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_allowed.ts index a7d6a1e0c9882..8e302e5fefde8 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_allowed.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_allowed.ts @@ -13,6 +13,9 @@ export const getServiceAllowedRoute: UMRestApiRouteFactory = () => ({ path: API_URLS.SERVICE_ALLOWED, validate: {}, handler: async ({ server }): Promise => { - return { serviceAllowed: server.syntheticsService.isAllowed }; + return { + serviceAllowed: server.syntheticsService.isAllowed, + signupUrl: server.syntheticsService.signupUrl, + }; }, }); From a5b7bbba21e5e651b9bc7a4486de659cfa4e823f Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 29 Mar 2022 18:32:20 -0600 Subject: [PATCH 045/108] [Maps] Allow implementers of ITooltipProperty to return React-DOM for the tooltip-value (#127069) * [Maps] Allow implementers of ITooltipProperty to return React-DOM for the tooltip-value * update snapshots * update ml tooltip to create reactnode instead of generating html string * revert unneeded change * remove actualDisplay and typicalDisplay * tslint * fix jest test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/common/index.ts | 1 + .../tooltips/es_agg_tooltip_property.ts | 3 +- ...ip_property.ts => es_tooltip_property.tsx} | 19 ++++- .../join_tooltip_property.tsx | 2 +- .../classes/tooltips/tooltip_property.ts | 2 +- .../feature_properties.test.tsx.snap | 54 +++++--------- .../features_tooltip/feature_properties.tsx | 22 +----- .../plugins/ml/public/maps/anomaly_source.tsx | 8 +- ...urce_field.ts => anomaly_source_field.tsx} | 55 +++++++++++--- .../plugins/ml/public/maps/maps_util.test.js | 43 +---------- .../ml/public/maps/results.test.mock.ts | 63 +++++++--------- x-pack/plugins/ml/public/maps/util.ts | 74 +++++-------------- 12 files changed, 132 insertions(+), 214 deletions(-) rename x-pack/plugins/maps/public/classes/tooltips/{es_tooltip_property.ts => es_tooltip_property.tsx} (83%) rename x-pack/plugins/ml/public/maps/{anomaly_source_field.ts => anomaly_source_field.tsx} (77%) diff --git a/x-pack/plugins/maps/common/index.ts b/x-pack/plugins/maps/common/index.ts index e140a855c3c30..fc86af0e2b1f5 100644 --- a/x-pack/plugins/maps/common/index.ts +++ b/x-pack/plugins/maps/common/index.ts @@ -8,6 +8,7 @@ export { AGG_TYPE, COLOR_MAP_TYPE, + DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, FIELD_ORIGIN, INITIAL_LOCATION, diff --git a/x-pack/plugins/maps/public/classes/tooltips/es_agg_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/es_agg_tooltip_property.ts index 54d5495db2389..24c250c55d708 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/es_agg_tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/es_agg_tooltip_property.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ReactNode } from 'react'; import { ESTooltipProperty } from './es_tooltip_property'; import { AGG_TYPE } from '../../../common/constants'; import { ITooltipProperty } from './tooltip_property'; @@ -27,7 +28,7 @@ export class ESAggTooltipProperty extends ESTooltipProperty { this._aggField = field; } - getHtmlDisplayValue(): string { + getHtmlDisplayValue(): ReactNode { const rawValue = this.getRawValue(); return typeof rawValue !== 'undefined' && this._aggField.isCount() ? parseInt(rawValue as string, 10).toLocaleString() diff --git a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.tsx similarity index 83% rename from x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts rename to x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.tsx index 04e8086ea6480..de6421895c654 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React, { ReactNode } from 'react'; import _ from 'lodash'; import { type Filter, buildExistsFilter, buildPhraseFilter } from '@kbn/es-query'; import { ITooltipProperty } from './tooltip_property'; @@ -45,7 +46,7 @@ export class ESTooltipProperty implements ITooltipProperty { return this._indexPattern.fields.getByName(this._field.getRootName()); } - getHtmlDisplayValue(): string { + getHtmlDisplayValue(): ReactNode { if (typeof this.getRawValue() === 'undefined') { return '-'; } @@ -62,9 +63,19 @@ export class ESTooltipProperty implements ITooltipProperty { const formatter = this._indexPattern.getFormatterForField(indexPatternField); const htmlConverter = formatter.getConverterFor('html'); - return htmlConverter - ? htmlConverter(this.getRawValue()) - : formatter.convert(this.getRawValue()); + return htmlConverter ? ( + + ) : ( + formatter.convert(this.getRawValue()) + ); } isFilterable(): boolean { diff --git a/x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property/join_tooltip_property.tsx b/x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property/join_tooltip_property.tsx index 30793572b4f6a..c6ca5e9b3d5f9 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property/join_tooltip_property.tsx +++ b/x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property/join_tooltip_property.tsx @@ -41,7 +41,7 @@ export class JoinTooltipProperty implements ITooltipProperty { return this._tooltipProperty.getRawValue(); } - getHtmlDisplayValue(): string { + getHtmlDisplayValue(): ReactNode { return this._tooltipProperty.getHtmlDisplayValue(); } diff --git a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts index 5fa23a3266190..0a4ca8af5dd79 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts @@ -16,7 +16,7 @@ import type { TooltipFeature } from '../../../../../plugins/maps/common/descript export interface ITooltipProperty { getPropertyKey(): string; getPropertyName(): string | ReactNode; - getHtmlDisplayValue(): string; + getHtmlDisplayValue(): ReactNode; getRawValue(): string | string[] | undefined; isFilterable(): boolean; getESFilters(): Promise; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/__snapshots__/feature_properties.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/__snapshots__/feature_properties.test.tsx.snap index 29df06a64a3f2..06440a83fb9c6 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/__snapshots__/feature_properties.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/__snapshots__/feature_properties.test.tsx.snap @@ -16,12 +16,9 @@ exports[`FeatureProperties should render 1`] = ` + > + foobar1 + + > + foobar2 + @@ -76,12 +70,9 @@ exports[`FeatureProperties should show filter button for filterable properties 1 + > + foobar1 + + > + foobar2 + @@ -135,12 +123,9 @@ exports[`FeatureProperties should show view actions button when there are availa + > + foobar1 + @@ -181,12 +166,9 @@ exports[`FeatureProperties should show view actions button when there are availa + > + foobar2 + diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx index a994731f0adec..25e1a69805d96 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx @@ -217,16 +217,7 @@ export class FeatureProperties extends Component { {tooltipProperty.getPropertyName()} - + {tooltipProperty.getHtmlDisplayValue()} @@ -338,16 +329,7 @@ export class FeatureProperties extends Component { {tooltipProperty.getPropertyName()} - + {tooltipProperty.getHtmlDisplayValue()} {this._renderFilterCell(tooltipProperty)} ); diff --git a/x-pack/plugins/ml/public/maps/anomaly_source.tsx b/x-pack/plugins/ml/public/maps/anomaly_source.tsx index e2d92a730d95a..07f6df52f44e5 100644 --- a/x-pack/plugins/ml/public/maps/anomaly_source.tsx +++ b/x-pack/plugins/ml/public/maps/anomaly_source.tsx @@ -251,13 +251,7 @@ export class AnomalySource implements IVectorSource { continue; } if (properties.hasOwnProperty(key)) { - const label = ANOMALY_SOURCE_FIELDS[key]?.label; - if (label) { - tooltipProperties.push(new AnomalySourceTooltipProperty(label, properties[key])); - } else if (!ANOMALY_SOURCE_FIELDS[key]) { - // partition field keys will be different each time so won't be in ANOMALY_SOURCE_FIELDS - tooltipProperties.push(new AnomalySourceTooltipProperty(key, properties[key])); - } + tooltipProperties.push(new AnomalySourceTooltipProperty(key, properties[key])); } } return tooltipProperties; diff --git a/x-pack/plugins/ml/public/maps/anomaly_source_field.ts b/x-pack/plugins/ml/public/maps/anomaly_source_field.tsx similarity index 77% rename from x-pack/plugins/ml/public/maps/anomaly_source_field.ts rename to x-pack/plugins/ml/public/maps/anomaly_source_field.tsx index ac60cb3b54fb5..70eb0a8d9c408 100644 --- a/x-pack/plugins/ml/public/maps/anomaly_source_field.ts +++ b/x-pack/plugins/ml/public/maps/anomaly_source_field.tsx @@ -6,11 +6,12 @@ */ // eslint-disable-next-line max-classes-per-file +import React, { ReactNode } from 'react'; import { escape } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Filter } from '@kbn/es-query'; import { IField, IVectorSource } from '../../../maps/public'; -import { FIELD_ORIGIN } from '../../../maps/common'; +import { FIELD_ORIGIN, DECIMAL_DEGREES_PRECISION } from '../../../maps/common'; import { TileMetaFeature } from '../../../maps/common/descriptor_types'; import { AnomalySource } from './anomaly_source'; import { ITooltipProperty } from '../../../maps/public'; @@ -25,6 +26,8 @@ export const TYPICAL_TO_ACTUAL = i18n.translate('xpack.ml.maps.anomalyLayerTypic defaultMessage: 'Typical to actual', }); +const INFLUENCER_LIMIT = 3; + export const ANOMALY_SOURCE_FIELDS: Record> = { record_score: { label: i18n.translate('xpack.ml.maps.anomalyLayerRecordScoreLabel', { @@ -50,15 +53,11 @@ export const ANOMALY_SOURCE_FIELDS: Record> = { }), type: 'string', }, - // this value is only used to place the point on the map - actual: {}, - actualDisplay: { + actual: { label: ACTUAL_LABEL, type: 'string', }, - // this value is only used to place the point on the map - typical: {}, - typicalDisplay: { + typical: { label: TYPICAL_LABEL, type: 'string', }, @@ -106,23 +105,55 @@ export const ANOMALY_SOURCE_FIELDS: Record> = { }, }; +const ROUND_POWER = Math.pow(10, DECIMAL_DEGREES_PRECISION); +function roundCoordinate(coordinate: number) { + return Math.round(Number(coordinate) * ROUND_POWER) / ROUND_POWER; +} + export class AnomalySourceTooltipProperty implements ITooltipProperty { - constructor(private readonly _label: string, private readonly _value: string) {} + constructor(private readonly _field: string, private readonly _value: string) {} async getESFilters(): Promise { return []; } - getHtmlDisplayValue(): string { + getHtmlDisplayValue(): string | ReactNode { + if (this._field === 'influencers') { + try { + const influencers = JSON.parse(this._value) as Array<{ + influencer_field_name: string; + influencer_field_values: string[]; + }>; + return ( +
    + {influencers.map(({ influencer_field_name: name, influencer_field_values: values }) => { + return
  • {`${name}: ${values.slice(0, INFLUENCER_LIMIT).join(', ')}`}
  • ; + })} +
+ ); + } catch (error) { + // ignore error and display unformated value + } + } else if (this._field === 'actual' || this._field === 'typical') { + try { + const point = JSON.parse(this._value) as number[]; + return `[${roundCoordinate(point[0])}, ${roundCoordinate(point[1])}]`; + } catch (error) { + // ignore error and display unformated value + } + } + return this._value.toString(); } getPropertyKey(): string { - return this._label; + return this._field; } getPropertyName(): string { - return this._label; + return ANOMALY_SOURCE_FIELDS[this._field] && ANOMALY_SOURCE_FIELDS[this._field].label + ? ANOMALY_SOURCE_FIELDS[this._field].label + : this._field; } getRawValue(): string | string[] | undefined { @@ -146,7 +177,7 @@ export class AnomalySourceField implements IField { async createTooltipProperty(value: string | string[] | undefined): Promise { return new AnomalySourceTooltipProperty( - await this.getLabel(), + this._field, escape(Array.isArray(value) ? value.join() : value ? value : '') ); } diff --git a/x-pack/plugins/ml/public/maps/maps_util.test.js b/x-pack/plugins/ml/public/maps/maps_util.test.js index dd6fde9e8b28c..be21a292eb163 100644 --- a/x-pack/plugins/ml/public/maps/maps_util.test.js +++ b/x-pack/plugins/ml/public/maps/maps_util.test.js @@ -5,7 +5,7 @@ * 2.0. */ -import { getInfluencersHtmlString, getResultsForJobId } from './util'; +import { getResultsForJobId } from './util'; import { mlResultsServiceMock, typicalExpected, @@ -14,47 +14,6 @@ import { } from './results.test.mock'; describe('Maps util', () => { - describe('getInfluencersHtmlString', () => { - const splitField = 'split_field_influencer'; - const valueFour = 'value_four'; - const influencerFour = 'influencer_four'; - const influencers = [ - { - influencer_field_name: 'influencer_one', - influencer_field_values: ['value_one', 'value_two', 'value_three', valueFour], - }, - { - influencer_field_name: 'influencer_two', - influencer_field_values: ['value_one', 'value_two', 'value_three', valueFour], - }, - { - influencer_field_name: splitField, - influencer_field_values: ['value_one', 'value_two'], - }, - { - influencer_field_name: 'influencer_three', - influencer_field_values: ['value_one', 'value_two', 'value_three', valueFour], - }, - { - influencer_field_name: influencerFour, - influencer_field_values: ['value_one', 'value_two', 'value_three', valueFour], - }, - ]; - - test('should create the html string when given an array of influencers', () => { - const expected = - '
  • influencer_one: value_one, value_two, value_three
  • influencer_two: value_one, value_two, value_three
  • influencer_three: value_one, value_two, value_three
'; - const actual = getInfluencersHtmlString(influencers, splitField); - expect(actual).toBe(expected); - // Should not include split field - expect(actual.includes(splitField)).toBe(false); - // should limit to the first three influencer values - expect(actual.includes(valueFour)).toBe(false); - // should limit to the first three influencer names - expect(actual.includes(influencerFour)).toBe(false); - }); - }); - describe('getResultsForJobId', () => { const jobId = 'jobId'; const searchFilters = { diff --git a/x-pack/plugins/ml/public/maps/results.test.mock.ts b/x-pack/plugins/ml/public/maps/results.test.mock.ts index f718e818ba73c..8db3aaeda62ad 100644 --- a/x-pack/plugins/ml/public/maps/results.test.mock.ts +++ b/x-pack/plugins/ml/public/maps/results.test.mock.ts @@ -5,6 +5,30 @@ * 2.0. */ +const influencers = [ + { + influencer_field_name: 'geo.dest', + influencer_field_values: ['CN', 'DO', 'RU', 'US'], + }, + { + influencer_field_name: 'clientip', + influencer_field_values: [ + '108.131.25.207', + '192.41.143.247', + '194.12.201.131', + '41.91.106.242', + ], + }, + { + influencer_field_name: 'agent.keyword', + influencer_field_values: [ + 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)', + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24', + 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1', + ], + }, +]; + const results = { took: 9, timed_out: false, @@ -41,29 +65,7 @@ const results = { typical: [39.9864616394043, -97.862548828125], actual: [29.261693651787937, -121.93940273718908], field_name: 'geo.coordinates', - influencers: [ - { - influencer_field_name: 'geo.dest', - influencer_field_values: ['CN', 'DO', 'RU', 'US'], - }, - { - influencer_field_name: 'clientip', - influencer_field_values: [ - '108.131.25.207', - '192.41.143.247', - '194.12.201.131', - '41.91.106.242', - ], - }, - { - influencer_field_name: 'agent.keyword', - influencer_field_values: [ - 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)', - 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24', - 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1', - ], - }, - ], + influencers, geo_results: { typical_point: '39.986461639404,-97.862548828125', actual_point: '29.261693651788,-121.939402737189', @@ -87,15 +89,12 @@ export const typicalExpected = { geometry: { coordinates: [-97.862548828125, 39.986461639404], type: 'Point' }, properties: { actual: [-121.939402737189, 29.261693651788], - actualDisplay: [-121.94, 29.26], fieldName: 'geo.coordinates', functionDescription: 'lat_long', - influencers: - '
  • geo.dest: CN, DO, RU
  • clientip: 108.131.25.207, 192.41.143.247, 194.12.201.131
  • agent.keyword: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322), Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24, Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1
', + influencers, record_score: 77, timestamp: 'February 27th 2022, 10:00:00', typical: [-97.862548828125, 39.986461639404], - typicalDisplay: [-97.86, 39.99], }, type: 'Feature', }, @@ -114,15 +113,12 @@ export const actualExpected = { }, properties: { actual: [-121.939402737189, 29.261693651788], - actualDisplay: [-121.94, 29.26], typical: [-97.862548828125, 39.986461639404], - typicalDisplay: [-97.86, 39.99], fieldName: 'geo.coordinates', functionDescription: 'lat_long', timestamp: 'February 27th 2022, 10:00:00', record_score: 77, - influencers: - '
  • geo.dest: CN, DO, RU
  • clientip: 108.131.25.207, 192.41.143.247, 194.12.201.131
  • agent.keyword: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322), Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24, Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1
', + influencers, }, }, ], @@ -141,15 +137,12 @@ export const typicalToActualExpected = { }, properties: { actual: [-121.939402737189, 29.261693651788], - actualDisplay: [-121.94, 29.26], typical: [-97.862548828125, 39.986461639404], - typicalDisplay: [-97.86, 39.99], fieldName: 'geo.coordinates', functionDescription: 'lat_long', timestamp: 'February 27th 2022, 10:00:00', record_score: 77, - influencers: - '
  • geo.dest: CN, DO, RU
  • clientip: 108.131.25.207, 192.41.143.247, 194.12.201.131
  • agent.keyword: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322), Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24, Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1
', + influencers, }, }, ], diff --git a/x-pack/plugins/ml/public/maps/util.ts b/x-pack/plugins/ml/public/maps/util.ts index 11e6b6f5a3920..f2a4719bc4ef8 100644 --- a/x-pack/plugins/ml/public/maps/util.ts +++ b/x-pack/plugins/ml/public/maps/util.ts @@ -23,42 +23,12 @@ export const ML_ANOMALY_LAYERS = { } as const; export type MlAnomalyLayersType = typeof ML_ANOMALY_LAYERS[keyof typeof ML_ANOMALY_LAYERS]; -const INFLUENCER_LIMIT = 3; -const INFLUENCER_MAX_VALUES = 3; - -export function getInfluencersHtmlString( - influencers: Array<{ influencer_field_name: string; influencer_field_values: string[] }>, - splitFields: string[] -) { - let htmlString = '
    '; - let influencerCount = 0; - for (let i = 0; i < influencers.length; i++) { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { influencer_field_name, influencer_field_values } = influencers[i]; - // Skip if there are no values or it's a partition field - if (!influencer_field_values.length || splitFields.includes(influencer_field_name)) continue; - - const fieldValuesString = influencer_field_values.slice(0, INFLUENCER_MAX_VALUES).join(', '); - - htmlString += `
  • ${influencer_field_name}: ${fieldValuesString}
  • `; - influencerCount += 1; - - if (influencerCount === INFLUENCER_LIMIT) { - break; - } - } - htmlString += '
'; - - return htmlString; -} // Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs -function getCoordinates(actualCoordinateStr: string, round: boolean = false): number[] { - const convertWithRounding = (point: string) => Math.round(Number(point) * 100) / 100; - const convert = (point: string) => Number(point); - return actualCoordinateStr +function getCoordinates(latLonString: string): number[] { + return latLonString .split(',') - .map(round ? convertWithRounding : convert) + .map((coordinate: string) => Number(coordinate)) .reverse(); } @@ -136,21 +106,10 @@ export async function getResultsForJobId( const features: Feature[] = resp?.hits.hits.map(({ _source }) => { const geoResults = _source.geo_results; - const actualCoordStr = geoResults && geoResults.actual_point; - const typicalCoordStr = geoResults && geoResults.typical_point; - let typical: number[] = []; - let typicalDisplay: number[] = []; - let actual: number[] = []; - let actualDisplay: number[] = []; - - if (actualCoordStr !== undefined) { - actual = getCoordinates(actualCoordStr); - actualDisplay = getCoordinates(actualCoordStr, true); - } - if (typicalCoordStr !== undefined) { - typical = getCoordinates(typicalCoordStr); - typicalDisplay = getCoordinates(typicalCoordStr, true); - } + const actual = + geoResults && geoResults.actual_point ? getCoordinates(geoResults.actual_point) : [0, 0]; + const typical = + geoResults && geoResults.typical_point ? getCoordinates(geoResults.typical_point) : [0, 0]; let geometry: Geometry; if (locationType === ML_ANOMALY_LAYERS.TYPICAL || locationType === ML_ANOMALY_LAYERS.ACTUAL) { @@ -173,25 +132,30 @@ export async function getResultsForJobId( ...(_source.over_field_name ? { [_source.over_field_name]: _source.over_field_value } : {}), }; + const splitFieldKeys = Object.keys(splitFields); + const influencers = _source.influencers + ? _source.influencers.filter( + ({ influencer_field_name: name, influencer_field_values: values }) => { + // remove influencers without values and influencers on partition fields + return values.length && !splitFieldKeys.includes(name); + } + ) + : []; + return { type: 'Feature', geometry, properties: { actual, - actualDisplay, typical, - typicalDisplay, fieldName: _source.field_name, functionDescription: _source.function_description, timestamp: formatHumanReadableDateTimeSeconds(_source.timestamp), record_score: Math.floor(_source.record_score), ...(Object.keys(splitFields).length > 0 ? splitFields : {}), - ...(_source.influencers?.length + ...(influencers.length ? { - influencers: getInfluencersHtmlString( - _source.influencers, - Object.keys(splitFields) - ), + influencers, } : {}), }, From 1f00343bab2d57b3e2d5dad1d99e9caa8a4e0b52 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 29 Mar 2022 20:49:29 -0400 Subject: [PATCH 046/108] Security Solution: enhance getAggregatableFields perf (#128824) The `getAggregatableFields` function is used to determine which fields can be passed to a stacked visualization. When there are many fields, this function can run long. This commit reimplements the function so that it run faster and uses less memory. --- .../common/__snapshots__/hooks.test.tsx.snap | 3 +++ .../alerts_kpis/common/hooks.test.tsx | 11 +++++++++- .../components/alerts_kpis/common/hooks.ts | 21 +++++++++---------- 3 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/__snapshots__/hooks.test.tsx.snap diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/__snapshots__/hooks.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/__snapshots__/hooks.test.tsx.snap new file mode 100644 index 0000000000000..6637bcf724c0f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/__snapshots__/hooks.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAggregatableFields 1`] = `Array []`; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx index d68c5c303cfd7..064798040fb8b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx @@ -7,7 +7,12 @@ import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { useInspectButton, UseInspectButtonParams, useStackByFields } from './hooks'; +import { + getAggregatableFields, + useInspectButton, + UseInspectButtonParams, + useStackByFields, +} from './hooks'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { TestProviders } from '../../../../common/mock'; @@ -16,6 +21,10 @@ jest.mock('react-router-dom', () => { return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; }); +test('getAggregatableFields', () => { + expect(getAggregatableFields(mockBrowserFields)).toMatchSnapshot(); +}); + describe('hooks', () => { describe('useInspectButton', () => { const defaultParams: UseInspectButtonParams = { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts index 65b87670810b0..85d8be5a2e846 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts @@ -55,17 +55,16 @@ export const useInspectButton = ({ }, [setQuery, loading, response, request, refetch, uniqueQueryId, deleteQuery]); }; -function getAggregatableFields(fields: { [fieldName: string]: Partial }) { - return Object.entries(fields).reduce( - (filteredOptions: EuiComboBoxOptionOption[], [key, field]) => { - if (field.aggregatable === true) { - return [...filteredOptions, { label: key, value: key }]; - } else { - return filteredOptions; - } - }, - [] - ); +export function getAggregatableFields(fields: { + [fieldName: string]: Partial; +}): EuiComboBoxOptionOption[] { + const result = []; + for (const [key, field] of Object.entries(fields)) { + if (field.aggregatable === true) { + result.push({ label: key, value: key }); + } + } + return result; } export const useStackByFields = () => { From 05a8b3218270fd2ca7b3f458bea3d3751cfb2621 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 29 Mar 2022 20:51:16 -0400 Subject: [PATCH 047/108] [CI] Build TS refs before API docs (#128816) --- .buildkite/scripts/steps/build_api_docs.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.buildkite/scripts/steps/build_api_docs.sh b/.buildkite/scripts/steps/build_api_docs.sh index 2f63d6efa0941..00387fa657a59 100755 --- a/.buildkite/scripts/steps/build_api_docs.sh +++ b/.buildkite/scripts/steps/build_api_docs.sh @@ -4,5 +4,11 @@ set -euo pipefail .buildkite/scripts/bootstrap.sh +echo "--- Build TS Refs" +node scripts/build_ts_refs \ + --clean \ + --no-cache \ + --force + echo "--- Build API Docs" node --max-old-space-size=12000 scripts/build_api_docs From 66f4b12b05d5ae2000cebb3f7c26a8e4c876f5e2 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Tue, 29 Mar 2022 22:48:10 -0300 Subject: [PATCH 048/108] [Security solution][Session view] - Adding Session Viewer Icons (#128828) --- .../public/timelines/components/timeline/body/actions/index.tsx | 2 +- .../session_view/public/components/process_tree_node/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 300f000bb4a63..cd96cfaf42db1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -257,7 +257,7 @@ const ActionsComponent: React.FC = ({ diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index 4a75948d3d3aa..213b8724fbf8d 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -207,7 +207,7 @@ export function ProcessTreeNode({ const showUserEscalation = user.id && user.id !== parent.user?.id; const interactiveSession = !!tty; - const sessionIcon = interactiveSession ? 'consoleApp' : 'compute'; + const sessionIcon = interactiveSession ? 'desktop' : 'gear'; const iconTestSubj = hasExec ? 'sessionView:processTreeNodeExecIcon' : 'sessionView:processTreeNodeForkIcon'; From 436dfbed8cb43fd39e32b684871a65360db22c8a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 30 Mar 2022 03:52:01 +0200 Subject: [PATCH 049/108] [Monitor management] Show public beta fair usage (#128770) * show public beta fair usage * add callout for async service errors Co-authored-by: Dominique Clarke --- .../monitor_management/locations.ts | 15 ++- .../monitor_management/monitor_types.ts | 27 ++-- .../action_bar/action_bar.tsx | 40 +----- .../monitor_list/monitor_async_error.test.tsx | 116 ++++++++++++++++++ .../monitor_list/monitor_async_error.tsx | 75 +++++++++++ .../monitor_list/monitor_list_container.tsx | 2 + .../monitor_management/show_sync_errors.tsx | 52 ++++++++ .../monitor_management/monitor_management.tsx | 4 +- .../state/reducers/monitor_management.ts | 1 + .../synthetics_service/synthetics_service.ts | 7 +- .../synthetics_service/get_monitor.ts | 3 +- 11 files changed, 285 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/show_sync_errors.tsx diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts index d11ae7c655405..82d2bc8afa412 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts @@ -64,12 +64,15 @@ export const ServiceLocationErrors = t.array( status: t.number, }), t.partial({ - failed_monitors: t.array( - t.interface({ - id: t.string, - message: t.string, - }) - ), + failed_monitors: t.union([ + t.array( + t.interface({ + id: t.string, + message: t.string, + }) + ), + t.null, + ]), }), ]), }) diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts index 44c643d2160d1..872ccdbb71ec8 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { secretKeys } from '../../constants/monitor_management'; import { ConfigKey } from './config_key'; -import { LocationsCodec } from './locations'; +import { LocationsCodec, ServiceLocationErrors } from './locations'; import { DataStreamCodec, ModeCodec, @@ -306,14 +306,23 @@ export type EncryptedSyntheticsMonitorWithId = t.TypeOf< typeof EncryptedSyntheticsMonitorWithIdCodec >; -export const MonitorManagementListResultCodec = t.type({ - monitors: t.array( - t.interface({ id: t.string, attributes: EncryptedSyntheticsMonitorCodec, updated_at: t.string }) - ), - page: t.number, - perPage: t.number, - total: t.union([t.number, t.null]), -}); +export const MonitorManagementListResultCodec = t.intersection([ + t.type({ + monitors: t.array( + t.interface({ + id: t.string, + attributes: EncryptedSyntheticsMonitorCodec, + updated_at: t.string, + }) + ), + page: t.number, + perPage: t.number, + total: t.union([t.number, t.null]), + }), + t.partial({ + syncErrors: ServiceLocationErrors, + }), +]); export type MonitorManagementListResult = t.TypeOf; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx index 3b30458974ed7..5f6e67e363171 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx @@ -19,7 +19,6 @@ import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; -import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { MONITOR_MANAGEMENT_ROUTE } from '../../../../common/constants'; import { UptimeSettingsContext } from '../../../contexts'; @@ -32,6 +31,7 @@ import { TestRun } from '../test_now_mode/test_now_mode'; import { monitorManagementListSelector } from '../../../state/selectors'; import { kibanaService } from '../../../state/kibana_service'; +import { showSyncErrors } from '../show_sync_errors'; export interface ActionBarProps { monitor: SyntheticsMonitor; @@ -103,43 +103,7 @@ export const ActionBar = ({ }); setIsSuccessful(true); } else if (hasErrors && !loading) { - Object.values(data.attributes.errors!).forEach((location) => { - const { status: responseStatus, reason } = location.error || {}; - kibanaService.toasts.addWarning({ - title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { - defaultMessage: `Unable to sync monitor config`, - }), - text: toMountPoint( - <> -

- {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { - defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, - values: { - location: locations?.find((loc) => loc?.id === location.locationId)?.label, - }, - })} -

- {responseStatus || reason ? ( -

- {responseStatus - ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { - defaultMessage: 'Status: {status}. ', - values: { status: responseStatus }, - }) - : null} - {reason - ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { - defaultMessage: 'Reason: {reason}.', - values: { reason }, - }) - : null} -

- ) : null} - - ), - toastLifeTimeMs: 30000, - }); - }); + showSyncErrors(data.attributes.errors, locations); setIsSuccessful(true); } }, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx new file mode 100644 index 0000000000000..1122d136c926c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx @@ -0,0 +1,116 @@ +/* + * 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 { screen } from '@testing-library/react'; +import React from 'react'; +import { DEFAULT_THROTTLING } from '../../../../common/runtime_types'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; +import { MonitorAsyncError } from './monitor_async_error'; + +describe('', () => { + const location1 = 'US Central'; + const location2 = 'US North'; + const reason1 = 'Unauthorized'; + const reason2 = 'Forbidden'; + const status1 = 401; + const status2 = 403; + const state = { + monitorManagementList: { + throttling: DEFAULT_THROTTLING, + enablement: null, + list: { + perPage: 5, + page: 1, + total: 6, + monitors: [], + syncErrors: [ + { + locationId: 'us_central', + error: { + reason: reason1, + status: status1, + }, + }, + { + locationId: 'us_north', + error: { + reason: reason2, + status: status2, + }, + }, + ], + }, + locations: [ + { + id: 'us_central', + label: location1, + geo: { + lat: 0, + lon: 0, + }, + url: '', + }, + { + id: 'us_north', + label: location2, + geo: { + lat: 0, + lon: 0, + }, + url: '', + }, + ], + error: { + serviceLocations: null, + monitorList: null, + enablement: null, + }, + loading: { + monitorList: true, + serviceLocations: false, + enablement: false, + }, + syntheticsService: { + loading: false, + }, + } as MonitorManagementListState, + }; + + it('renders when errors are defined', () => { + render(, { state }); + + expect(screen.getByText(new RegExp(reason1))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(`${status1}`))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(reason2))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(`${status2}`))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(location1))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(location2))).toBeInTheDocument(); + }); + + it('renders null when errors are empty', () => { + render(, { + state: { + ...state, + monitorManagementList: { + ...state.monitorManagementList, + list: { + ...state.monitorManagementList.list, + syncErrors: [], + }, + }, + }, + }); + + expect(screen.queryByText(new RegExp(reason1))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(`${status1}`))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(reason2))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(`${status2}`))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(location1))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(location2))).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx new file mode 100644 index 0000000000000..c9e9dba2027a4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx @@ -0,0 +1,75 @@ +/* + * 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, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { monitorManagementListSelector } from '../../../state/selectors'; + +export const MonitorAsyncError = () => { + const [isDismissed, setIsDismissed] = useState(false); + const { list, locations } = useSelector(monitorManagementListSelector); + const syncErrors = list.syncErrors; + const hasSyncErrors = syncErrors && syncErrors.length > 0; + + return hasSyncErrors && !isDismissed ? ( + <> + + } + color="warning" + iconType="alert" + > +

+ +

+
    + {Object.values(syncErrors).map((e) => { + return ( +
  • {`${ + locations.find((location) => location.id === e.locationId)?.label + } - ${STATUS_LABEL}: ${e.error.status}; ${REASON_LABEL}: ${e.error.reason}.`}
  • + ); + })} +
+ setIsDismissed(true)} color="warning"> + {DISMISS_LABEL} + +
+ + + ) : null; +}; + +const REASON_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.monitorSync.failure.reasonLabel', + { + defaultMessage: 'Reason', + } +); + +const STATUS_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.monitorSync.failure.statusLabel', + { + defaultMessage: 'Status', + } +); + +const DISMISS_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.monitorSync.failure.dismissLabel', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx index a3f041a33a9f8..53afdf49c1592 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx @@ -13,6 +13,7 @@ import { ConfigKey } from '../../../../common/runtime_types'; import { getMonitors } from '../../../state/actions'; import { monitorManagementListSelector } from '../../../state/selectors'; import { MonitorManagementListPageState } from './monitor_list'; +import { MonitorAsyncError } from './monitor_async_error'; import { useInlineErrors } from '../hooks/use_inline_errors'; import { MonitorListTabs } from './list_tabs'; import { AllMonitors } from './all_monitors'; @@ -66,6 +67,7 @@ export const MonitorListContainer: React.FC = () => { return ( <> + { + Object.values(errors).forEach((location) => { + const { status: responseStatus, reason } = location.error || {}; + kibanaService.toasts.addWarning({ + title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { + defaultMessage: `Unable to sync monitor config`, + }), + text: toMountPoint( + <> +

+ {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { + defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, + values: { + location: locations?.find((loc) => loc?.id === location.locationId)?.label, + }, + })} +

+ {responseStatus || reason ? ( +

+ {responseStatus + ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { + defaultMessage: 'Status: {status}. ', + values: { status: responseStatus }, + }) + : null} + {reason + ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { + defaultMessage: 'Reason: {reason}.', + values: { reason }, + }) + : null} +

+ ) : null} + + ), + toastLifeTimeMs: 30000, + }); + }); +}; diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx index 3e0e9b955f31f..71785dbaf78ee 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx @@ -17,6 +17,7 @@ import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadc import { MonitorListContainer } from '../../components/monitor_management/monitor_list/monitor_list_container'; import { EnablementEmptyState } from '../../components/monitor_management/monitor_list/enablement_empty_state'; import { useEnablement } from '../../components/monitor_management/hooks/use_enablement'; +import { useLocations } from '../../components/monitor_management/hooks/use_locations'; import { Loader } from '../../components/monitor_management/loader/loader'; export const MonitorManagementPage: React.FC = () => { @@ -32,6 +33,7 @@ export const MonitorManagementPage: React.FC = () => { loading: enablementLoading, enableSynthetics, } = useEnablement(); + const { loading: locationsLoading } = useLocations(); const { list: monitorList } = useSelector(monitorManagementListSelector); const { isEnabled } = enablement; @@ -62,7 +64,7 @@ export const MonitorManagementPage: React.FC = () => { return ( <> ({ search: schema.maybe(schema.string()), }), }, - handler: async ({ request, savedObjectsClient }): Promise => { + handler: async ({ request, savedObjectsClient, server }): Promise => { const { perPage = 50, page, sortField, sortOrder, search } = request.query; // TODO: add query/filtering params const { @@ -78,6 +78,7 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ ...rest, perPage: perPageT, monitors, + syncErrors: server.syntheticsService.syncErrors, }; }, }); From ace270fba0abb2c8b35522e3179d5a79e818b7a4 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Tue, 29 Mar 2022 19:58:49 -0600 Subject: [PATCH 050/108] [Security Solution][Detections] Fixes show alerts for execution action on Rule Execution Log (#128843) ## Summary One-liner fix for the `Show alerts for execution` action on the Rule Execution Log table. Had the wrong key after changing the response interface. Working on the follow-up feedback PR from https://github.com/elastic/kibana/pull/126215, and will be including additional test coverage there, but wanted to get this in before the first BC for testing purposes.

--- .../rules/details/execution_log_table/execution_log_table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx index 212ac9ec5b94f..03ccd9ea5ee70 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx @@ -223,7 +223,7 @@ const ExecutionLogTableComponent: React.FC = ({ icon: 'filter', type: 'icon', onClick: (value: object) => { - const executionId = get(value, EXECUTION_UUID_FIELD_NAME); + const executionId = get(value, 'execution_uuid'); if (executionId) { onFilterByExecutionIdCallback(executionId); } From 253b9a3c8501e125e6a6472902ebe073f4e471ca Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 29 Mar 2022 22:12:08 -0400 Subject: [PATCH 051/108] Time slider control (#128305) Adds the Time Slider Control to the controls plugin. --- .../steps/storybooks/build_and_upload.js | 1 + .../time_slider_persistable_state.ts | 47 +++ .../common/control_types/time_slider/types.ts | 17 + src/plugins/controls/common/index.ts | 2 + src/plugins/controls/jest.config.js | 18 + src/plugins/controls/jest_setup.ts | 14 + .../public/__stories__/controls.stories.tsx | 4 +- .../storybook_control_factories.ts | 6 + .../time_slider.component.stories.tsx | 190 ++++++++++ .../public/control_types/time_slider/index.ts | 11 + .../time_slider/time_slider.component.scss | 47 +++ .../time_slider/time_slider.component.tsx | 336 ++++++++++++++++++ .../control_types/time_slider/time_slider.tsx | 94 +++++ .../time_slider/time_slider_editor.tsx | 103 ++++++ .../time_slider_embeddable.test.ts | 287 +++++++++++++++ .../time_slider/time_slider_embeddable.tsx | 321 +++++++++++++++++ .../time_slider_embeddable_factory.tsx | 60 ++++ .../time_slider/time_slider_reducers.ts | 21 ++ .../time_slider/time_slider_strings.ts | 46 +++ src/plugins/controls/public/index.ts | 7 +- src/plugins/controls/public/plugin.ts | 18 + src/plugins/controls/public/services/data.ts | 15 + src/plugins/controls/public/services/index.ts | 2 + .../controls/public/services/kibana/data.ts | 81 ++++- .../controls/public/services/kibana/index.ts | 2 + .../public/services/kibana/settings.ts | 27 ++ .../controls/public/services/settings.ts | 12 + .../public/services/storybook/data.ts | 8 +- .../public/services/storybook/index.ts | 2 + .../public/services/storybook/settings.ts | 16 + .../controls/public/services/stub/index.ts | 2 + .../time_slider_embeddable_factory.ts | 22 ++ src/plugins/controls/server/plugin.ts | 2 + src/plugins/controls/tsconfig.json | 3 +- 34 files changed, 1835 insertions(+), 9 deletions(-) create mode 100644 src/plugins/controls/common/control_types/time_slider/time_slider_persistable_state.ts create mode 100644 src/plugins/controls/common/control_types/time_slider/types.ts create mode 100644 src/plugins/controls/jest.config.js create mode 100644 src/plugins/controls/jest_setup.ts create mode 100644 src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx create mode 100644 src/plugins/controls/public/control_types/time_slider/index.ts create mode 100644 src/plugins/controls/public/control_types/time_slider/time_slider.component.scss create mode 100644 src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx create mode 100644 src/plugins/controls/public/control_types/time_slider/time_slider.tsx create mode 100644 src/plugins/controls/public/control_types/time_slider/time_slider_editor.tsx create mode 100644 src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.test.ts create mode 100644 src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.tsx create mode 100644 src/plugins/controls/public/control_types/time_slider/time_slider_embeddable_factory.tsx create mode 100644 src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts create mode 100644 src/plugins/controls/public/control_types/time_slider/time_slider_strings.ts create mode 100644 src/plugins/controls/public/services/kibana/settings.ts create mode 100644 src/plugins/controls/public/services/settings.ts create mode 100644 src/plugins/controls/public/services/storybook/settings.ts create mode 100644 src/plugins/controls/server/control_types/time_slider/time_slider_embeddable_factory.ts diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js index 9d40edc905763..482640b8d9cc0 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.js +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js @@ -16,6 +16,7 @@ const STORYBOOKS = [ 'canvas', 'ci_composite', 'cloud', + 'controls', 'custom_integrations', 'dashboard_enhanced', 'dashboard', diff --git a/src/plugins/controls/common/control_types/time_slider/time_slider_persistable_state.ts b/src/plugins/controls/common/control_types/time_slider/time_slider_persistable_state.ts new file mode 100644 index 0000000000000..660f33a15a33d --- /dev/null +++ b/src/plugins/controls/common/control_types/time_slider/time_slider_persistable_state.ts @@ -0,0 +1,47 @@ +/* + * 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 { + EmbeddableStateWithType, + EmbeddablePersistableStateService, +} from '../../../../embeddable/common'; +import { TimeSliderControlEmbeddableInput } from './types'; +import { SavedObjectReference } from '../../../../../core/types'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../../data_views/common'; + +type TimeSliderInputWithType = Partial & { type: string }; +const dataViewReferenceName = 'timeSliderDataView'; + +export const createTimeSliderInject = (): EmbeddablePersistableStateService['inject'] => { + return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => { + const workingState = { ...state } as EmbeddableStateWithType | TimeSliderInputWithType; + references.forEach((reference) => { + if (reference.name === dataViewReferenceName) { + (workingState as TimeSliderInputWithType).dataViewId = reference.id; + } + }); + return workingState as EmbeddableStateWithType; + }; +}; + +export const createTimeSliderExtract = (): EmbeddablePersistableStateService['extract'] => { + return (state: EmbeddableStateWithType) => { + const workingState = { ...state } as EmbeddableStateWithType | TimeSliderInputWithType; + const references: SavedObjectReference[] = []; + + if ('dataViewId' in workingState) { + references.push({ + name: dataViewReferenceName, + type: DATA_VIEW_SAVED_OBJECT_TYPE, + id: workingState.dataViewId!, + }); + delete workingState.dataViewId; + } + return { state: workingState as EmbeddableStateWithType, references }; + }; +}; diff --git a/src/plugins/controls/common/control_types/time_slider/types.ts b/src/plugins/controls/common/control_types/time_slider/types.ts new file mode 100644 index 0000000000000..73d364da80caa --- /dev/null +++ b/src/plugins/controls/common/control_types/time_slider/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ControlInput } from '../../types'; + +export const TIME_SLIDER_CONTROL = 'timeSlider'; + +export interface TimeSliderControlEmbeddableInput extends ControlInput { + fieldName: string; + dataViewId: string; + value?: [number | null, number | null]; +} diff --git a/src/plugins/controls/common/index.ts b/src/plugins/controls/common/index.ts index 23779e225ce47..ff2c39346f075 100644 --- a/src/plugins/controls/common/index.ts +++ b/src/plugins/controls/common/index.ts @@ -16,3 +16,5 @@ export { OPTIONS_LIST_CONTROL } from './control_types/options_list/types'; export { RANGE_SLIDER_CONTROL } from './control_types/range_slider/types'; export { getDefaultControlGroupInput } from './control_group/control_group_constants'; + +export { TIME_SLIDER_CONTROL } from './control_types/time_slider/types'; diff --git a/src/plugins/controls/jest.config.js b/src/plugins/controls/jest.config.js new file mode 100644 index 0000000000000..bf024134ef60c --- /dev/null +++ b/src/plugins/controls/jest.config.js @@ -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 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: ['/src/plugins/controls'], + testRunner: 'jasmine2', + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/controls', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/controls/{common,public,server}/**/*.{ts,tsx}'], + setupFiles: ['/src/plugins/controls/jest_setup.ts'], +}; diff --git a/src/plugins/controls/jest_setup.ts b/src/plugins/controls/jest_setup.ts new file mode 100644 index 0000000000000..bef43fb98d3f0 --- /dev/null +++ b/src/plugins/controls/jest_setup.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +// Start the services with stubs +import { pluginServices } from './public/services'; +import { registry } from './public/services/stub'; + +registry.start({}); +pluginServices.setRegistry(registry); diff --git a/src/plugins/controls/public/__stories__/controls.stories.tsx b/src/plugins/controls/public/__stories__/controls.stories.tsx index 12bf0cacbe136..12e595f483e04 100644 --- a/src/plugins/controls/public/__stories__/controls.stories.tsx +++ b/src/plugins/controls/public/__stories__/controls.stories.tsx @@ -63,7 +63,7 @@ const storybookStubOptionsListRequest = async ( ); replaceOptionsListMethod(storybookStubOptionsListRequest); -const ControlGroupStoryComponent: FC<{ +export const ControlGroupStoryComponent: FC<{ panels?: ControlsPanels; edit?: boolean; }> = ({ panels, edit }) => { @@ -161,7 +161,7 @@ export const ConfiguredControlGroupStory = () => ( } as OptionsListEmbeddableInput, }, optionsList3: { - type: OPTIONS_LIST_CONTROL, + type: 'TIME_SLIDER', order: 3, width: 'auto', explicitInput: { diff --git a/src/plugins/controls/public/__stories__/storybook_control_factories.ts b/src/plugins/controls/public/__stories__/storybook_control_factories.ts index 12674a97d856d..f0c0611ad45fe 100644 --- a/src/plugins/controls/public/__stories__/storybook_control_factories.ts +++ b/src/plugins/controls/public/__stories__/storybook_control_factories.ts @@ -8,6 +8,7 @@ import { OptionsListEmbeddableFactory } from '../control_types/options_list'; import { RangeSliderEmbeddableFactory } from '../control_types/range_slider'; +import { TimesliderEmbeddableFactory } from '../control_types/time_slider'; import { ControlsService } from '../services/controls'; import { ControlFactory } from '..'; @@ -25,4 +26,9 @@ export const populateStorybookControlFactories = (controlsServiceStub: ControlsS const rangeSliderControlFactory = rangeSliderFactoryStub as unknown as ControlFactory; rangeSliderControlFactory.getDefaultInput = () => ({}); controlsServiceStub.registerControlType(rangeSliderControlFactory); + + const timesliderFactoryStub = new TimesliderEmbeddableFactory(); + const timeSliderControlFactory = timesliderFactoryStub as unknown as ControlFactory; + timeSliderControlFactory.getDefaultInput = () => ({}); + controlsServiceStub.registerControlType(timeSliderControlFactory); }; diff --git a/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx b/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx new file mode 100644 index 0000000000000..7ae7871497045 --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx @@ -0,0 +1,190 @@ +/* + * 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, { FC, useCallback, useState } from 'react'; +import moment from 'moment'; +import { EuiFormControlLayout } from '@elastic/eui'; + +import { TimeSliderProps, TimeSlider } from '../time_slider.component'; + +export default { + title: 'Time Slider', + description: '', +}; + +const TimeSliderWrapper: FC> = (props) => { + const [value, setValue] = useState(props.value); + const onChange = useCallback( + (newValue: [number | null, number | null]) => { + const lowValue = newValue[0]; + const highValue = newValue[1]; + + setValue([lowValue, highValue]); + }, + [setValue] + ); + + return ( +
+ + + +
+ ); +}; + +const undefinedValue: [null, null] = [null, null]; +const undefinedRange: [undefined, undefined] = [undefined, undefined]; + +export const TimeSliderNoValuesOrRange = () => { + // If range is undefined, that should be inndicate that we are loading the range + return ; +}; + +export const TimeSliderUndefinedRangeNoValue = () => { + // If a range is [undefined, undefined] then it was loaded, but no values were found. + return ; +}; + +export const TimeSliderUndefinedRangeWithValue = () => { + const lastWeek = moment().subtract(7, 'days'); + const now = moment(); + + return ( + + ); +}; + +export const TimeSliderWithRangeAndNoValue = () => { + const lastWeek = moment().subtract(7, 'days'); + const now = moment(); + + return ( + + ); +}; + +export const TimeSliderWithRangeAndLowerValue = () => { + const lastWeek = moment().subtract(7, 'days'); + const now = moment(); + + const threeDays = moment().subtract(3, 'days'); + + return ( + + ); +}; + +export const TimeSliderWithRangeAndUpperValue = () => { + const lastWeek = moment().subtract(7, 'days'); + const now = moment(); + + const threeDays = moment().subtract(3, 'days'); + + return ( + + ); +}; + +export const TimeSliderWithLowRangeOverlap = () => { + const lastWeek = moment().subtract(7, 'days'); + const now = moment(); + + const threeDays = moment().subtract(3, 'days'); + const twoDays = moment().subtract(2, 'days'); + + return ( + + ); +}; + +export const TimeSliderWithLowRangeOverlapAndIgnoredValidation = () => { + const lastWeek = moment().subtract(7, 'days'); + const now = moment(); + + const threeDays = moment().subtract(3, 'days'); + const twoDays = moment().subtract(2, 'days'); + + return ( + + ); +}; + +export const TimeSliderWithRangeLowerThanValue = () => { + const twoWeeksAgo = moment().subtract(14, 'days'); + const lastWeek = moment().subtract(7, 'days'); + + const now = moment(); + const threeDays = moment().subtract(3, 'days'); + + return ( + + ); +}; + +export const TimeSliderWithRangeHigherThanValue = () => { + const twoWeeksAgo = moment().subtract(14, 'days'); + const lastWeek = moment().subtract(7, 'days'); + + const now = moment(); + const threeDays = moment().subtract(3, 'days'); + + return ( + + ); +}; + +export const PartialValueLowerThanRange = () => { + // Selected value is March 8 -> March 9 + // Range is March 11 -> 25 + const eightDaysAgo = moment().subtract(8, 'days'); + + const lastWeek = moment().subtract(7, 'days'); + const today = moment(); + + return ( + + ); +}; + +export const PartialValueHigherThanRange = () => { + // Selected value is March 8 -> March 9 + // Range is March 11 -> 25 + const eightDaysAgo = moment().subtract(8, 'days'); + + const lastWeek = moment().subtract(7, 'days'); + const today = moment(); + + return ( + + ); +}; diff --git a/src/plugins/controls/public/control_types/time_slider/index.ts b/src/plugins/controls/public/control_types/time_slider/index.ts new file mode 100644 index 0000000000000..1cd5900164676 --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { TimesliderEmbeddableFactory } from './time_slider_embeddable_factory'; +export { type TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types'; +export {} from '../../../common'; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.component.scss b/src/plugins/controls/public/control_types/time_slider/time_slider.component.scss new file mode 100644 index 0000000000000..3f8a37ec44d37 --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/time_slider.component.scss @@ -0,0 +1,47 @@ +.timeSlider__anchorOverride { + display:block; + >div { + height: 100%; + } +} + +.timeSlider__popoverOverride { + width: 100%; + max-width: 100%; + height: 100%; +} + +.timeSlider__panelOverride { + min-width: $euiSizeXXL * 15; +} + +.timeSlider__anchor { + text-decoration: none; + width: 100%; + background-color: $euiFormBackgroundColor; + box-shadow: none; + @include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true); + overflow: hidden; + height: 100%; + + &:enabled:focus { + background-color: $euiFormBackgroundColor; + } + + .euiText { + background-color: $euiFormBackgroundColor; + } + + .timeSlider__anchorText { + font-weight: $euiFontWeightBold; + } + + .timeSlider__anchorText--default { + color: $euiColorMediumShade; + } + + .timeSlider__anchorText--invalid { + text-decoration: line-through; + color: $euiColorMediumShade; + } +} \ No newline at end of file diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx new file mode 100644 index 0000000000000..9ce7cf825e863 --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx @@ -0,0 +1,336 @@ +/* + * 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, { FC, useState, useMemo, useCallback } from 'react'; +import { isNil } from 'lodash'; +import { + EuiText, + EuiLoadingSpinner, + EuiInputPopover, + EuiPopoverTitle, + EuiSpacer, + EuiFlexItem, + EuiFlexGroup, + EuiToolTip, + EuiButtonIcon, +} from '@elastic/eui'; +import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks'; +import moment from 'moment-timezone'; +import { calcAutoIntervalNear } from '../../../../data/common'; +import { ValidatedDualRange } from '../../../../kibana_react/public'; +import { TimeSliderStrings } from './time_slider_strings'; +import './time_slider.component.scss'; + +function getScaledDateFormat(interval: number): string { + if (interval >= moment.duration(1, 'y').asMilliseconds()) { + return 'YYYY'; + } + + if (interval >= moment.duration(1, 'd').asMilliseconds()) { + return 'MMM D'; + } + + if (interval >= moment.duration(6, 'h').asMilliseconds()) { + return 'Do HH'; + } + + if (interval >= moment.duration(1, 'h').asMilliseconds()) { + return 'HH:mm'; + } + + if (interval >= moment.duration(1, 'm').asMilliseconds()) { + return 'HH:mm'; + } + + if (interval >= moment.duration(1, 's').asMilliseconds()) { + return 'mm:ss'; + } + + return 'ss.SSS'; +} + +export function getInterval(min: number, max: number, steps = 6): number { + const duration = max - min; + let interval = calcAutoIntervalNear(steps, duration).asMilliseconds(); + // Sometimes auto interval is not quite right and returns 2X or 3X requested ticks + // Adjust the interval to get closer to the requested number of ticks + const actualSteps = duration / interval; + if (actualSteps > steps * 1.5) { + const factor = Math.round(actualSteps / steps); + interval *= factor; + } else if (actualSteps < 5) { + interval *= 0.5; + } + return interval; +} + +export interface TimeSliderProps { + range?: [number | undefined, number | undefined]; + value: [number | null, number | null]; + onChange: (range: [number | null, number | null]) => void; + dateFormat?: string; + timezone?: string; + fieldName: string; + ignoreValidation?: boolean; +} + +const isValidRange = (maybeRange: TimeSliderProps['range']): maybeRange is [number, number] => { + return maybeRange !== undefined && !isNil(maybeRange[0]) && !isNil(maybeRange[1]); +}; + +const unselectedClass = 'timeSlider__anchorText--default'; +const validClass = 'timeSlider__anchorText'; +const invalidClass = 'timeSlider__anchorText--invalid'; + +export const TimeSlider: FC = (props) => { + const defaultProps = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', + ignoreValidation: false, + timezone: 'Browser', + ...props, + }; + const { range, value, timezone, dateFormat, fieldName, ignoreValidation } = defaultProps; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const togglePopover = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [isPopoverOpen, setIsPopoverOpen]); + + const getTimezone = useCallback(() => { + const detectedTimezone = moment.tz.guess(); + + return timezone === 'Browser' ? detectedTimezone : timezone; + }, [timezone]); + + const epochToKbnDateFormat = useCallback( + (epoch: number) => { + const tz = getTimezone(); + return moment.tz(epoch, tz).format(dateFormat); + }, + [dateFormat, getTimezone] + ); + + // If we don't have a range or we have is loading, show the loading state + const hasRange = range !== undefined; + + // We have values if we have a range or value entry for both position + const hasValues = + (value[0] !== null || (hasRange && range[0] !== undefined)) && + (value[1] !== null || (hasRange && range[1] !== undefined)); + + let valueText: JSX.Element | null = null; + if (hasValues) { + let lower = value[0] !== null ? value[0] : range![0]!; + let upper = value[1] !== null ? value[1] : range![1]!; + + if (value[0] !== null && lower > upper) { + upper = lower; + } else if (value[1] !== null && lower > upper) { + lower = upper; + } + + const hasLowerValueInRange = + value[0] !== null && isValidRange(range) && value[0] >= range[0] && value[0] <= range[1]; + // It's out of range if the upper value is above the upper range or below the lower range + const hasUpperValueInRange = + value[1] !== null && isValidRange(range) && value[1] <= range[1] && value[1] >= range[0]; + + let lowClass = unselectedClass; + let highClass = unselectedClass; + if (value[0] !== null && (hasLowerValueInRange || ignoreValidation)) { + lowClass = validClass; + } else if (value[0] !== null) { + lowClass = invalidClass; + } + + if (value[1] !== null && (hasUpperValueInRange || ignoreValidation)) { + highClass = validClass; + } else if (value[1] !== null) { + highClass = invalidClass; + } + + // if no value then anchorText default + // if hasLowerValueInRange || skipValidation then anchor text + // else strikethrough + + valueText = ( + + {epochToKbnDateFormat(lower)} +   →   + {epochToKbnDateFormat(upper)} + + ); + } + + const button = ( + + ); + + return ( + setIsPopoverOpen(false)} + panelPaddingSize="s" + anchorPosition="downCenter" + disableFocusTrap + repositionOnScroll + > + {isValidRange(range) ? ( + + ) : ( + + )} + + ); +}; + +const TimeSliderComponentPopoverNoDocuments: FC = () => { + return {TimeSliderStrings.noDocumentsPopover.getLabel()}; +}; + +export const TimeSliderComponentPopover: FC< + TimeSliderProps & { + range: [number, number]; + getTimezone: () => string; + epochToKbnDateFormat: (epoch: number) => string; + } +> = ({ range, value, onChange, getTimezone, epochToKbnDateFormat, fieldName }) => { + const [lowerBound, upperBound] = range; + let [lowerValue, upperValue] = value; + + if (lowerValue === null) { + lowerValue = lowerBound; + } + + if (upperValue === null) { + upperValue = upperBound; + } + + const fullRange = useMemo( + () => [Math.min(lowerValue!, lowerBound), Math.max(upperValue!, upperBound)], + [lowerValue, lowerBound, upperValue, upperBound] + ); + + const getTicks = useCallback( + (min: number, max: number, interval: number): EuiRangeTick[] => { + const format = getScaledDateFormat(interval); + const tz = getTimezone(); + + let tick = Math.ceil(min / interval) * interval; + const ticks: EuiRangeTick[] = []; + while (tick < max) { + ticks.push({ + value: tick, + label: moment.tz(tick, tz).format(format), + }); + tick += interval; + } + + return ticks; + }, + [getTimezone] + ); + + const ticks = useMemo(() => { + const interval = getInterval(fullRange[0], fullRange[1]); + return getTicks(fullRange[0], fullRange[1], interval); + }, [fullRange, getTicks]); + + const onChangeHandler = useCallback( + ([_min, _max]: [number | string, number | string]) => { + // If a value is undefined and the number that is given here matches the range bounds + // then we will ignore it, becuase they probably didn't actually select that value + const report: [number | null, number | null] = [null, null]; + + let min: number; + let max: number; + if (typeof _min === 'string') { + min = parseFloat(_min); + min = isNaN(min) ? range[0] : min; + } else { + min = _min; + } + + if (typeof _max === 'string') { + max = parseFloat(_max); + max = isNaN(max) ? range[0] : max; + } else { + max = _max; + } + + if (value[0] !== null || min !== range[0]) { + report[0] = min; + } + if (value[1] !== null || max !== range[1]) { + report[1] = max; + } + + onChange(report); + }, + [onChange, value, range] + ); + + const levels = [{ min: range[0], max: range[1], color: 'success' }]; + + return ( + <> + {fieldName} + + {epochToKbnDateFormat(lowerValue)} - {epochToKbnDateFormat(upperValue)} + + + + + + + + + onChange([null, null])} + aria-label={TimeSliderStrings.resetButton.getLabel()} + data-test-subj="timeSlider__clearRangeButton" + /> + + + + + + ); +}; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider.tsx new file mode 100644 index 0000000000000..d2198416fdc42 --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/time_slider.tsx @@ -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 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, { FC, useCallback, useState, useMemo } from 'react'; +import { BehaviorSubject } from 'rxjs'; +import { debounce } from 'lodash'; +import { useStateObservable } from '../../hooks/use_state_observable'; +import { useReduxEmbeddableContext } from '../../../../presentation_util/public'; +import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types'; +import { timeSliderReducers } from './time_slider_reducers'; +import { TimeSlider as Component } from './time_slider.component'; + +export interface TimeSliderSubjectState { + range?: { + min?: number; + max?: number; + }; + loading: boolean; +} + +interface TimeSliderProps { + componentStateSubject: BehaviorSubject; + dateFormat: string; + timezone: string; + fieldName: string; + ignoreValidation: boolean; +} + +export const TimeSlider: FC = ({ + componentStateSubject, + dateFormat, + timezone, + fieldName, + ignoreValidation, +}) => { + const { + useEmbeddableDispatch, + useEmbeddableSelector, + actions: { selectRange }, + } = useReduxEmbeddableContext(); + const dispatch = useEmbeddableDispatch(); + + const { range: availableRange } = useStateObservable( + componentStateSubject, + componentStateSubject.getValue() + ); + + const { min, max } = availableRange + ? availableRange + : ({} as { + min?: number; + max?: number; + }); + + const { value } = useEmbeddableSelector((state) => state); + + const [selectedValue, setSelectedValue] = useState<[number | null, number | null]>( + value || [null, null] + ); + + const dispatchChange = useCallback( + (range: [number | null, number | null]) => { + dispatch(selectRange(range)); + }, + [dispatch, selectRange] + ); + + const debouncedDispatchChange = useMemo(() => debounce(dispatchChange, 500), [dispatchChange]); + + const onChangeComplete = useCallback( + (range: [number | null, number | null]) => { + debouncedDispatchChange(range); + setSelectedValue(range); + }, + [setSelectedValue, debouncedDispatchChange] + ); + + return ( + + ); +}; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_editor.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider_editor.tsx new file mode 100644 index 0000000000000..8e5f107df8201 --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/time_slider_editor.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import useMount from 'react-use/lib/useMount'; +import React, { useEffect, useState } from 'react'; +import { EuiFormRow } from '@elastic/eui'; + +import { pluginServices } from '../../services'; +import { ControlEditorProps } from '../../types'; +import { DataViewListItem, DataView } from '../../../../data_views/common'; +import { + LazyDataViewPicker, + LazyFieldPicker, + withSuspense, +} from '../../../../presentation_util/public'; +import { TimeSliderStrings } from './time_slider_strings'; + +interface TimeSliderEditorState { + dataViewListItems: DataViewListItem[]; + dataView?: DataView; + fieldName?: string; +} + +const FieldPicker = withSuspense(LazyFieldPicker, null); +const DataViewPicker = withSuspense(LazyDataViewPicker, null); + +export const TimeSliderEditor = ({ + onChange, + initialInput, + setValidState, + setDefaultTitle, +}: ControlEditorProps) => { + // Controls Services Context + const { dataViews } = pluginServices.getHooks(); + const { getIdsWithTitle, getDefaultId, get } = dataViews.useService(); + + const [state, setState] = useState({ + fieldName: initialInput?.fieldName, + dataViewListItems: [], + }); + + useMount(() => { + let mounted = true; + if (state.fieldName) setDefaultTitle(state.fieldName); + (async () => { + const dataViewListItems = await getIdsWithTitle(); + const initialId = initialInput?.dataViewId ?? (await getDefaultId()); + let dataView: DataView | undefined; + if (initialId) { + onChange({ dataViewId: initialId }); + dataView = await get(initialId); + } + if (!mounted) return; + setState((s) => ({ ...s, dataView, dataViewListItems })); + })(); + return () => { + mounted = false; + }; + }); + + useEffect( + () => setValidState(Boolean(state.fieldName) && Boolean(state.dataView)), + [state.fieldName, setValidState, state.dataView] + ); + + const { dataView, fieldName } = state; + return ( + <> + + { + onChange({ dataViewId }); + get(dataViewId).then((newDataView) => + setState((s) => ({ ...s, dataView: newDataView })) + ); + }} + trigger={{ + label: state.dataView?.title ?? TimeSliderStrings.editor.getNoDataViewTitle(), + }} + /> + + + field.type === 'date'} + selectedFieldName={fieldName} + dataView={dataView} + onSelectField={(field) => { + setDefaultTitle(field.displayName ?? field.name); + onChange({ fieldName: field.name }); + setState((s) => ({ ...s, fieldName: field.name })); + }} + /> + + + ); +}; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.test.ts b/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.test.ts new file mode 100644 index 0000000000000..c4a42e6b03e2c --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.test.ts @@ -0,0 +1,287 @@ +/* + * 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 { of } from 'rxjs'; +import { delay, map } from 'rxjs/operators'; +import { TimeSliderControlEmbeddableInput } from '.'; +import { TimeSliderControlEmbeddable } from './time_slider_embeddable'; +import { stubLogstashDataView } from '../../../../data_views/common/data_view.stub'; +import { pluginServices } from '../../services'; +import { TestScheduler } from 'rxjs/testing'; +import { buildRangeFilter } from '@kbn/es-query'; + +const buildFilter = (range: [number | null, number | null]) => { + const filterPieces: Record = {}; + if (range[0] !== null) { + filterPieces.gte = range[0]; + } + if (range[1] !== null) { + filterPieces.lte = range[1]; + } + + const filter = buildRangeFilter( + stubLogstashDataView.getFieldByName('bytes')!, + filterPieces, + stubLogstashDataView + ); + filter.meta.key = 'bytes'; + + return filter; +}; + +const rangeMin = 20; +const rangeMax = 30; +const range = { min: rangeMin, max: rangeMax }; + +const lowerValue: [number, number] = [15, 25]; +const upperValue: [number, number] = [25, 35]; +const partialLowValue: [number, null] = [25, null]; +const partialHighValue: [null, number] = [null, 25]; +const withinRangeValue: [number, number] = [21, 29]; +const outOfRangeValue: [number, number] = [31, 40]; + +const rangeFilter = buildFilter([rangeMin, rangeMax]); +const lowerValueFilter = buildFilter(lowerValue); +const lowerValuePartialFilter = buildFilter([20, 25]); +const upperValueFilter = buildFilter(upperValue); +const upperValuePartialFilter = buildFilter([25, 30]); + +const partialLowValueFilter = buildFilter(partialLowValue); +const partialHighValueFilter = buildFilter(partialHighValue); +const withinRangeValueFilter = buildFilter(withinRangeValue); +const outOfRangeValueFilter = buildFilter(outOfRangeValue); + +const baseInput: TimeSliderControlEmbeddableInput = { + id: 'id', + fieldName: 'bytes', + dataViewId: stubLogstashDataView.id!, +}; + +describe('Time Slider Control Embeddable', () => { + const services = pluginServices.getServices(); + const fetchRange = jest.spyOn(services.data, 'fetchFieldRange'); + const getDataView = jest.spyOn(services.data, 'getDataView'); + const fetchRange$ = jest.spyOn(services.data, 'fetchFieldRange$'); + const getDataView$ = jest.spyOn(services.data, 'getDataView$'); + + beforeEach(() => { + jest.resetAllMocks(); + + fetchRange.mockResolvedValue(range); + fetchRange$.mockReturnValue(of(range).pipe(delay(100))); + getDataView.mockResolvedValue(stubLogstashDataView); + getDataView$.mockReturnValue(of(stubLogstashDataView)); + }); + + describe('outputting filters', () => { + let testScheduler: TestScheduler; + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + }); + + const testFilterOutput = ( + input: any, + expectedFilterAfterRangeFetch: any, + mockRange: { min?: number; max?: number } = range + ) => { + testScheduler.run(({ expectObservable, cold }) => { + fetchRange$.mockReturnValue(cold('--b', { b: mockRange })); + const expectedMarbles = 'a-b'; + const expectedValues = { + a: undefined, + b: expectedFilterAfterRangeFetch ? [expectedFilterAfterRangeFetch] : undefined, + }; + + const embeddable = new TimeSliderControlEmbeddable(input, {}); + const source$ = embeddable.getOutput$().pipe(map((o) => o.filters)); + + expectObservable(source$).toBe(expectedMarbles, expectedValues); + }); + }; + + it('outputs no filter when no value is given', () => { + testFilterOutput(baseInput, undefined); + }); + + it('outputs the value filter after the range is fetched', () => { + testFilterOutput({ ...baseInput, value: withinRangeValue }, withinRangeValueFilter); + }); + + it('outputs a partial filter for a low partial value', () => { + testFilterOutput({ ...baseInput, value: partialLowValue }, partialLowValueFilter); + }); + + it('outputs a partial filter for a high partial value', () => { + testFilterOutput({ ...baseInput, value: partialHighValue }, partialHighValueFilter); + }); + + describe('with validation', () => { + it('outputs a partial value filter if value is below range', () => { + testFilterOutput({ ...baseInput, value: lowerValue }, lowerValuePartialFilter); + }); + + it('outputs a partial value filter if value is above range', () => { + testFilterOutput({ ...baseInput, value: upperValue }, upperValuePartialFilter); + }); + + it('outputs range filter value if value is completely out of range', () => { + testFilterOutput({ ...baseInput, value: outOfRangeValue }, rangeFilter); + }); + + it('outputs no filter when no range available', () => { + testFilterOutput({ ...baseInput, value: withinRangeValue }, undefined, {}); + }); + }); + + describe('with validation off', () => { + it('outputs the lower value filter', () => { + testFilterOutput( + { ...baseInput, ignoreParentSettings: { ignoreValidations: true }, value: lowerValue }, + lowerValueFilter + ); + }); + + it('outputs the uppwer value filter', () => { + testFilterOutput( + { ...baseInput, ignoreParentSettings: { ignoreValidations: true }, value: upperValue }, + upperValueFilter + ); + }); + + it('outputs the out of range filter', () => { + testFilterOutput( + { + ...baseInput, + ignoreParentSettings: { ignoreValidations: true }, + value: outOfRangeValue, + }, + outOfRangeValueFilter + ); + }); + + it('outputs the value filter when no range found', () => { + testFilterOutput( + { + ...baseInput, + ignoreParentSettings: { ignoreValidations: true }, + value: withinRangeValue, + }, + withinRangeValueFilter, + { min: undefined, max: undefined } + ); + }); + }); + }); + + describe('fetching range', () => { + it('fetches range on init', () => { + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + testScheduler.run(({ cold, expectObservable }) => { + const mockRange = { min: 1, max: 2 }; + fetchRange$.mockReturnValue(cold('--b', { b: mockRange })); + + const expectedMarbles = 'a-b'; + const expectedValues = { + a: undefined, + b: mockRange, + }; + + const embeddable = new TimeSliderControlEmbeddable(baseInput, {}); + const source$ = embeddable.getComponentState$().pipe(map((state) => state.range)); + + const { fieldName, ...inputForFetch } = baseInput; + + expectObservable(source$).toBe(expectedMarbles, expectedValues); + expect(fetchRange$).toBeCalledWith(stubLogstashDataView, fieldName, { + ...inputForFetch, + filters: undefined, + query: undefined, + timeRange: undefined, + viewMode: 'edit', + }); + }); + }); + + it('fetches range on input change', () => { + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + testScheduler.run(({ cold, expectObservable, flush }) => { + const mockRange = { min: 1, max: 2 }; + fetchRange$.mockReturnValue(cold('a', { a: mockRange })); + + const embeddable = new TimeSliderControlEmbeddable(baseInput, {}); + const updatedInput = { ...baseInput, fieldName: '@timestamp' }; + + embeddable.updateInput(updatedInput); + + expect(fetchRange$).toBeCalledTimes(2); + expect(fetchRange$.mock.calls[1][1]).toBe(updatedInput.fieldName); + }); + }); + + it('passes input to fetch range to build the query', () => { + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + testScheduler.run(({ cold, expectObservable, flush }) => { + const mockRange = { min: 1, max: 2 }; + fetchRange$.mockReturnValue(cold('a', { a: mockRange })); + + const input = { + ...baseInput, + query: {} as any, + filters: {} as any, + timeRange: {} as any, + }; + + new TimeSliderControlEmbeddable(input, {}); + + expect(fetchRange$).toBeCalledTimes(1); + const args = fetchRange$.mock.calls[0][2]; + expect(args.query).toBe(input.query); + expect(args.filters).toBe(input.filters); + expect(args.timeRange).toBe(input.timeRange); + }); + }); + + it('does not pass ignored parent settings', () => { + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + testScheduler.run(({ cold, expectObservable, flush }) => { + const mockRange = { min: 1, max: 2 }; + fetchRange$.mockReturnValue(cold('a', { a: mockRange })); + + const input = { + ...baseInput, + query: '' as any, + filters: {} as any, + timeRange: {} as any, + ignoreParentSettings: { ignoreFilters: true, ignoreQuery: true, ignoreTimerange: true }, + }; + + new TimeSliderControlEmbeddable(input, {}); + + expect(fetchRange$).toBeCalledTimes(1); + const args = fetchRange$.mock.calls[0][2]; + expect(args.query).not.toBe(input.query); + expect(args.filters).not.toBe(input.filters); + expect(args.timeRange).not.toBe(input.timeRange); + }); + }); + }); +}); diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.tsx new file mode 100644 index 0000000000000..d6507c10b2d6f --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable.tsx @@ -0,0 +1,321 @@ +/* + * 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 { compareFilters, buildRangeFilter, RangeFilterParams } from '@kbn/es-query'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { isEqual } from 'lodash'; +import deepEqual from 'fast-deep-equal'; +import { merge, Subscription, BehaviorSubject, Observable } from 'rxjs'; +import { map, distinctUntilChanged, skip, take, mergeMap } from 'rxjs/operators'; + +import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types'; + +import { + withSuspense, + LazyReduxEmbeddableWrapper, + ReduxEmbeddableWrapperPropsWithChildren, +} from '../../../../presentation_util/public'; + +import { TIME_SLIDER_CONTROL } from '../../'; +import { ControlsSettingsService } from '../../services/settings'; +import { Embeddable, IContainer } from '../../../../embeddable/public'; +import { ControlsDataService } from '../../services/data'; +import { DataView } from '../../../../data_views/public'; +import { ControlOutput } from '../..'; +import { pluginServices } from '../../services'; + +import { TimeSlider as TimeSliderComponent, TimeSliderSubjectState } from './time_slider'; +import { timeSliderReducers } from './time_slider_reducers'; + +const TimeSliderControlReduxWrapper = withSuspense< + ReduxEmbeddableWrapperPropsWithChildren +>(LazyReduxEmbeddableWrapper); + +const diffDataFetchProps = (current?: any, last?: any) => { + if (!current || !last) return false; + const { filters: currentFilters, ...currentWithoutFilters } = current; + const { filters: lastFilters, ...lastWithoutFilters } = last; + if (!deepEqual(currentWithoutFilters, lastWithoutFilters)) return false; + if (!compareFilters(lastFilters ?? [], currentFilters ?? [])) return false; + return true; +}; + +export class TimeSliderControlEmbeddable extends Embeddable< + TimeSliderControlEmbeddableInput, + ControlOutput +> { + public readonly type = TIME_SLIDER_CONTROL; + public deferEmbeddableLoad = true; + + private subscriptions: Subscription = new Subscription(); + private node?: HTMLElement; + + // Internal data fetching state for this input control. + private dataView?: DataView; + + private componentState: TimeSliderSubjectState; + private componentStateSubject$ = new BehaviorSubject({ + range: undefined, + loading: false, + }); + + // Internal state subject will let us batch updates to the externally accessible state subject + private internalComponentStateSubject$ = new BehaviorSubject({ + range: undefined, + loading: false, + }); + + private internalOutput: ControlOutput; + + private fetchRange$: ControlsDataService['fetchFieldRange$']; + private getDataView$: ControlsDataService['getDataView$']; + private getDateFormat: ControlsSettingsService['getDateFormat']; + private getTimezone: ControlsSettingsService['getTimezone']; + + constructor(input: TimeSliderControlEmbeddableInput, output: ControlOutput, parent?: IContainer) { + super(input, output, parent); // get filters for initial output... + + const { + data: { fetchFieldRange$, getDataView$ }, + settings: { getDateFormat, getTimezone }, + } = pluginServices.getServices(); + this.fetchRange$ = fetchFieldRange$; + this.getDataView$ = getDataView$; + this.getDateFormat = getDateFormat; + this.getTimezone = getTimezone; + + this.componentState = { loading: true }; + this.updateComponentState(this.componentState, true); + + this.internalOutput = {}; + + this.initialize(); + } + + private initialize() { + // If value is undefined, then we can be finished with initialization because we're not going to output a filter + if (this.getInput().value === undefined) { + this.setInitializationFinished(); + } + + this.setupSubscriptions(); + } + + private setupSubscriptions() { + // We need to fetch data when any of these values change + const dataFetchPipe = this.getInput$().pipe( + map((newInput) => ({ + lastReloadRequestTime: newInput.lastReloadRequestTime, + dataViewId: newInput.dataViewId, + fieldName: newInput.fieldName, + timeRange: newInput.timeRange, + filters: newInput.filters, + query: newInput.query, + })), + distinctUntilChanged(diffDataFetchProps) + ); + + // When data fetch pipe emits, we start the fetch + this.subscriptions.add(dataFetchPipe.subscribe(this.fetchAvailableTimerange)); + + const availableRangePipe = this.internalComponentStateSubject$.pipe( + map((state) => (state.range ? { min: state.range.min, max: state.range.max } : {})), + distinctUntilChanged((a, b) => isEqual(a, b)) + ); + + this.subscriptions.add( + merge( + this.getInput$().pipe( + skip(1), // Skip the first input value + distinctUntilChanged((a, b) => isEqual(a.value, b.value)) + ), + availableRangePipe.pipe(skip(1)) + ).subscribe(() => { + this.setInitializationFinished(); + this.buildFilter(); + + this.componentStateSubject$.next(this.componentState); + }) + ); + } + + private buildFilter = () => { + const { fieldName, value, ignoreParentSettings } = this.getInput(); + + const min = value ? value[0] : null; + const max = value ? value[1] : null; + const hasRange = + this.componentState.range!.max !== undefined && this.componentState.range!.min !== undefined; + + this.getCurrentDataView$().subscribe((dataView) => { + const range: RangeFilterParams = {}; + let filterMin: number | undefined; + let filterMax: number | undefined; + const field = dataView.getFieldByName(fieldName); + + if (ignoreParentSettings?.ignoreValidations) { + if (min !== null) { + range.gte = min; + } + + if (max !== null) { + range.lte = max; + } + } else { + // If we have a value or a range use the min/max of those, otherwise undefined + if (min !== null && this.componentState.range!.min !== undefined) { + filterMin = Math.max(min || 0, this.componentState.range!.min || 0); + } + + if (max !== null && this.componentState.range!.max) { + filterMax = Math.min( + max || Number.MAX_SAFE_INTEGER, + this.componentState.range!.max || Number.MAX_SAFE_INTEGER + ); + } + + // Last check, if the value is completely outside the range then we will just default to the range + if ( + hasRange && + ((min !== null && min > this.componentState.range!.max!) || + (max !== null && max < this.componentState.range!.min!)) + ) { + filterMin = this.componentState.range!.min; + filterMax = this.componentState.range!.max; + } + + if (hasRange && filterMin !== undefined) { + range.gte = filterMin; + } + if (hasRange && filterMax !== undefined) { + range.lte = filterMax; + } + } + + if (range.lte !== undefined || range.gte !== undefined) { + const rangeFilter = buildRangeFilter(field!, range, dataView); + rangeFilter.meta.key = field?.name; + + this.updateInternalOutput({ filters: [rangeFilter] }, true); + this.updateComponentState({ loading: false }); + } else { + this.updateInternalOutput({ filters: undefined, dataViews: [dataView] }, true); + this.updateComponentState({ loading: false }); + } + }); + }; + + private updateComponentState(changes: Partial, publish = false) { + this.componentState = { + ...this.componentState, + ...changes, + }; + + this.internalComponentStateSubject$.next(this.componentState); + + if (publish) { + this.componentStateSubject$.next(this.componentState); + } + } + + private updateInternalOutput(changes: Partial, publish = false) { + this.internalOutput = { + ...this.internalOutput, + ...changes, + }; + + if (publish) { + this.updateOutput(this.internalOutput); + } + } + + private getCurrentDataView$ = () => { + const { dataViewId } = this.getInput(); + if (this.dataView && this.dataView.id === dataViewId) + return new Observable((subscriber) => { + subscriber.next(this.dataView); + subscriber.complete(); + }); + + return this.getDataView$(dataViewId); + }; + + private fetchAvailableTimerange = () => { + this.updateComponentState({ loading: true }, true); + this.updateInternalOutput({ loading: true }, true); + + const { fieldName, ignoreParentSettings, query, filters, timeRange, ...input } = + this.getInput(); + + const inputForFetch = { + ...input, + ...(ignoreParentSettings?.ignoreQuery ? {} : { query }), + ...(ignoreParentSettings?.ignoreFilters ? {} : { filters }), + ...(ignoreParentSettings?.ignoreTimerange ? {} : { timeRange }), + }; + + try { + this.getCurrentDataView$() + .pipe( + mergeMap((dataView) => this.fetchRange$(dataView, fieldName, inputForFetch)), + take(1) + ) + .subscribe(({ min, max }) => { + this.updateInternalOutput({ loading: false }); + this.updateComponentState({ + range: { + min: min === null ? undefined : min, + max: max === null ? undefined : max, + }, + loading: false, + }); + }); + } catch (e) { + this.updateComponentState({ loading: false }, true); + this.updateInternalOutput({ loading: false }, true); + } + }; + + public getComponentState$ = () => { + return this.componentStateSubject$; + }; + + public destroy = () => { + super.destroy(); + this.subscriptions.unsubscribe(); + }; + + public reload = () => { + this.fetchAvailableTimerange(); + }; + + public render = (node: HTMLElement) => { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + + ReactDOM.render( + + + , + node + ); + }; +} diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable_factory.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable_factory.tsx new file mode 100644 index 0000000000000..d1bd1508a45cb --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/time_slider_embeddable_factory.tsx @@ -0,0 +1,60 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; + +import { TIME_SLIDER_CONTROL } from '../../'; +import { ControlEmbeddable, IEditableControlFactory } from '../../types'; +import { EmbeddableFactoryDefinition, IContainer } from '../../../../embeddable/public'; +import { + createOptionsListExtract, + createOptionsListInject, +} from '../../../common/control_types/options_list/options_list_persistable_state'; +import { TimeSliderEditor } from './time_slider_editor'; +import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types'; +import { TimeSliderStrings } from './time_slider_strings'; + +export class TimesliderEmbeddableFactory + implements EmbeddableFactoryDefinition, IEditableControlFactory +{ + public type = TIME_SLIDER_CONTROL; + public canCreateNew = () => false; + + constructor() {} + + public async create(initialInput: TimeSliderControlEmbeddableInput, parent?: IContainer) { + const { TimeSliderControlEmbeddable } = await import('./time_slider_embeddable'); + + return Promise.resolve(new TimeSliderControlEmbeddable(initialInput, {}, parent)); + } + + public presaveTransformFunction = ( + newInput: Partial, + embeddable?: ControlEmbeddable + ) => { + if ( + embeddable && + (!deepEqual(newInput.fieldName, embeddable.getInput().fieldName) || + !deepEqual(newInput.dataViewId, embeddable.getInput().dataViewId)) + ) { + // if the field name or data view id has changed in this editing session, selected options are invalid, so reset them. + newInput.value = undefined; + } + return newInput; + }; + + public controlEditorComponent = TimeSliderEditor; + + public isEditable = () => Promise.resolve(false); + + public getDisplayName = () => TimeSliderStrings.getDisplayName(); + public getDescription = () => TimeSliderStrings.getDescription(); + + public inject = createOptionsListInject(); + public extract = createOptionsListExtract(); +} diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts b/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts new file mode 100644 index 0000000000000..d4cb8aba8f510 --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/time_slider_reducers.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PayloadAction } from '@reduxjs/toolkit'; +import { WritableDraft } from 'immer/dist/types/types-external'; + +import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types'; + +export const timeSliderReducers = { + selectRange: ( + state: WritableDraft, + action: PayloadAction<[number | null, number | null]> + ) => { + state.value = action.payload; + }, +}; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider_strings.ts b/src/plugins/controls/public/control_types/time_slider/time_slider_strings.ts new file mode 100644 index 0000000000000..2c61d7d43a797 --- /dev/null +++ b/src/plugins/controls/public/control_types/time_slider/time_slider_strings.ts @@ -0,0 +1,46 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const TimeSliderStrings = { + getDisplayName: () => + i18n.translate('controls.timeSlider.displayName', { + defaultMessage: 'Time slider', + }), + getDescription: () => + i18n.translate('controls.timeSlider.description', { + defaultMessage: 'Add a slider for selecting a time range', + }), + editor: { + getDataViewTitle: () => + i18n.translate('controls.timeSlider.editor.dataViewTitle', { + defaultMessage: 'Data view', + }), + getNoDataViewTitle: () => + i18n.translate('controls.timeSlider.editor.noDataViewTitle', { + defaultMessage: 'Select data view', + }), + getFieldTitle: () => + i18n.translate('controls.timeSlider.editor.fieldTitle', { + defaultMessage: 'Field', + }), + }, + resetButton: { + getLabel: () => + i18n.translate('controls.timeSlider.resetButton.label', { + defaultMessage: 'Reset selections', + }), + }, + noDocumentsPopover: { + getLabel: () => + i18n.translate('controls.timeSlider.noDocuments.label', { + defaultMessage: 'There were no documents found. Range selection unavailable.', + }), + }, +}; diff --git a/src/plugins/controls/public/index.ts b/src/plugins/controls/public/index.ts index db4586b315075..5cc7ffce2c328 100644 --- a/src/plugins/controls/public/index.ts +++ b/src/plugins/controls/public/index.ts @@ -24,7 +24,12 @@ export type { ControlInput, } from '../common/types'; -export { CONTROL_GROUP_TYPE, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '../common'; +export { + CONTROL_GROUP_TYPE, + OPTIONS_LIST_CONTROL, + RANGE_SLIDER_CONTROL, + TIME_SLIDER_CONTROL, +} from '../common'; export { ControlGroupContainer, diff --git a/src/plugins/controls/public/plugin.ts b/src/plugins/controls/public/plugin.ts index 96cb7eeef3a27..b583f14d94ddd 100644 --- a/src/plugins/controls/public/plugin.ts +++ b/src/plugins/controls/public/plugin.ts @@ -29,7 +29,12 @@ import { CONTROL_GROUP_TYPE, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, + TIME_SLIDER_CONTROL, } from '.'; +import { + TimesliderEmbeddableFactory, + TimeSliderControlEmbeddableInput, +} from './control_types/time_slider'; import { controlsService } from './services/kibana/controls'; import { EmbeddableFactory } from '../../embeddable/public'; @@ -97,6 +102,19 @@ export class ControlsPlugin rangeSliderFactory ); registerControlType(rangeSliderFactory); + + // Time Slider Control Factory Setup + const timeSliderFactoryDef = new TimesliderEmbeddableFactory(); + const timeSliderFactory = embeddable.registerEmbeddableFactory( + TIME_SLIDER_CONTROL, + timeSliderFactoryDef + )(); + this.transferEditorFunctions( + timeSliderFactoryDef, + timeSliderFactory + ); + + registerControlType(timeSliderFactory); }); return { diff --git a/src/plugins/controls/public/services/data.ts b/src/plugins/controls/public/services/data.ts index f11e451995535..57c2342beb425 100644 --- a/src/plugins/controls/public/services/data.ts +++ b/src/plugins/controls/public/services/data.ts @@ -6,9 +6,24 @@ * Side Public License, v 1. */ +import { Observable } from 'rxjs'; import { DataPublicPluginStart } from '../../../data/public'; +import { DataView } from '../../../data_views/public'; +import { ControlInput } from '../types'; export interface ControlsDataService { + fetchFieldRange: ( + dataView: DataView, + fieldName: string, + input: ControlInput + ) => Promise<{ min: number; max: number }>; + fetchFieldRange$: ( + dataView: DataView, + fieldName: string, + input: ControlInput + ) => Observable<{ min?: number; max?: number }>; + getDataView: DataPublicPluginStart['dataViews']['get']; + getDataView$: (id: string) => Observable; autocomplete: DataPublicPluginStart['autocomplete']; query: DataPublicPluginStart['query']; searchSource: DataPublicPluginStart['search']['searchSource']; diff --git a/src/plugins/controls/public/services/index.ts b/src/plugins/controls/public/services/index.ts index d1dcd9b158a0a..1a8c8b0c4bdfe 100644 --- a/src/plugins/controls/public/services/index.ts +++ b/src/plugins/controls/public/services/index.ts @@ -15,6 +15,7 @@ import { ControlsDataService } from './data'; import { ControlsService } from './controls'; import { ControlsHTTPService } from './http'; import { ControlsOptionsListService } from './options_list'; +import { ControlsSettingsService } from './settings'; export interface ControlsServices { // dependency services @@ -22,6 +23,7 @@ export interface ControlsServices { overlays: ControlsOverlaysService; data: ControlsDataService; http: ControlsHTTPService; + settings: ControlsSettingsService; // controls plugin's own services controls: ControlsService; diff --git a/src/plugins/controls/public/services/kibana/data.ts b/src/plugins/controls/public/services/kibana/data.ts index 0830233109e2a..9eb981d90c237 100644 --- a/src/plugins/controls/public/services/kibana/data.ts +++ b/src/plugins/controls/public/services/kibana/data.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import { DataViewField } from 'src/plugins/data_views/common'; +import { get } from 'lodash'; +import { from } from 'rxjs'; import { ControlsDataService } from '../data'; import { ControlsPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../../../../presentation_util/public'; @@ -15,14 +18,86 @@ export type DataServiceFactory = KibanaPluginServiceFactory< ControlsPluginStartDeps >; +const minMaxAgg = (field?: DataViewField) => { + const aggBody: Record = {}; + if (field) { + if (field.scripted) { + aggBody.script = { + source: field.script, + lang: field.lang, + }; + } else { + aggBody.field = field.name; + } + } + + return { + maxAgg: { + max: aggBody, + }, + minAgg: { + min: aggBody, + }, + }; +}; + export const dataServiceFactory: DataServiceFactory = ({ startPlugins }) => { const { - data: { autocomplete, query, search }, + data: { query: queryPlugin, search, autocomplete }, } = startPlugins; + const { data } = startPlugins; + + const fetchFieldRange: ControlsDataService['fetchFieldRange'] = async ( + dataView, + fieldName, + input + ) => { + const { ignoreParentSettings, query, timeRange } = input; + let { filters = [] } = input; + + const field = dataView.getFieldByName(fieldName); + + if (!field) { + throw new Error('Field Missing Error'); + } + + if (timeRange) { + const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + if (timeFilter) { + filters = filters.concat(timeFilter); + } + } + + const searchSource = await data.search.searchSource.create(); + searchSource.setField('size', 0); + searchSource.setField('index', dataView); + + const aggs = minMaxAgg(field); + searchSource.setField('aggs', aggs); + + searchSource.setField('filter', ignoreParentSettings?.ignoreFilters ? [] : filters); + searchSource.setField('query', ignoreParentSettings?.ignoreQuery ? undefined : query); + + const resp = await searchSource.fetch$().toPromise(); + + const min = get(resp, 'rawResponse.aggregations.minAgg.value', undefined); + const max = get(resp, 'rawResponse.aggregations.maxAgg.value', undefined); + + return { + min: min === null ? undefined : min, + max: max === null ? undefined : max, + }; + }; + return { + fetchFieldRange, + fetchFieldRange$: (dataView, fieldName, input) => + from(fetchFieldRange(dataView, fieldName, input)), + getDataView: data.dataViews.get, + getDataView$: (id: string) => from(data.dataViews.get(id)), autocomplete, - query, + query: queryPlugin, searchSource: search.searchSource, - timefilter: query.timefilter.timefilter, + timefilter: queryPlugin.timefilter.timefilter, }; }; diff --git a/src/plugins/controls/public/services/kibana/index.ts b/src/plugins/controls/public/services/kibana/index.ts index f87bd744b3541..19a2c0891ac8f 100644 --- a/src/plugins/controls/public/services/kibana/index.ts +++ b/src/plugins/controls/public/services/kibana/index.ts @@ -21,6 +21,7 @@ import { overlaysServiceFactory } from './overlays'; import { dataServiceFactory } from './data'; import { httpServiceFactory } from './http'; import { optionsListServiceFactory } from './options_list'; +import { settingsServiceFactory } from './settings'; export const providers: PluginServiceProviders< ControlsServices, @@ -30,6 +31,7 @@ export const providers: PluginServiceProviders< data: new PluginServiceProvider(dataServiceFactory), overlays: new PluginServiceProvider(overlaysServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), + settings: new PluginServiceProvider(settingsServiceFactory), optionsList: new PluginServiceProvider(optionsListServiceFactory, ['data', 'http']), controls: new PluginServiceProvider(controlsServiceFactory), diff --git a/src/plugins/controls/public/services/kibana/settings.ts b/src/plugins/controls/public/services/kibana/settings.ts new file mode 100644 index 0000000000000..27e308f19693b --- /dev/null +++ b/src/plugins/controls/public/services/kibana/settings.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 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 { ControlsSettingsService } from '../settings'; +import { ControlsPluginStartDeps } from '../../types'; +import { KibanaPluginServiceFactory } from '../../../../presentation_util/public'; + +export type SettingsServiceFactory = KibanaPluginServiceFactory< + ControlsSettingsService, + ControlsPluginStartDeps +>; + +export const settingsServiceFactory: SettingsServiceFactory = ({ coreStart }) => { + return { + getDateFormat: () => { + return coreStart.uiSettings.get('dateFormat', 'MMM D, YYYY @ HH:mm:ss.SSS'); + }, + getTimezone: () => { + return coreStart.uiSettings.get('dateFormat:tz', 'Browser'); + }, + }; +}; diff --git a/src/plugins/controls/public/services/settings.ts b/src/plugins/controls/public/services/settings.ts new file mode 100644 index 0000000000000..476a99c5bd733 --- /dev/null +++ b/src/plugins/controls/public/services/settings.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface ControlsSettingsService { + getTimezone: () => string; + getDateFormat: () => string; +} diff --git a/src/plugins/controls/public/services/storybook/data.ts b/src/plugins/controls/public/services/storybook/data.ts index bfdcf05767b01..3e63066d0d5ff 100644 --- a/src/plugins/controls/public/services/storybook/data.ts +++ b/src/plugins/controls/public/services/storybook/data.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; +import { of, Observable } from 'rxjs'; import { PluginServiceFactory } from '../../../../presentation_util/public'; import { DataPublicPluginStart } from '../../../../data/public'; -import { DataViewField } from '../../../../data_views/common'; +import { DataViewField, DataView } from '../../../../data_views/common'; import { ControlsDataService } from '../data'; let valueSuggestionMethod = ({ field, query }: { field: DataViewField; query: string }) => @@ -38,4 +38,8 @@ export const dataServiceFactory: DataServiceFactory = () => ({ timefilter: { createFilter: () => {}, } as unknown as DataPublicPluginStart['query']['timefilter']['timefilter'], + fetchFieldRange: () => Promise.resolve({ min: 0, max: 100 }), + fetchFieldRange$: () => new Observable<{ min: number; max: number }>(), + getDataView: () => Promise.resolve({} as DataView), + getDataView$: () => new Observable({} as any), }); diff --git a/src/plugins/controls/public/services/storybook/index.ts b/src/plugins/controls/public/services/storybook/index.ts index 4eabd6a1bb006..535befd925f49 100644 --- a/src/plugins/controls/public/services/storybook/index.ts +++ b/src/plugins/controls/public/services/storybook/index.ts @@ -17,6 +17,7 @@ import { dataServiceFactory } from './data'; import { overlaysServiceFactory } from './overlays'; import { dataViewsServiceFactory } from './data_views'; import { httpServiceFactory } from '../stub/http'; +import { settingsServiceFactory } from './settings'; import { optionsListServiceFactory } from './options_list'; import { controlsServiceFactory } from '../stub/controls'; @@ -28,6 +29,7 @@ export const providers: PluginServiceProviders = { http: new PluginServiceProvider(httpServiceFactory), data: new PluginServiceProvider(dataServiceFactory), overlays: new PluginServiceProvider(overlaysServiceFactory), + settings: new PluginServiceProvider(settingsServiceFactory), controls: new PluginServiceProvider(controlsServiceFactory), optionsList: new PluginServiceProvider(optionsListServiceFactory), diff --git a/src/plugins/controls/public/services/storybook/settings.ts b/src/plugins/controls/public/services/storybook/settings.ts new file mode 100644 index 0000000000000..40b175bcec7bb --- /dev/null +++ b/src/plugins/controls/public/services/storybook/settings.ts @@ -0,0 +1,16 @@ +/* + * 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 { PluginServiceFactory } from '../../../../presentation_util/public'; +import { ControlsSettingsService } from '../settings'; + +export type SettingsServiceFactory = PluginServiceFactory; +export const settingsServiceFactory: SettingsServiceFactory = () => ({ + getTimezone: () => 'Browser', + getDateFormat: () => 'MMM D, YYYY @ HH:mm:ss.SSS', +}); diff --git a/src/plugins/controls/public/services/stub/index.ts b/src/plugins/controls/public/services/stub/index.ts index ddb0a76057648..486c42a999f9a 100644 --- a/src/plugins/controls/public/services/stub/index.ts +++ b/src/plugins/controls/public/services/stub/index.ts @@ -19,12 +19,14 @@ import { controlsServiceFactory } from './controls'; import { dataServiceFactory } from '../storybook/data'; import { dataViewsServiceFactory } from '../storybook/data_views'; import { optionsListServiceFactory } from '../storybook/options_list'; +import { settingsServiceFactory } from '../storybook/settings'; export const providers: PluginServiceProviders = { http: new PluginServiceProvider(httpServiceFactory), data: new PluginServiceProvider(dataServiceFactory), overlays: new PluginServiceProvider(overlaysServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), + settings: new PluginServiceProvider(settingsServiceFactory), controls: new PluginServiceProvider(controlsServiceFactory), optionsList: new PluginServiceProvider(optionsListServiceFactory), diff --git a/src/plugins/controls/server/control_types/time_slider/time_slider_embeddable_factory.ts b/src/plugins/controls/server/control_types/time_slider/time_slider_embeddable_factory.ts new file mode 100644 index 0000000000000..0b48dc94707c4 --- /dev/null +++ b/src/plugins/controls/server/control_types/time_slider/time_slider_embeddable_factory.ts @@ -0,0 +1,22 @@ +/* + * 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 { EmbeddableRegistryDefinition } from '../../../../embeddable/server'; +import { TIME_SLIDER_CONTROL } from '../../../common'; +import { + createTimeSliderExtract, + createTimeSliderInject, +} from '../../../common/control_types/time_slider/time_slider_persistable_state'; + +export const timeSliderPersistableStateServiceFactory = (): EmbeddableRegistryDefinition => { + return { + id: TIME_SLIDER_CONTROL, + extract: createTimeSliderExtract(), + inject: createTimeSliderInject(), + }; +}; diff --git a/src/plugins/controls/server/plugin.ts b/src/plugins/controls/server/plugin.ts index 261737f2775c4..6ff67f702be54 100644 --- a/src/plugins/controls/server/plugin.ts +++ b/src/plugins/controls/server/plugin.ts @@ -13,6 +13,7 @@ import { PluginSetup as DataSetup } from '../../data/server'; import { setupOptionsListSuggestionsRoute } from './control_types/options_list/options_list_suggestions_route'; import { controlGroupContainerPersistableStateServiceFactory } from './control_group/control_group_container_factory'; import { optionsListPersistableStateServiceFactory } from './control_types/options_list/options_list_embeddable_factory'; +import { timeSliderPersistableStateServiceFactory } from './control_types/time_slider/time_slider_embeddable_factory'; interface SetupDeps { embeddable: EmbeddableSetup; @@ -22,6 +23,7 @@ interface SetupDeps { export class ControlsPlugin implements Plugin { public setup(core: CoreSetup, { embeddable, data }: SetupDeps) { embeddable.registerEmbeddableFactory(optionsListPersistableStateServiceFactory()); + embeddable.registerEmbeddableFactory(timeSliderPersistableStateServiceFactory()); embeddable.registerEmbeddableFactory( controlGroupContainerPersistableStateServiceFactory(embeddable) diff --git a/src/plugins/controls/tsconfig.json b/src/plugins/controls/tsconfig.json index ed0c2e63011d0..10ec84fbd32f6 100644 --- a/src/plugins/controls/tsconfig.json +++ b/src/plugins/controls/tsconfig.json @@ -13,7 +13,8 @@ "public/**/*.json", "server/**/*", "storybook/**/*", - "../../../typings/**/*" + "../../../typings/**/*", + "./jest_setup.ts" ], "references": [ { "path": "../../core/tsconfig.json" }, From ad026f89cd1ecf1dc692dbedcd022e4b5f482163 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 29 Mar 2022 21:22:30 -0500 Subject: [PATCH 052/108] Revert "[Monitor management] Show public beta fair usage (#128770)" This reverts commit 436dfbed8cb43fd39e32b684871a65360db22c8a. --- .../monitor_management/locations.ts | 15 +-- .../monitor_management/monitor_types.ts | 27 ++-- .../action_bar/action_bar.tsx | 40 +++++- .../monitor_list/monitor_async_error.test.tsx | 116 ------------------ .../monitor_list/monitor_async_error.tsx | 75 ----------- .../monitor_list/monitor_list_container.tsx | 2 - .../monitor_management/show_sync_errors.tsx | 52 -------- .../monitor_management/monitor_management.tsx | 4 +- .../state/reducers/monitor_management.ts | 1 - .../synthetics_service/synthetics_service.ts | 7 +- .../synthetics_service/get_monitor.ts | 3 +- 11 files changed, 57 insertions(+), 285 deletions(-) delete mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor_management/show_sync_errors.tsx diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts index 82d2bc8afa412..d11ae7c655405 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts @@ -64,15 +64,12 @@ export const ServiceLocationErrors = t.array( status: t.number, }), t.partial({ - failed_monitors: t.union([ - t.array( - t.interface({ - id: t.string, - message: t.string, - }) - ), - t.null, - ]), + failed_monitors: t.array( + t.interface({ + id: t.string, + message: t.string, + }) + ), }), ]), }) diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts index 872ccdbb71ec8..44c643d2160d1 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { secretKeys } from '../../constants/monitor_management'; import { ConfigKey } from './config_key'; -import { LocationsCodec, ServiceLocationErrors } from './locations'; +import { LocationsCodec } from './locations'; import { DataStreamCodec, ModeCodec, @@ -306,23 +306,14 @@ export type EncryptedSyntheticsMonitorWithId = t.TypeOf< typeof EncryptedSyntheticsMonitorWithIdCodec >; -export const MonitorManagementListResultCodec = t.intersection([ - t.type({ - monitors: t.array( - t.interface({ - id: t.string, - attributes: EncryptedSyntheticsMonitorCodec, - updated_at: t.string, - }) - ), - page: t.number, - perPage: t.number, - total: t.union([t.number, t.null]), - }), - t.partial({ - syncErrors: ServiceLocationErrors, - }), -]); +export const MonitorManagementListResultCodec = t.type({ + monitors: t.array( + t.interface({ id: t.string, attributes: EncryptedSyntheticsMonitorCodec, updated_at: t.string }) + ), + page: t.number, + perPage: t.number, + total: t.union([t.number, t.null]), +}); export type MonitorManagementListResult = t.TypeOf; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx index 5f6e67e363171..3b30458974ed7 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { MONITOR_MANAGEMENT_ROUTE } from '../../../../common/constants'; import { UptimeSettingsContext } from '../../../contexts'; @@ -31,7 +32,6 @@ import { TestRun } from '../test_now_mode/test_now_mode'; import { monitorManagementListSelector } from '../../../state/selectors'; import { kibanaService } from '../../../state/kibana_service'; -import { showSyncErrors } from '../show_sync_errors'; export interface ActionBarProps { monitor: SyntheticsMonitor; @@ -103,7 +103,43 @@ export const ActionBar = ({ }); setIsSuccessful(true); } else if (hasErrors && !loading) { - showSyncErrors(data.attributes.errors, locations); + Object.values(data.attributes.errors!).forEach((location) => { + const { status: responseStatus, reason } = location.error || {}; + kibanaService.toasts.addWarning({ + title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { + defaultMessage: `Unable to sync monitor config`, + }), + text: toMountPoint( + <> +

+ {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { + defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, + values: { + location: locations?.find((loc) => loc?.id === location.locationId)?.label, + }, + })} +

+ {responseStatus || reason ? ( +

+ {responseStatus + ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { + defaultMessage: 'Status: {status}. ', + values: { status: responseStatus }, + }) + : null} + {reason + ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { + defaultMessage: 'Reason: {reason}.', + values: { reason }, + }) + : null} +

+ ) : null} + + ), + toastLifeTimeMs: 30000, + }); + }); setIsSuccessful(true); } }, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx deleted file mode 100644 index 1122d136c926c..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx +++ /dev/null @@ -1,116 +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 { screen } from '@testing-library/react'; -import React from 'react'; -import { DEFAULT_THROTTLING } from '../../../../common/runtime_types'; -import { render } from '../../../lib/helper/rtl_helpers'; -import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; -import { MonitorAsyncError } from './monitor_async_error'; - -describe('', () => { - const location1 = 'US Central'; - const location2 = 'US North'; - const reason1 = 'Unauthorized'; - const reason2 = 'Forbidden'; - const status1 = 401; - const status2 = 403; - const state = { - monitorManagementList: { - throttling: DEFAULT_THROTTLING, - enablement: null, - list: { - perPage: 5, - page: 1, - total: 6, - monitors: [], - syncErrors: [ - { - locationId: 'us_central', - error: { - reason: reason1, - status: status1, - }, - }, - { - locationId: 'us_north', - error: { - reason: reason2, - status: status2, - }, - }, - ], - }, - locations: [ - { - id: 'us_central', - label: location1, - geo: { - lat: 0, - lon: 0, - }, - url: '', - }, - { - id: 'us_north', - label: location2, - geo: { - lat: 0, - lon: 0, - }, - url: '', - }, - ], - error: { - serviceLocations: null, - monitorList: null, - enablement: null, - }, - loading: { - monitorList: true, - serviceLocations: false, - enablement: false, - }, - syntheticsService: { - loading: false, - }, - } as MonitorManagementListState, - }; - - it('renders when errors are defined', () => { - render(, { state }); - - expect(screen.getByText(new RegExp(reason1))).toBeInTheDocument(); - expect(screen.getByText(new RegExp(`${status1}`))).toBeInTheDocument(); - expect(screen.getByText(new RegExp(reason2))).toBeInTheDocument(); - expect(screen.getByText(new RegExp(`${status2}`))).toBeInTheDocument(); - expect(screen.getByText(new RegExp(location1))).toBeInTheDocument(); - expect(screen.getByText(new RegExp(location2))).toBeInTheDocument(); - }); - - it('renders null when errors are empty', () => { - render(, { - state: { - ...state, - monitorManagementList: { - ...state.monitorManagementList, - list: { - ...state.monitorManagementList.list, - syncErrors: [], - }, - }, - }, - }); - - expect(screen.queryByText(new RegExp(reason1))).not.toBeInTheDocument(); - expect(screen.queryByText(new RegExp(`${status1}`))).not.toBeInTheDocument(); - expect(screen.queryByText(new RegExp(reason2))).not.toBeInTheDocument(); - expect(screen.queryByText(new RegExp(`${status2}`))).not.toBeInTheDocument(); - expect(screen.queryByText(new RegExp(location1))).not.toBeInTheDocument(); - expect(screen.queryByText(new RegExp(location2))).not.toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx deleted file mode 100644 index c9e9dba2027a4..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx +++ /dev/null @@ -1,75 +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, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { monitorManagementListSelector } from '../../../state/selectors'; - -export const MonitorAsyncError = () => { - const [isDismissed, setIsDismissed] = useState(false); - const { list, locations } = useSelector(monitorManagementListSelector); - const syncErrors = list.syncErrors; - const hasSyncErrors = syncErrors && syncErrors.length > 0; - - return hasSyncErrors && !isDismissed ? ( - <> - - } - color="warning" - iconType="alert" - > -

- -

-
    - {Object.values(syncErrors).map((e) => { - return ( -
  • {`${ - locations.find((location) => location.id === e.locationId)?.label - } - ${STATUS_LABEL}: ${e.error.status}; ${REASON_LABEL}: ${e.error.reason}.`}
  • - ); - })} -
- setIsDismissed(true)} color="warning"> - {DISMISS_LABEL} - -
- - - ) : null; -}; - -const REASON_LABEL = i18n.translate( - 'xpack.uptime.monitorManagement.monitorSync.failure.reasonLabel', - { - defaultMessage: 'Reason', - } -); - -const STATUS_LABEL = i18n.translate( - 'xpack.uptime.monitorManagement.monitorSync.failure.statusLabel', - { - defaultMessage: 'Status', - } -); - -const DISMISS_LABEL = i18n.translate( - 'xpack.uptime.monitorManagement.monitorSync.failure.dismissLabel', - { - defaultMessage: 'Dismiss', - } -); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx index 53afdf49c1592..a3f041a33a9f8 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx @@ -13,7 +13,6 @@ import { ConfigKey } from '../../../../common/runtime_types'; import { getMonitors } from '../../../state/actions'; import { monitorManagementListSelector } from '../../../state/selectors'; import { MonitorManagementListPageState } from './monitor_list'; -import { MonitorAsyncError } from './monitor_async_error'; import { useInlineErrors } from '../hooks/use_inline_errors'; import { MonitorListTabs } from './list_tabs'; import { AllMonitors } from './all_monitors'; @@ -67,7 +66,6 @@ export const MonitorListContainer: React.FC = () => { return ( <> - { - Object.values(errors).forEach((location) => { - const { status: responseStatus, reason } = location.error || {}; - kibanaService.toasts.addWarning({ - title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { - defaultMessage: `Unable to sync monitor config`, - }), - text: toMountPoint( - <> -

- {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { - defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, - values: { - location: locations?.find((loc) => loc?.id === location.locationId)?.label, - }, - })} -

- {responseStatus || reason ? ( -

- {responseStatus - ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { - defaultMessage: 'Status: {status}. ', - values: { status: responseStatus }, - }) - : null} - {reason - ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { - defaultMessage: 'Reason: {reason}.', - values: { reason }, - }) - : null} -

- ) : null} - - ), - toastLifeTimeMs: 30000, - }); - }); -}; diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx index 71785dbaf78ee..3e0e9b955f31f 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx @@ -17,7 +17,6 @@ import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadc import { MonitorListContainer } from '../../components/monitor_management/monitor_list/monitor_list_container'; import { EnablementEmptyState } from '../../components/monitor_management/monitor_list/enablement_empty_state'; import { useEnablement } from '../../components/monitor_management/hooks/use_enablement'; -import { useLocations } from '../../components/monitor_management/hooks/use_locations'; import { Loader } from '../../components/monitor_management/loader/loader'; export const MonitorManagementPage: React.FC = () => { @@ -33,7 +32,6 @@ export const MonitorManagementPage: React.FC = () => { loading: enablementLoading, enableSynthetics, } = useEnablement(); - const { loading: locationsLoading } = useLocations(); const { list: monitorList } = useSelector(monitorManagementListSelector); const { isEnabled } = enablement; @@ -64,7 +62,7 @@ export const MonitorManagementPage: React.FC = () => { return ( <> ({ search: schema.maybe(schema.string()), }), }, - handler: async ({ request, savedObjectsClient, server }): Promise => { + handler: async ({ request, savedObjectsClient }): Promise => { const { perPage = 50, page, sortField, sortOrder, search } = request.query; // TODO: add query/filtering params const { @@ -78,7 +78,6 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ ...rest, perPage: perPageT, monitors, - syncErrors: server.syntheticsService.syncErrors, }; }, }); From 9122810c17a7fce4942f8ce1c2fffb42d193e2e0 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 29 Mar 2022 22:44:58 -0400 Subject: [PATCH 053/108] Update spaces list and saved object management page (#128669) --- .../objects_table/components/table.tsx | 6 + .../share_saved_objects_to_space_action.tsx | 4 +- .../public/services/column_service.test.ts | 8 +- .../public/services/column_service.ts | 7 +- .../public/services/columns/constants.ts | 29 +++ .../share_saved_objects_to_space_column.tsx | 171 +++++++++++++++--- .../public/services/index.ts | 4 +- .../public/services/types/action.ts | 2 +- .../public/services/types/column.ts | 31 +++- .../public/services/types/index.ts | 2 +- 10 files changed, 223 insertions(+), 41 deletions(-) create mode 100644 src/plugins/saved_objects_management/public/services/columns/constants.ts diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index a7f43438e5525..fb0e55fb272a1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -227,6 +227,12 @@ export class Table extends PureComponent { } as EuiTableFieldDataColumnType>, ...(taggingApi ? [taggingApi.ui.getTableColumnDefinition()] : []), ...columnRegistry.getAll().map((column) => { + column.setColumnContext({ capabilities }); + column.registerOnFinishCallback(() => { + const { refreshOnFinish = () => [] } = column; + const objectsToRefresh = refreshOnFinish(); + onActionRefresh(objectsToRefresh); + }); return { ...column.euiColumn, sortable: false, diff --git a/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.tsx b/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.tsx index e36c13bd8fd8b..b1a80c71e4ea2 100644 --- a/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.tsx +++ b/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.tsx @@ -33,10 +33,10 @@ export class ShareToSpaceSavedObjectsManagementAction extends SavedObjectsManage public euiAction = { name: i18n.translate('savedObjectsManagement.shareToSpace.actionTitle', { - defaultMessage: 'Share to space', + defaultMessage: 'Assign spaces', }), description: i18n.translate('savedObjectsManagement.shareToSpace.actionDescription', { - defaultMessage: 'Share this saved object to one or more spaces', + defaultMessage: 'Change the spaces this object is assigned to', }), icon: 'share', type: 'icon', diff --git a/src/plugins/saved_objects_management/public/services/column_service.test.ts b/src/plugins/saved_objects_management/public/services/column_service.test.ts index 5676cfaa81285..1124e71b36c52 100644 --- a/src/plugins/saved_objects_management/public/services/column_service.test.ts +++ b/src/plugins/saved_objects_management/public/services/column_service.test.ts @@ -14,8 +14,10 @@ import { } from './column_service'; import { SavedObjectsManagementColumn } from './types'; -class DummyColumn implements SavedObjectsManagementColumn { - constructor(public id: string) {} +class DummyColumn extends SavedObjectsManagementColumn { + constructor(public id: string) { + super(); + } public euiColumn = { field: 'id', @@ -29,7 +31,7 @@ describe('SavedObjectsManagementColumnRegistry', () => { let service: SavedObjectsManagementColumnService; let setup: SavedObjectsManagementColumnServiceSetup; - const createColumn = (id: string): SavedObjectsManagementColumn => { + const createColumn = (id: string): SavedObjectsManagementColumn => { return new DummyColumn(id); }; diff --git a/src/plugins/saved_objects_management/public/services/column_service.ts b/src/plugins/saved_objects_management/public/services/column_service.ts index 8189fc7d07f83..d9c88ec8834e5 100644 --- a/src/plugins/saved_objects_management/public/services/column_service.ts +++ b/src/plugins/saved_objects_management/public/services/column_service.ts @@ -14,18 +14,18 @@ export interface SavedObjectsManagementColumnServiceSetup { /** * register given column in the registry. */ - register: (column: SavedObjectsManagementColumn) => void; + register: (column: SavedObjectsManagementColumn) => void; } export interface SavedObjectsManagementColumnServiceStart { /** * return all {@link SavedObjectsManagementColumn | columns} currently registered. */ - getAll: () => Array>; + getAll: () => SavedObjectsManagementColumn[]; } export class SavedObjectsManagementColumnService { - private readonly columns = new Map>(); + private readonly columns = new Map(); setup(): SavedObjectsManagementColumnServiceSetup { return { @@ -52,6 +52,5 @@ function registerSpacesApiColumns( service: SavedObjectsManagementColumnService, spacesApi: SpacesApi ) { - // Note: this column is hidden for now because no saved objects are shareable. It should be uncommented when at least one saved object type is multi-namespace. service.setup().register(new ShareToSpaceSavedObjectsManagementColumn(spacesApi.ui)); } diff --git a/src/plugins/saved_objects_management/public/services/columns/constants.ts b/src/plugins/saved_objects_management/public/services/columns/constants.ts new file mode 100644 index 0000000000000..9f459fbbf2fbe --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/columns/constants.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 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. + */ + +/** + * This is a hard-coded list that can be removed when each of these "share-capable" object types are changed to be shareable. + * Note, this list does not preclude other object types from being made shareable in the future, it just consists of the object types that + * we are working towards making shareable in the near term. + * + * This is purely for changing the tooltip in the Saved Object Management UI, it's not used anywhere else. + */ +export const SHAREABLE_SOON_OBJECT_TYPES = [ + 'tag', + 'dashboard', + 'canvas-workpad', + 'canvas-element', + 'lens', + 'visualization', + 'map', + 'graph-workspace', + 'search', + 'query', + 'rule', + 'connector', +]; diff --git a/src/plugins/saved_objects_management/public/services/columns/share_saved_objects_to_space_column.tsx b/src/plugins/saved_objects_management/public/services/columns/share_saved_objects_to_space_column.tsx index 6971021a81e84..3ce8b82f98acf 100644 --- a/src/plugins/saved_objects_management/public/services/columns/share_saved_objects_to_space_column.tsx +++ b/src/plugins/saved_objects_management/public/services/columns/share_saved_objects_to_space_column.tsx @@ -6,49 +6,170 @@ * Side Public License, v 1. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import type { SpaceListProps, SpacesApiUi } from '../../../../../../x-pack/plugins/spaces/public'; -import type { SavedObjectsManagementColumn } from '../types'; +import type { SavedObjectsNamespaceType } from 'src/core/public'; +import { EuiIconTip, EuiToolTip } from '@elastic/eui'; + +import type { + ShareToSpaceFlyoutProps, + SpaceListProps, + SpacesApiUi, +} from '../../../../../../x-pack/plugins/spaces/public'; +import type { SavedObjectsManagementRecord } from '../types'; +import { SavedObjectsManagementColumn } from '../types'; +import { SHAREABLE_SOON_OBJECT_TYPES } from './constants'; interface WrapperProps { + objectType: string; + objectNamespaceType: SavedObjectsNamespaceType; spacesApiUi: SpacesApiUi; - props: SpaceListProps; + spaceListProps: SpaceListProps; + flyoutProps: ShareToSpaceFlyoutProps; } -const Wrapper = ({ spacesApiUi, props }: WrapperProps) => { - const LazyComponent = useMemo(() => spacesApiUi.components.getSpaceList, [spacesApiUi]); +const columnName = i18n.translate('savedObjectsManagement.shareToSpace.columnTitle', { + defaultMessage: 'Spaces', +}); +const columnDescription = i18n.translate('savedObjectsManagement.shareToSpace.columnDescription', { + defaultMessage: 'The spaces that this object is currently assigned to', +}); +const isolatedObjectTypeTitle = i18n.translate( + 'savedObjectsManagement.shareToSpace.isolatedObjectTypeTitle', + { defaultMessage: 'Isolated saved object' } +); +const isolatedObjectTypeContent = i18n.translate( + 'savedObjectsManagement.shareToSpace.isolatedObjectTypeContent', + { + defaultMessage: + 'This saved object is available in only one space, it cannot be assigned to multiple spaces.', + } +); +const shareableSoonObjectTypeTitle = i18n.translate( + 'savedObjectsManagement.shareToSpace.shareableSoonObjectTypeTitle', + { defaultMessage: 'Coming soon: Assign saved object to multiple spaces' } +); +const shareableSoonObjectTypeContent = i18n.translate( + 'savedObjectsManagement.shareToSpace.shareableSoonObjectTypeContent', + { + defaultMessage: + 'This saved object is available in only one space. In a future release, you can assign it to multiple spaces.', + } +); +const globalObjectTypeTitle = i18n.translate( + 'savedObjectsManagement.shareToSpace.globalObjectTypeTitle', + { defaultMessage: 'Global saved object' } +); +const globalObjectTypeContent = i18n.translate( + 'savedObjectsManagement.shareToSpace.globalObjectTypeContent', + { defaultMessage: 'This saved object is available in all spaces and cannot be changed.' } +); + +const Wrapper = ({ + objectType, + objectNamespaceType, + spacesApiUi, + spaceListProps, + flyoutProps, +}: WrapperProps) => { + const [showFlyout, setShowFlyout] = useState(false); + + function listOnClick() { + setShowFlyout(true); + } + + function onClose() { + setShowFlyout(false); + flyoutProps.onClose?.(); + } + + const LazySpaceList = useMemo(() => spacesApiUi.components.getSpaceList, [spacesApiUi]); + const LazyShareToSpaceFlyout = useMemo( + () => spacesApiUi.components.getShareToSpaceFlyout, + [spacesApiUi] + ); + const LazySpaceAvatar = useMemo(() => spacesApiUi.components.getSpaceAvatar, [spacesApiUi]); - return ; + if (objectNamespaceType === 'single' || objectNamespaceType === 'multiple-isolated') { + const tooltipProps = SHAREABLE_SOON_OBJECT_TYPES.includes(objectType) + ? { title: shareableSoonObjectTypeTitle, content: shareableSoonObjectTypeContent } + : { title: isolatedObjectTypeTitle, content: isolatedObjectTypeContent }; + return ; + } else if (objectNamespaceType === 'agnostic') { + return ( + + + + ); + } + + return ( + <> + + {showFlyout && } + + ); }; -export class ShareToSpaceSavedObjectsManagementColumn - implements SavedObjectsManagementColumn -{ +export class ShareToSpaceSavedObjectsManagementColumn extends SavedObjectsManagementColumn { public id: string = 'share_saved_objects_to_space'; public euiColumn = { field: 'namespaces', - name: i18n.translate('savedObjectsManagement.shareToSpace.columnTitle', { - defaultMessage: 'Shared spaces', - }), - description: i18n.translate('savedObjectsManagement.shareToSpace.columnDescription', { - defaultMessage: 'The other spaces that this object is currently shared to', - }), - render: (namespaces: string[] | undefined) => { - if (!namespaces) { - return null; - } - - const props: SpaceListProps = { - namespaces, + name: columnName, + description: columnDescription, + render: (namespaces: string[] | undefined, record: SavedObjectsManagementRecord) => { + const spaceListProps: SpaceListProps = { + namespaces: namespaces ?? [], + behaviorContext: 'outside-space', + }; + const flyoutProps: ShareToSpaceFlyoutProps = { + savedObjectTarget: { + type: record.type, + id: record.id, + namespaces: namespaces ?? [], + title: record.meta.title, + icon: record.meta.icon, + }, + flyoutIcon: 'share', + onUpdate: (updatedObjects: Array<{ type: string; id: string }>) => + (this.objectsToRefresh = [...updatedObjects]), + onClose: this.onClose, + enableCreateCopyCallout: true, + enableCreateNewSpaceLink: true, }; - return ; + return ( + + ); }, }; + public refreshOnFinish = () => this.objectsToRefresh; + + private objectsToRefresh: Array<{ type: string; id: string }> = []; - constructor(private readonly spacesApiUi: SpacesApiUi) {} + constructor(private readonly spacesApiUi: SpacesApiUi) { + super(); + } + + private onClose = () => { + this.finish(); + }; } diff --git a/src/plugins/saved_objects_management/public/services/index.ts b/src/plugins/saved_objects_management/public/services/index.ts index c45c81d3122ad..997a1b96b418f 100644 --- a/src/plugins/saved_objects_management/public/services/index.ts +++ b/src/plugins/saved_objects_management/public/services/index.ts @@ -16,5 +16,5 @@ export type { SavedObjectsManagementColumnServiceSetup, } from './column_service'; export { SavedObjectsManagementColumnService } from './column_service'; -export type { SavedObjectsManagementColumn, SavedObjectsManagementRecord } from './types'; -export { SavedObjectsManagementAction } from './types'; +export type { SavedObjectsManagementRecord } from './types'; +export { SavedObjectsManagementColumn, SavedObjectsManagementAction } from './types'; diff --git a/src/plugins/saved_objects_management/public/services/types/action.ts b/src/plugins/saved_objects_management/public/services/types/action.ts index 625405e9955fd..b0ab229065555 100644 --- a/src/plugins/saved_objects_management/public/services/types/action.ts +++ b/src/plugins/saved_objects_management/public/services/types/action.ts @@ -7,7 +7,7 @@ */ import { ReactNode } from 'react'; -import { Capabilities } from 'src/core/public'; +import type { Capabilities } from 'src/core/public'; import { SavedObjectsManagementRecord } from '.'; interface ActionContext { diff --git a/src/plugins/saved_objects_management/public/services/types/column.ts b/src/plugins/saved_objects_management/public/services/types/column.ts index 1be279db91205..fcdb33015df59 100644 --- a/src/plugins/saved_objects_management/public/services/types/column.ts +++ b/src/plugins/saved_objects_management/public/services/types/column.ts @@ -7,9 +7,34 @@ */ import { EuiTableFieldDataColumnType } from '@elastic/eui'; +import type { Capabilities } from 'src/core/public'; import { SavedObjectsManagementRecord } from '.'; -export interface SavedObjectsManagementColumn { - id: string; - euiColumn: Omit, 'sortable'>; +interface ColumnContext { + capabilities: Capabilities; +} + +export abstract class SavedObjectsManagementColumn { + public abstract id: string; + public abstract euiColumn: Omit< + EuiTableFieldDataColumnType, + 'sortable' + >; + public refreshOnFinish?: () => Array<{ type: string; id: string }>; + + private callbacks: Function[] = []; + + protected columnContext: ColumnContext | null = null; + + public setColumnContext(columnContext: ColumnContext) { + this.columnContext = columnContext; + } + + public registerOnFinishCallback(callback: Function) { + this.callbacks.push(callback); + } + + protected finish() { + this.callbacks.forEach((callback) => callback()); + } } diff --git a/src/plugins/saved_objects_management/public/services/types/index.ts b/src/plugins/saved_objects_management/public/services/types/index.ts index 82b45f9df33f0..ba95f78c7afc9 100644 --- a/src/plugins/saved_objects_management/public/services/types/index.ts +++ b/src/plugins/saved_objects_management/public/services/types/index.ts @@ -7,5 +7,5 @@ */ export { SavedObjectsManagementAction } from './action'; -export type { SavedObjectsManagementColumn } from './column'; +export { SavedObjectsManagementColumn } from './column'; export type { SavedObjectsManagementRecord } from './record'; From 482f819a0580d58e766e1db7e8e012a351e41247 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Tue, 29 Mar 2022 20:11:55 -0700 Subject: [PATCH 054/108] [Security Solution][Alerts] Replace schemas derived from FieldMaps with versioned alert schema (#127218) * Replace schemas derived from FieldMaps with versioned alert schema * Import fixes and comment * Another import fix * Separate read and write schemas * Separate read and write schemas for common alert fields * fix import * Update ALERT_RULE_PARAMETERS type * Fix getField type * Fix more types * Remove unneeded index signature from PersistenceAlertServiceResult * Fix types and tests * Update comment describing new schema process * Update Ancestor800 type * Add modified PR description as initial README * Remove duplication in CommonAlertFields definition * Add explicit undefined value for rule in mock Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/technical_field_names.ts | 3 + .../field_maps/experimental_rule_field_map.ts | 2 +- .../field_maps/technical_rule_field_map.ts | 5 +- .../common/schemas/8.0.0/index.ts | 52 ++++ .../rule_registry/common/schemas/README.md | 1 + .../rule_registry/common/schemas/index.ts | 20 ++ .../server/utils/create_lifecycle_executor.ts | 9 +- .../server/utils/get_common_alert_fields.ts | 26 +- .../server/utils/persistence_types.ts | 3 +- .../schemas/alerts/8.0.0/index.ts | 178 +++++++++++++ .../detection_engine/schemas/alerts/README.md | 54 ++++ .../detection_engine/schemas/alerts/index.ts | 28 +++ .../schemas/request/rule_schemas.ts | 2 +- .../common/field_maps/{ => 8.0.0}/alerts.ts | 4 +- .../common/field_maps/8.0.0/index.ts | 11 + .../common/field_maps/{ => 8.0.0}/rules.ts | 0 .../common/field_maps/field_names.ts | 12 + .../common/field_maps/index.ts | 4 +- .../schedule_notification_actions.ts | 8 +- .../factories/bulk_create_factory.ts | 14 +- .../factories/utils/build_alert.test.ts | 2 + .../rule_types/factories/utils/build_alert.ts | 156 ++++++++---- .../utils/build_alert_group_from_sequence.ts | 89 +++---- .../factories/utils/build_bulk_body.ts | 7 +- .../factories/utils/filter_source.ts | 3 +- .../utils/generate_building_block_ids.ts | 9 +- .../rule_types/factories/wrap_hits_factory.ts | 26 +- .../factories/wrap_sequences_factory.ts | 7 +- .../lib/detection_engine/rule_types/types.ts | 19 -- .../signals/__mocks__/es_results.ts | 137 +++++++++- .../signals/bulk_create_ml_signals.ts | 3 +- .../detection_engine/signals/executors/eql.ts | 7 +- .../signals/filter_duplicate_signals.test.ts | 48 ---- .../signals/filter_duplicate_signals.ts | 29 --- .../signals/search_after_bulk_create.test.ts | 234 +++++++++++++++--- .../signals/threshold/build_signal_history.ts | 6 +- .../bulk_create_threshold_signals.ts | 3 +- .../lib/detection_engine/signals/types.ts | 17 +- .../detection_engine/signals/utils.test.ts | 22 +- .../lib/detection_engine/signals/utils.ts | 22 +- .../basic/tests/open_close_signals.ts | 24 +- .../basic/tests/update_rac_alerts.ts | 26 +- .../tests/open_close_signals.ts | 10 +- .../detection_engine_api_integration/utils.ts | 14 +- 44 files changed, 994 insertions(+), 362 deletions(-) create mode 100644 x-pack/plugins/rule_registry/common/schemas/8.0.0/index.ts create mode 100644 x-pack/plugins/rule_registry/common/schemas/README.md create mode 100644 x-pack/plugins/rule_registry/common/schemas/index.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/8.0.0/index.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/README.md create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/index.ts rename x-pack/plugins/security_solution/common/field_maps/{ => 8.0.0}/alerts.ts (97%) create mode 100644 x-pack/plugins/security_solution/common/field_maps/8.0.0/index.ts rename x-pack/plugins/security_solution/common/field_maps/{ => 8.0.0}/rules.ts (100%) delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index f76043c2a6afc..e6b6494e68a56 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -54,6 +54,7 @@ const ALERT_RULE_INTERVAL = `${ALERT_RULE_NAMESPACE}.interval` as const; const ALERT_RULE_LICENSE = `${ALERT_RULE_NAMESPACE}.license` as const; const ALERT_RULE_CATEGORY = `${ALERT_RULE_NAMESPACE}.category` as const; const ALERT_RULE_NAME = `${ALERT_RULE_NAMESPACE}.name` as const; +const ALERT_RULE_NAMESPACE_FIELD = `${ALERT_RULE_NAMESPACE}.namespace` as const; const ALERT_RULE_NOTE = `${ALERT_RULE_NAMESPACE}.note` as const; const ALERT_RULE_PARAMETERS = `${ALERT_RULE_NAMESPACE}.parameters` as const; const ALERT_RULE_REFERENCES = `${ALERT_RULE_NAMESPACE}.references` as const; @@ -111,6 +112,7 @@ const fields = { ALERT_RULE_INTERVAL, ALERT_RULE_LICENSE, ALERT_RULE_NAME, + ALERT_RULE_NAMESPACE_FIELD, ALERT_RULE_NOTE, ALERT_RULE_PARAMETERS, ALERT_RULE_REFERENCES, @@ -166,6 +168,7 @@ export { ALERT_RULE_INTERVAL, ALERT_RULE_LICENSE, ALERT_RULE_NAME, + ALERT_RULE_NAMESPACE_FIELD, ALERT_RULE_NOTE, ALERT_RULE_PARAMETERS, ALERT_RULE_REFERENCES, diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/experimental_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/experimental_rule_field_map.ts index f7b4de188ec11..473850a20d0b9 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/experimental_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/experimental_rule_field_map.ts @@ -5,7 +5,7 @@ * 2.0. */ -import * as Fields from '../../../common/technical_rule_data_field_names'; +import * as Fields from '../../technical_rule_data_field_names'; export const experimentalRuleFieldMap = { [Fields.ALERT_INSTANCE_ID]: { type: 'keyword', required: true }, diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index c81329baad572..72db005578e30 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { pickWithPatterns } from '../../../common/pick_with_patterns'; -import * as Fields from '../../../common/technical_rule_data_field_names'; + +import { pickWithPatterns } from '../../pick_with_patterns'; +import * as Fields from '../../technical_rule_data_field_names'; import { ecsFieldMap } from './ecs_field_map'; export const technicalRuleFieldMap = { diff --git a/x-pack/plugins/rule_registry/common/schemas/8.0.0/index.ts b/x-pack/plugins/rule_registry/common/schemas/8.0.0/index.ts new file mode 100644 index 0000000000000..269caa5c21fb6 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/schemas/8.0.0/index.ts @@ -0,0 +1,52 @@ +/* + * 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 { Values } from '@kbn/utility-types'; +import { + ALERT_INSTANCE_ID, + ALERT_UUID, + ALERT_RULE_CATEGORY, + ALERT_RULE_CONSUMER, + ALERT_RULE_EXECUTION_UUID, + ALERT_RULE_NAME, + ALERT_RULE_PRODUCER, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UUID, + SPACE_IDS, + ALERT_RULE_TAGS, + TIMESTAMP, +} from '@kbn/rule-data-utils'; + +/* DO NOT MODIFY THIS SCHEMA TO ADD NEW FIELDS. These types represent the alerts that shipped in 8.0.0. +Any changes to these types should be bug fixes so the types more accurately represent the alerts from 8.0.0. + +If you are adding new fields for a new release of Kibana, create a new sibling folder to this one +for the version to be released and add the field(s) to the schema in that folder. + +Then, update `../index.ts` to import from the new folder that has the latest schemas, add the +new schemas to the union of all alert schemas, and re-export the new schemas as the `*Latest` schemas. +*/ + +const commonAlertIdFieldNames = [ALERT_INSTANCE_ID, ALERT_UUID]; +export type CommonAlertIdFieldName800 = Values; + +export interface CommonAlertFields800 { + [ALERT_RULE_CATEGORY]: string; + [ALERT_RULE_CONSUMER]: string; + [ALERT_RULE_EXECUTION_UUID]: string; + [ALERT_RULE_NAME]: string; + [ALERT_RULE_PRODUCER]: string; + [ALERT_RULE_TYPE_ID]: string; + [ALERT_RULE_UUID]: string; + [SPACE_IDS]: string[]; + [ALERT_RULE_TAGS]: string[]; + [TIMESTAMP]: string; +} + +export type CommonAlertFieldName800 = keyof CommonAlertFields800; + +export type AlertWithCommonFields800 = T & CommonAlertFields800; diff --git a/x-pack/plugins/rule_registry/common/schemas/README.md b/x-pack/plugins/rule_registry/common/schemas/README.md new file mode 100644 index 0000000000000..7995cd8aab0e7 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/schemas/README.md @@ -0,0 +1 @@ +See x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/README.md for full description of versioned alert schema strategy and how it's used in the Security Solution's Detection Engine. diff --git a/x-pack/plugins/rule_registry/common/schemas/index.ts b/x-pack/plugins/rule_registry/common/schemas/index.ts new file mode 100644 index 0000000000000..e12c59617fd00 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/schemas/index.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 type { + CommonAlertFieldName800, + CommonAlertIdFieldName800, + CommonAlertFields800, + AlertWithCommonFields800, +} from './8.0.0'; + +export type { + CommonAlertFieldName800 as CommonAlertFieldNameLatest, + CommonAlertIdFieldName800 as CommonAlertIdFieldNameLatest, + CommonAlertFields800 as CommonAlertFieldsLatest, + AlertWithCommonFields800 as AlertWithCommonFieldsLatest, +}; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index c9912b0e29438..9f79768416e27 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -37,16 +37,13 @@ import { TIMESTAMP, VERSION, } from '../../common/technical_rule_data_field_names'; +import { CommonAlertFieldNameLatest, CommonAlertIdFieldNameLatest } from '../../common/schemas'; import { IRuleDataClient } from '../rule_data_client'; import { AlertExecutorOptionsWithExtraServices } from '../types'; import { fetchExistingAlerts } from './fetch_existing_alerts'; -import { - CommonAlertFieldName, - CommonAlertIdFieldName, - getCommonAlertFields, -} from './get_common_alert_fields'; +import { getCommonAlertFields } from './get_common_alert_fields'; -type ImplicitTechnicalFieldName = CommonAlertFieldName | CommonAlertIdFieldName; +type ImplicitTechnicalFieldName = CommonAlertFieldNameLatest | CommonAlertIdFieldNameLatest; type ExplicitTechnicalAlertFields = Partial< Omit diff --git a/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts b/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts index db8c56f84b2c4..a4f429c634dc9 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { Values } from '@kbn/utility-types'; import { - ALERT_INSTANCE_ID, - ALERT_UUID, ALERT_RULE_CATEGORY, ALERT_RULE_CONSUMER, ALERT_RULE_EXECUTION_UUID, @@ -22,30 +19,11 @@ import { } from '@kbn/rule-data-utils'; import { AlertExecutorOptions } from '../../../alerting/server'; -import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; - -const commonAlertFieldNames = [ - ALERT_RULE_CATEGORY, - ALERT_RULE_CONSUMER, - ALERT_RULE_EXECUTION_UUID, - ALERT_RULE_NAME, - ALERT_RULE_PRODUCER, - ALERT_RULE_TYPE_ID, - ALERT_RULE_UUID, - SPACE_IDS, - ALERT_RULE_TAGS, - TIMESTAMP, -]; -export type CommonAlertFieldName = Values; - -const commonAlertIdFieldNames = [ALERT_INSTANCE_ID, ALERT_UUID]; -export type CommonAlertIdFieldName = Values; - -export type CommonAlertFields = Pick; +import { CommonAlertFieldsLatest } from '../../common/schemas'; export const getCommonAlertFields = ( options: AlertExecutorOptions -): CommonAlertFields => { +): CommonAlertFieldsLatest => { return { [ALERT_RULE_CATEGORY]: options.rule.ruleTypeName, [ALERT_RULE_CONSUMER]: options.rule.consumer, diff --git a/x-pack/plugins/rule_registry/server/utils/persistence_types.ts b/x-pack/plugins/rule_registry/server/utils/persistence_types.ts index 70e37dd20e34d..5c80d60ee5118 100644 --- a/x-pack/plugins/rule_registry/server/utils/persistence_types.ts +++ b/x-pack/plugins/rule_registry/server/utils/persistence_types.ts @@ -17,6 +17,7 @@ import { import { WithoutReservedActionGroups } from '../../../alerting/common'; import { IRuleDataClient } from '../rule_data_client'; import { BulkResponseErrorAggregation } from './utils'; +import { AlertWithCommonFieldsLatest } from '../../common/schemas'; export type PersistenceAlertService = ( alerts: Array<{ @@ -27,7 +28,7 @@ export type PersistenceAlertService = ( ) => Promise>; export interface PersistenceAlertServiceResult { - createdAlerts: Array; + createdAlerts: Array & { _id: string; _index: string }>; errors: BulkResponseErrorAggregation; } diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/8.0.0/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/8.0.0/index.ts new file mode 100644 index 0000000000000..f695aad1eb125 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/8.0.0/index.ts @@ -0,0 +1,178 @@ +/* + * 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 { + ALERT_BUILDING_BLOCK_TYPE, + ALERT_REASON, + ALERT_RISK_SCORE, + ALERT_RULE_AUTHOR, + ALERT_RULE_CONSUMER, + ALERT_RULE_CREATED_AT, + ALERT_RULE_CREATED_BY, + ALERT_RULE_DESCRIPTION, + ALERT_RULE_ENABLED, + ALERT_RULE_FROM, + ALERT_RULE_INTERVAL, + ALERT_RULE_LICENSE, + ALERT_RULE_NAME, + ALERT_RULE_NAMESPACE_FIELD, + ALERT_RULE_NOTE, + ALERT_RULE_PARAMETERS, + ALERT_RULE_REFERENCES, + ALERT_RULE_RULE_ID, + ALERT_RULE_RULE_NAME_OVERRIDE, + ALERT_RULE_TAGS, + ALERT_RULE_TO, + ALERT_RULE_TYPE, + ALERT_RULE_UPDATED_AT, + ALERT_RULE_UPDATED_BY, + ALERT_RULE_UUID, + ALERT_RULE_VERSION, + ALERT_SEVERITY, + ALERT_STATUS, + ALERT_UUID, + ALERT_WORKFLOW_STATUS, + EVENT_KIND, + SPACE_IDS, + TIMESTAMP, +} from '@kbn/rule-data-utils'; +// TODO: Create and import 8.0.0 versioned ListArray schema +import { ListArray } from '@kbn/securitysolution-io-ts-list-types'; +// TODO: Create and import 8.0.0 versioned alerting-types schemas +import { + RiskScoreMapping, + SeverityMapping, + Threats, + Type, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { + ALERT_ANCESTORS, + ALERT_DEPTH, + ALERT_ORIGINAL_TIME, + ALERT_RULE_ACTIONS, + ALERT_RULE_EXCEPTIONS_LIST, + ALERT_RULE_FALSE_POSITIVES, + ALERT_GROUP_ID, + ALERT_GROUP_INDEX, + ALERT_RULE_IMMUTABLE, + ALERT_RULE_MAX_SIGNALS, + ALERT_RULE_RISK_SCORE_MAPPING, + ALERT_RULE_SEVERITY_MAPPING, + ALERT_RULE_THREAT, + ALERT_RULE_THROTTLE, + ALERT_RULE_TIMELINE_ID, + ALERT_RULE_TIMELINE_TITLE, + ALERT_RULE_TIMESTAMP_OVERRIDE, +} from '../../../../field_maps/field_names'; +// TODO: Create and import 8.0.0 versioned RuleAlertAction type +import { RuleAlertAction, SearchTypes } from '../../../types'; +import { AlertWithCommonFields800 } from '../../../../../../rule_registry/common/schemas/8.0.0'; + +/* DO NOT MODIFY THIS SCHEMA TO ADD NEW FIELDS. These types represent the alerts that shipped in 8.0.0. +Any changes to these types should be bug fixes so the types more accurately represent the alerts from 8.0.0. + +If you are adding new fields for a new release of Kibana, create a new sibling folder to this one +for the version to be released and add the field(s) to the schema in that folder. + +Then, update `../index.ts` to import from the new folder that has the latest schemas, add the +new schemas to the union of all alert schemas, and re-export the new schemas as the `*Latest` schemas. +*/ + +export interface Ancestor800 { + rule: string | undefined; + id: string; + type: string; + index: string; + depth: number; +} + +export interface BaseFields800 { + [TIMESTAMP]: string; + [SPACE_IDS]: string[]; + [EVENT_KIND]: 'signal'; + [ALERT_ORIGINAL_TIME]: string | undefined; + // When we address https://github.com/elastic/kibana/issues/102395 and change ID generation logic, consider moving + // ALERT_UUID creation into buildAlert and keep ALERT_UUID with the rest of BaseFields fields. As of 8.2 though, + // ID generation logic is fragmented and it would be more confusing to put any of it in buildAlert + // [ALERT_UUID]: string; + [ALERT_RULE_CONSUMER]: string; + [ALERT_ANCESTORS]: Ancestor800[]; + [ALERT_STATUS]: string; + [ALERT_WORKFLOW_STATUS]: string; + [ALERT_DEPTH]: number; + [ALERT_REASON]: string; + [ALERT_BUILDING_BLOCK_TYPE]: string | undefined; + [ALERT_SEVERITY]: string; + [ALERT_RISK_SCORE]: number; + // TODO: version rule schemas and pull in 8.0.0 versioned rule schema to define alert rule parameters type + [ALERT_RULE_PARAMETERS]: { [key: string]: SearchTypes }; + [ALERT_RULE_ACTIONS]: RuleAlertAction[]; + [ALERT_RULE_AUTHOR]: string[]; + [ALERT_RULE_CREATED_AT]: string; + [ALERT_RULE_CREATED_BY]: string; + [ALERT_RULE_DESCRIPTION]: string; + [ALERT_RULE_ENABLED]: boolean; + [ALERT_RULE_EXCEPTIONS_LIST]: ListArray; + [ALERT_RULE_FALSE_POSITIVES]: string[]; + [ALERT_RULE_FROM]: string; + [ALERT_RULE_IMMUTABLE]: boolean; + [ALERT_RULE_INTERVAL]: string; + [ALERT_RULE_LICENSE]: string | undefined; + [ALERT_RULE_MAX_SIGNALS]: number; + [ALERT_RULE_NAME]: string; + [ALERT_RULE_NAMESPACE_FIELD]: string | undefined; + [ALERT_RULE_NOTE]: string | undefined; + [ALERT_RULE_REFERENCES]: string[]; + [ALERT_RULE_RISK_SCORE_MAPPING]: RiskScoreMapping; + [ALERT_RULE_RULE_ID]: string; + [ALERT_RULE_RULE_NAME_OVERRIDE]: string | undefined; + [ALERT_RULE_SEVERITY_MAPPING]: SeverityMapping; + [ALERT_RULE_TAGS]: string[]; + [ALERT_RULE_THREAT]: Threats; + [ALERT_RULE_THROTTLE]: string | undefined; + [ALERT_RULE_TIMELINE_ID]: string | undefined; + [ALERT_RULE_TIMELINE_TITLE]: string | undefined; + [ALERT_RULE_TIMESTAMP_OVERRIDE]: string | undefined; + [ALERT_RULE_TO]: string; + [ALERT_RULE_TYPE]: Type; + [ALERT_RULE_UPDATED_AT]: string; + [ALERT_RULE_UPDATED_BY]: string; + [ALERT_RULE_UUID]: string; + [ALERT_RULE_VERSION]: number; + 'kibana.alert.rule.risk_score': number; + 'kibana.alert.rule.severity': string; + 'kibana.alert.rule.building_block_type': string | undefined; + [key: string]: SearchTypes; +} + +// This type is used after the alert UUID is generated and stored in the _id and ALERT_UUID fields +export interface WrappedFields800 { + _id: string; + _index: string; + _source: T & { [ALERT_UUID]: string }; +} + +export interface EqlBuildingBlockFields800 extends BaseFields800 { + [ALERT_GROUP_ID]: string; + [ALERT_GROUP_INDEX]: number; + [ALERT_BUILDING_BLOCK_TYPE]: 'default'; +} + +export interface EqlShellFields800 extends BaseFields800 { + [ALERT_GROUP_ID]: string; + [ALERT_UUID]: string; +} + +export type EqlBuildingBlockAlert800 = AlertWithCommonFields800; + +export type EqlShellAlert800 = AlertWithCommonFields800; + +export type GenericAlert800 = AlertWithCommonFields800; + +// This is the type of the final generated alert including base fields, common fields +// added by the alertWithPersistence function, and arbitrary fields copied from source documents +export type DetectionAlert800 = GenericAlert800 | EqlShellAlert800 | EqlBuildingBlockAlert800; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/README.md b/x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/README.md new file mode 100644 index 0000000000000..c7acf84813f08 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/README.md @@ -0,0 +1,54 @@ +# Summary + +Original PR: https://github.com/elastic/kibana/pull/127218 +The goal here is to create a system of schemas that are: + +- Easy to read +- Usable historical records of alert schemas from previous Kibana versions +- Accurate for every field +- Usable on both server and client side + +# Motivation - Development speed and quality + +We have already run into one bug (https://github.com/elastic/kibana/issues/125885) where a required field was not populated in some alert documents. Once a bug ships that creates documents incorrectly, any fix requires user action to initiate a re-index of the alerts in addition to the developer time to create and validate the fix. The changes proposed here would catch this bug at compile time. These sorts of bugs become harder to catch as the schema evolves over time and fields get added, removed, and changed. Keeping the schemas separated by version will help reduce the risk of repeated schema changes over time causing fields to be incorrectly included in or omitted from alert documents. + +We are also spending more time than necessary communicating details of the alerts schema over Slack and Zoom. It will be far more efficient for the code to clearly communicate more details about the alert schema. With a more comprehensive static schema, the knowledge will transfer to new developers more efficiently. + +Static types are a powerful tool for ensuring code correctness. However, each deviation of the static type from the actual runtime structure adds places where developers may need to cast, assert, or use conditional logic to satisfy the compiler. The current static types require frequent workarounds when the static types don't match what developers know or believe is true about the runtime type of the alert documents. These runtime workarounds establish patterns that evade the type system - costing developer time to create and maintain in addition to increasing the risk of bugs due to the additional complexity. Accurate static types are excellent documentation of the data structures we use but it's crucial that the static types are comprehensive to minimize cases where runtime checks are needed. + +# Structure - Common Alert Schema Directory + +The schemas in this directory have 2 primary purposes: (1) separate the alert document schemas from the FieldMaps, and (2) set up a code structure that enables easy versioning of alert schemas. During the Detection Engine migration to the rule registry we used the FieldMaps to define the alert schema, but ended up with numerous type casts and some bugs in the process. This common directory stores the various alert schemas by Kibana version. + +x-pack/plugins/security_solution/common/detection_engine/schemas/alerts initially contains index.ts and one folder, 8.0.0. index.ts imports the schemas from 8.0.0 and re-exports them as ...Latest, denoting that those are the "write" schemas. The reason for this is that as we add new schemas, there are many places server side where we want to ensure that we're writing the latest alert schema. By having index.ts re-export 8.0.0 schemas, when we add make a new alert schema in the future (e.g. adding an additional field in 8.x) we can simply update index.ts to re-export the new schema instead of the previous schema. index.ts also exports a DetectionAlert which is the "read" schema - this type will be maintained as a union of all versioned alert schemas, which is needed to accurately type alerts that are read from the alerts index. + +## Reading vs writing alerts + +When writing code that deals with creating a new alert document, always use the schema from alerts/index.ts, not from a specific version folder. This way when the schema is updated in the future, your code will automatically use the latest alert schema and the static type system will tell us if code is writing alerts that don't conform to the new schema. + +When writing code that deals with reading alerts, it must be able to handle alerts from any schema version. The "read schema" in index.ts DetectionAlert is a union of all of the versioned alert schemas since a valid alert from the .alerts index could be from any version. Initially there is only one versioned schema, so DetectionAlert is identical to DetectionAlert800. + +Generally, Solution code should not be directly importing alert schemas from a specific version. Alert writing code should use the latest schema, and alert reading code should use the union of all schemas. + +## Adding new schemas + +In the future, when we want to add new fields, we should create a new folder named with the version the field is being added in, create the updated schema in the new folder, and update index.ts to re-export the schemas for the new version instead of the previous version. Also, update the "read schema" DetectionAlert type in index.ts to include the new schema in addition to the previous schemas. The schema in the new version folder can either build on the previous version, e.g. 8.4.0 could import the schema from 8.0.0 and simply add a few new fields, or for larger changes the new version could build the schema from scratch. Old schemas should not change when new fields are added! + +## Changing existing schemas + +The schema in the 8.0.0 folder, and any future versioned folders after the version is released, should not be updated with new fields. Old schemas should only be updated if a bug is discovered and it is determined that the schema does not accurately represent the alert documents that were actually written by that version, e.g. if a field is typed as string in the schema but was actually written as string[]. The goal of these schemas is to represent documents accurately as they were written and since we aren't changing the documents that already exist, the schema should generally not change. + +## No changes + +If a version of Kibana makes no changes to the schema, a new folder for that version is not needed. + +# Design decisions + +- Why not combine the FieldMaps and alert schema, creating a single structure that can define both? + FieldMaps are integrated tightly with Elasticsearch mappings already, with minimal support for accurate TypeScript types of the fields. We want to avoid adding tons of extra information in to the FieldMaps that would not be used for the Elasticsearch mappings. Instead later we can write a bit of code to ensure that the alert schemas are compatible with the FieldMap schemas, essentially ensuring that the alert schemas extend the FieldMap schemas. + +- Why is | undefined used in field definitions instead of making fields optional? + Making all fields required, but some | undefined in the type, helps ensure that we don't forget to copy over fields that may be undefined. If the field is optional, e.g. [ALERT_RULE_NOTE]?: string, then the compiler won't complain if the field is completely left out when we build the alert document. However, when it's defined as [ALERT_RULE_NOTE]: string | undefined instead, the field must be explicitly provided when creating an object literal of the alert type - even if the value is undefined. This makes it harder to forget to populate all of the fields. This can be seen in build_alert.ts where removing one of the optional fields from the return value results in a compiler error. + +- Why do we need to version the schemas instead of adding all new fields as | undefined? + Adding new fields as | undefined when they're actually required reduces the accuracy of the schema, which makes it less useful and harder to work with. If we decide to add a new field and always populate it going forward then accurately representing that in the static type makes it easier to work with alerts during the alert creation process. When a field is typed as | undefined but a developer knows that it should always exist, it encourages patterns that fight the type system through type-casting, assertions, using ?? , etc. This makes the code harder to read, harder to reason about, and thus harder to maintain because the knowledge of "this field is typed as | undefined but actually always exists here" is not represented in the code and only lives in developers minds. Versioned alert schemas aim to turn the static types into an asset that precisely documents what the alert document structure is. diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/index.ts new file mode 100644 index 0000000000000..51b5c505b817a --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/alerts/index.ts @@ -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 type { + Ancestor800, + BaseFields800, + DetectionAlert800, + WrappedFields800, + EqlBuildingBlockFields800, + EqlShellFields800, +} from './8.0.0'; + +// When new Alert schemas are created for new Kibana versions, add the DetectionAlert type from the new version +// here, e.g. `export type DetectionAlert = DetectionAlert800 | DetectionAlert820` if a new schema is created in 8.2.0 +export type DetectionAlert = DetectionAlert800; + +export type { + Ancestor800 as AncestorLatest, + BaseFields800 as BaseFieldsLatest, + DetectionAlert800 as DetectionAlertLatest, + WrappedFields800 as WrappedFieldsLatest, + EqlBuildingBlockFields800 as EqlBuildingBlockFieldsLatest, + EqlShellFields800 as EqlShellFieldsLatest, +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 950460580925e..69a748c3bd95c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -219,7 +219,7 @@ const { patch: eqlPatchParams, response: eqlResponseParams, } = buildAPISchemas(eqlRuleParams); -export { eqlCreateParams }; +export { eqlCreateParams, eqlResponseParams }; const threatMatchRuleParams = { required: { diff --git a/x-pack/plugins/security_solution/common/field_maps/alerts.ts b/x-pack/plugins/security_solution/common/field_maps/8.0.0/alerts.ts similarity index 97% rename from x-pack/plugins/security_solution/common/field_maps/alerts.ts rename to x-pack/plugins/security_solution/common/field_maps/8.0.0/alerts.ts index 330f85a8a8343..0694c15551efc 100644 --- a/x-pack/plugins/security_solution/common/field_maps/alerts.ts +++ b/x-pack/plugins/security_solution/common/field_maps/8.0.0/alerts.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { FieldMap } from '../../../rule_registry/common/field_map'; - -export const alertsFieldMap: FieldMap = { +export const alertsFieldMap = { 'kibana.alert.ancestors': { type: 'object', array: true, diff --git a/x-pack/plugins/security_solution/common/field_maps/8.0.0/index.ts b/x-pack/plugins/security_solution/common/field_maps/8.0.0/index.ts new file mode 100644 index 0000000000000..e293dac663816 --- /dev/null +++ b/x-pack/plugins/security_solution/common/field_maps/8.0.0/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertsFieldMap, alertsFieldMap } from './alerts'; +import { RulesFieldMap, rulesFieldMap } from './rules'; +export type { AlertsFieldMap, RulesFieldMap }; +export { alertsFieldMap, rulesFieldMap }; diff --git a/x-pack/plugins/security_solution/common/field_maps/rules.ts b/x-pack/plugins/security_solution/common/field_maps/8.0.0/rules.ts similarity index 100% rename from x-pack/plugins/security_solution/common/field_maps/rules.ts rename to x-pack/plugins/security_solution/common/field_maps/8.0.0/rules.ts diff --git a/x-pack/plugins/security_solution/common/field_maps/field_names.ts b/x-pack/plugins/security_solution/common/field_maps/field_names.ts index 1cb40063202d0..164f87b4e7b5c 100644 --- a/x-pack/plugins/security_solution/common/field_maps/field_names.ts +++ b/x-pack/plugins/security_solution/common/field_maps/field_names.ts @@ -23,6 +23,18 @@ export const ALERT_ORIGINAL_EVENT_KIND = `${ALERT_ORIGINAL_EVENT}.kind` as const export const ALERT_ORIGINAL_EVENT_MODULE = `${ALERT_ORIGINAL_EVENT}.module` as const; export const ALERT_ORIGINAL_EVENT_TYPE = `${ALERT_ORIGINAL_EVENT}.type` as const; +export const ALERT_RULE_ACTIONS = `${ALERT_RULE_NAMESPACE}.actions` as const; +export const ALERT_RULE_EXCEPTIONS_LIST = `${ALERT_RULE_NAMESPACE}.exceptions_list` as const; +export const ALERT_RULE_FALSE_POSITIVES = `${ALERT_RULE_NAMESPACE}.false_positives` as const; +export const ALERT_RULE_IMMUTABLE = `${ALERT_RULE_NAMESPACE}.immutable` as const; +export const ALERT_RULE_MAX_SIGNALS = `${ALERT_RULE_NAMESPACE}.max_signals` as const; +export const ALERT_RULE_META = `${ALERT_RULE_NAMESPACE}.meta` as const; +export const ALERT_RULE_RISK_SCORE_MAPPING = `${ALERT_RULE_NAMESPACE}.risk_score_mapping` as const; +export const ALERT_RULE_SEVERITY_MAPPING = `${ALERT_RULE_NAMESPACE}.severity_mapping` as const; +export const ALERT_RULE_THREAT = `${ALERT_RULE_NAMESPACE}.threat` as const; export const ALERT_RULE_THRESHOLD = `${ALERT_RULE_NAMESPACE}.threshold` as const; export const ALERT_RULE_THRESHOLD_FIELD = `${ALERT_RULE_THRESHOLD}.field` as const; +export const ALERT_RULE_THROTTLE = `${ALERT_RULE_NAMESPACE}.throttle` as const; export const ALERT_RULE_TIMELINE_ID = `${ALERT_RULE_NAMESPACE}.timeline_id` as const; +export const ALERT_RULE_TIMELINE_TITLE = `${ALERT_RULE_NAMESPACE}.timeline_title` as const; +export const ALERT_RULE_TIMESTAMP_OVERRIDE = `${ALERT_RULE_NAMESPACE}.timestamp_override` as const; diff --git a/x-pack/plugins/security_solution/common/field_maps/index.ts b/x-pack/plugins/security_solution/common/field_maps/index.ts index e293dac663816..260b56ffe9141 100644 --- a/x-pack/plugins/security_solution/common/field_maps/index.ts +++ b/x-pack/plugins/security_solution/common/field_maps/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AlertsFieldMap, alertsFieldMap } from './alerts'; -import { RulesFieldMap, rulesFieldMap } from './rules'; +import { AlertsFieldMap, alertsFieldMap } from './8.0.0/alerts'; +import { RulesFieldMap, rulesFieldMap } from './8.0.0/rules'; export type { AlertsFieldMap, RulesFieldMap }; export { alertsFieldMap, rulesFieldMap }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts index 394e431203a24..d4cc8385429da 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -10,15 +10,15 @@ import { Alert } from '../../../../../alerting/server'; import { expandDottedObject } from '../../../../common/utils/expand_dotted'; import { RuleParams } from '../schemas/rule_schemas'; import aadFieldConversion from '../routes/index/signal_aad_mapping.json'; -import { isRACAlert } from '../signals/utils'; -import { RACAlert } from '../rule_types/types'; +import { isDetectionAlert } from '../signals/utils'; +import { DetectionAlert } from '../../../../common/detection_engine/schemas/alerts'; export type NotificationRuleTypeParams = RuleParams & { id: string; name: string; }; -const convertToLegacyAlert = (alert: RACAlert) => +const convertToLegacyAlert = (alert: DetectionAlert) => Object.entries(aadFieldConversion).reduce((acc, [legacyField, aadField]) => { const val = alert[aadField]; if (val != null) { @@ -36,7 +36,7 @@ const convertToLegacyAlert = (alert: RACAlert) => */ const formatAlertsForNotificationActions = (alerts: unknown[]): unknown[] => { return alerts.map((alert) => - isRACAlert(alert) + isDetectionAlert(alert) ? { ...expandDottedObject(convertToLegacyAlert(alert)), ...expandDottedObject(alert), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts index efe02f2ec6b7a..60a3be4b5345f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts @@ -9,17 +9,21 @@ import { performance } from 'perf_hooks'; import { isEmpty } from 'lodash'; import { Logger } from 'kibana/server'; -import { BaseHit } from '../../../../../common/detection_engine/types'; import { BuildRuleMessage } from '../../signals/rule_messages'; import { makeFloatString } from '../../signals/utils'; import { RefreshTypes } from '../../types'; import { PersistenceAlertService } from '../../../../../../rule_registry/server'; +import { + BaseFieldsLatest, + WrappedFieldsLatest, +} from '../../../../../common/detection_engine/schemas/alerts'; +import { AlertWithCommonFieldsLatest } from '../../../../../../rule_registry/common/schemas'; -export interface GenericBulkCreateResponse { +export interface GenericBulkCreateResponse { success: boolean; bulkCreateDuration: string; createdItemsCount: number; - createdItems: Array; + createdItems: Array & { _id: string; _index: string }>; errors: string[]; } @@ -30,8 +34,8 @@ export const bulkCreateFactory = buildRuleMessage: BuildRuleMessage, refreshForBulkCreate: RefreshTypes ) => - async >( - wrappedDocs: Array> + async ( + wrappedDocs: Array> ): Promise> => { if (wrappedDocs.length === 0) { return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index a39c5f77bc289..5768306999f79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -65,6 +65,7 @@ describe('buildAlert', () => { const timestamp = alert[TIMESTAMP]; const expected = { [TIMESTAMP]: timestamp, + [EVENT_KIND]: 'signal', [SPACE_IDS]: [SPACE_ID], [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [ALERT_ANCESTORS]: [ @@ -235,6 +236,7 @@ describe('buildAlert', () => { const expected = { [TIMESTAMP]: timestamp, + [EVENT_KIND]: 'signal', [SPACE_IDS]: [SPACE_ID], [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [ALERT_ANCESTORS]: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index 07f2dfa31015c..e1259be5062a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -8,14 +8,34 @@ import { ALERT_REASON, ALERT_RISK_SCORE, + ALERT_RULE_AUTHOR, ALERT_RULE_CONSUMER, - ALERT_RULE_NAMESPACE, + ALERT_RULE_CREATED_AT, + ALERT_RULE_CREATED_BY, + ALERT_RULE_DESCRIPTION, + ALERT_RULE_ENABLED, + ALERT_RULE_FROM, + ALERT_RULE_INTERVAL, + ALERT_RULE_LICENSE, + ALERT_RULE_NAME, + ALERT_RULE_NAMESPACE_FIELD, + ALERT_RULE_NOTE, ALERT_RULE_PARAMETERS, + ALERT_RULE_REFERENCES, + ALERT_RULE_RULE_ID, + ALERT_RULE_RULE_NAME_OVERRIDE, + ALERT_RULE_TAGS, + ALERT_RULE_TO, + ALERT_RULE_TYPE, + ALERT_RULE_UPDATED_AT, + ALERT_RULE_UPDATED_BY, ALERT_RULE_UUID, + ALERT_RULE_VERSION, ALERT_SEVERITY, ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_WORKFLOW_STATUS, + EVENT_KIND, SPACE_IDS, TIMESTAMP, } from '@kbn/rule-data-utils'; @@ -23,14 +43,13 @@ import { flattenWithPrefix } from '@kbn/securitysolution-rules'; import { createHash } from 'crypto'; -import { Ancestor, BaseSignalHit, SimpleHit, ThresholdResult } from '../../../signals/types'; +import { BaseSignalHit, SimpleHit, ThresholdResult } from '../../../signals/types'; import { getField, getValidDateFromDoc, - isWrappedRACAlert, + isWrappedDetectionAlert, isWrappedSignalHit, } from '../../../signals/utils'; -import { RACAlert } from '../../types'; import { SERVER_APP_ID } from '../../../../../../common/constants'; import { SearchTypes } from '../../../../telemetry/types'; import { @@ -40,6 +59,19 @@ import { ALERT_THRESHOLD_RESULT, ALERT_ORIGINAL_EVENT, ALERT_BUILDING_BLOCK_TYPE, + ALERT_RULE_ACTIONS, + ALERT_RULE_THROTTLE, + ALERT_RULE_TIMELINE_ID, + ALERT_RULE_TIMELINE_TITLE, + ALERT_RULE_META, + ALERT_RULE_TIMESTAMP_OVERRIDE, + ALERT_RULE_FALSE_POSITIVES, + ALERT_RULE_MAX_SIGNALS, + ALERT_RULE_RISK_SCORE_MAPPING, + ALERT_RULE_SEVERITY_MAPPING, + ALERT_RULE_THREAT, + ALERT_RULE_EXCEPTIONS_LIST, + ALERT_RULE_IMMUTABLE, } from '../../../../../../common/field_maps/field_names'; import { CompleteRule, RuleParams } from '../../../schemas/rule_schemas'; import { @@ -48,13 +80,18 @@ import { } from '../../../schemas/rule_converters'; import { transformTags } from '../../../routes/rules/utils'; import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; +import { + AncestorLatest, + BaseFieldsLatest, +} from '../../../../../../common/detection_engine/schemas/alerts'; -export const generateAlertId = (alert: RACAlert) => { +export const generateAlertId = (alert: BaseFieldsLatest) => { return createHash('sha256') .update( - (alert[ALERT_ANCESTORS] as Ancestor[]) - .reduce((acc, ancestor) => acc.concat(ancestor.id, ancestor.index), '') - .concat(alert[ALERT_RULE_UUID] as string) + alert[ALERT_ANCESTORS].reduce( + (acc, ancestor) => acc.concat(ancestor.id, ancestor.index), + '' + ).concat(alert[ALERT_RULE_UUID]) ) .digest('hex'); }; @@ -64,17 +101,15 @@ export const generateAlertId = (alert: RACAlert) => { * alert's ancestors array. * @param doc The parent event */ -export const buildParent = (doc: SimpleHit): Ancestor => { - const isSignal: boolean = isWrappedSignalHit(doc) || isWrappedRACAlert(doc); - const parent: Ancestor = { +export const buildParent = (doc: SimpleHit): AncestorLatest => { + const isSignal: boolean = isWrappedSignalHit(doc) || isWrappedDetectionAlert(doc); + const parent: AncestorLatest = { id: doc._id, type: isSignal ? 'signal' : 'event', index: doc._index, - depth: isSignal ? getField(doc, ALERT_DEPTH) ?? 1 : 0, + depth: isSignal ? (getField(doc, ALERT_DEPTH) as number | undefined) ?? 1 : 0, + rule: isSignal ? (getField(doc, ALERT_RULE_UUID) as string) : undefined, }; - if (isSignal) { - parent.rule = getField(doc, ALERT_RULE_UUID); - } return parent; }; @@ -83,9 +118,10 @@ export const buildParent = (doc: SimpleHit): Ancestor => { * creating an array of N+1 ancestors. * @param doc The parent event for which to extend the ancestry. */ -export const buildAncestors = (doc: SimpleHit): Ancestor[] => { +export const buildAncestors = (doc: SimpleHit): AncestorLatest[] => { const newAncestor = buildParent(doc); - const existingAncestors: Ancestor[] = getField(doc, ALERT_ANCESTORS) ?? []; + const existingAncestors: AncestorLatest[] = + (getField(doc, ALERT_ANCESTORS) as AncestorLatest[] | undefined) ?? []; return [...existingAncestors, newAncestor]; }; @@ -106,10 +142,13 @@ export const buildAlert = ( severityOverride: string; riskScoreOverride: number; } -): RACAlert => { +): BaseFieldsLatest => { const parents = docs.map(buildParent); const depth = parents.reduce((acc, parent) => Math.max(parent.depth, acc), 0) + 1; - const ancestors = docs.reduce((acc: Ancestor[], doc) => acc.concat(buildAncestors(doc)), []); + const ancestors = docs.reduce( + (acc: AncestorLatest[], doc) => acc.concat(buildAncestors(doc)), + [] + ); const { output_index: outputIndex, ...commonRuleParams } = commonParamsCamelToSnake( completeRule.ruleParams @@ -133,35 +172,67 @@ export const buildAlert = ( updatedAt, } = completeRule.ruleConfig; + const params = completeRule.ruleParams; + + const originalTime = getValidDateFromDoc({ + doc: docs[0], + timestampOverride: undefined, + }); + return { [TIMESTAMP]: new Date().toISOString(), - [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [SPACE_IDS]: spaceId != null ? [spaceId] : [], + [EVENT_KIND]: 'signal', + [ALERT_ORIGINAL_TIME]: originalTime?.toISOString(), + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [ALERT_ANCESTORS]: ancestors, [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [ALERT_WORKFLOW_STATUS]: 'open', [ALERT_DEPTH]: depth, [ALERT_REASON]: reason, - [ALERT_BUILDING_BLOCK_TYPE]: completeRule.ruleParams.buildingBlockType, - [ALERT_SEVERITY]: overrides?.severityOverride ?? completeRule.ruleParams.severity, - [ALERT_RISK_SCORE]: overrides?.riskScoreOverride ?? completeRule.ruleParams.riskScore, + [ALERT_BUILDING_BLOCK_TYPE]: params.buildingBlockType, + [ALERT_SEVERITY]: overrides?.severityOverride ?? params.severity, + [ALERT_RISK_SCORE]: overrides?.riskScoreOverride ?? params.riskScore, [ALERT_RULE_PARAMETERS]: ruleParamsSnakeCase, - ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { - uuid: completeRule.alertId, - actions: actions.map(transformAlertToRuleAction), - created_at: createdAt.toISOString(), - created_by: createdBy ?? '', - enabled, - interval: schedule.interval, - name: overrides?.nameOverride ?? name, - tags: transformTags(tags), - throttle: throttle ?? undefined, - updated_at: updatedAt.toISOString(), - updated_by: updatedBy ?? '', - type: completeRule.ruleParams.type, - ...commonRuleParams, - }), - } as unknown as RACAlert; + [ALERT_RULE_ACTIONS]: actions.map(transformAlertToRuleAction), + [ALERT_RULE_AUTHOR]: params.author, + [ALERT_RULE_CREATED_AT]: createdAt.toISOString(), + [ALERT_RULE_CREATED_BY]: createdBy ?? '', + [ALERT_RULE_DESCRIPTION]: params.description, + [ALERT_RULE_ENABLED]: enabled, + [ALERT_RULE_EXCEPTIONS_LIST]: params.exceptionsList, + [ALERT_RULE_FALSE_POSITIVES]: params.falsePositives, + [ALERT_RULE_FROM]: params.from, + [ALERT_RULE_IMMUTABLE]: params.immutable, + [ALERT_RULE_INTERVAL]: schedule.interval, + [ALERT_RULE_LICENSE]: params.license, + [ALERT_RULE_MAX_SIGNALS]: params.maxSignals, + [ALERT_RULE_NAME]: overrides?.nameOverride ?? name, + [ALERT_RULE_NAMESPACE_FIELD]: params.namespace, + [ALERT_RULE_NOTE]: params.note, + [ALERT_RULE_REFERENCES]: params.references, + [ALERT_RULE_RISK_SCORE_MAPPING]: params.riskScoreMapping, + [ALERT_RULE_RULE_ID]: params.ruleId, + [ALERT_RULE_RULE_NAME_OVERRIDE]: params.ruleNameOverride, + [ALERT_RULE_SEVERITY_MAPPING]: params.severityMapping, + [ALERT_RULE_TAGS]: transformTags(tags), + [ALERT_RULE_THREAT]: params.threat, + [ALERT_RULE_THROTTLE]: throttle ?? undefined, + [ALERT_RULE_TIMELINE_ID]: params.timelineId, + [ALERT_RULE_TIMELINE_TITLE]: params.timelineTitle, + [ALERT_RULE_TIMESTAMP_OVERRIDE]: params.timestampOverride, + [ALERT_RULE_TO]: params.to, + [ALERT_RULE_TYPE]: params.type, + [ALERT_RULE_UPDATED_AT]: updatedAt.toISOString(), + [ALERT_RULE_UPDATED_BY]: updatedBy ?? '', + [ALERT_RULE_UUID]: completeRule.alertId, + [ALERT_RULE_VERSION]: params.version, + ...flattenWithPrefix(ALERT_RULE_META, params.meta), + // These fields don't exist in the mappings, but leaving here for now to limit changes to the alert building logic + 'kibana.alert.rule.risk_score': params.riskScore, + 'kibana.alert.rule.severity': params.severity, + 'kibana.alert.rule.building_block_type': params.buildingBlockType, + }; }; const isThresholdResult = (thresholdResult: SearchTypes): thresholdResult is ThresholdResult => { @@ -178,12 +249,7 @@ export const additionalAlertFields = (doc: BaseSignalHit) => { if (thresholdResult != null && !isThresholdResult(thresholdResult)) { throw new Error(`threshold_result failed to validate: ${thresholdResult}`); } - const originalTime = getValidDateFromDoc({ - doc, - timestampOverride: undefined, - }); - const additionalFields: Record = { - [ALERT_ORIGINAL_TIME]: originalTime != null ? originalTime.toISOString() : undefined, + const additionalFields: Record = { ...(thresholdResult != null ? { [ALERT_THRESHOLD_RESULT]: thresholdResult } : {}), }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.ts index 26e0289732bfb..a991da87485a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.ts @@ -11,7 +11,6 @@ import { Logger } from 'kibana/server'; import type { ConfigType } from '../../../../../config'; import { Ancestor, SignalSource, SignalSourceHit } from '../../../signals/types'; -import { RACAlert, WrappedRACAlert } from '../../types'; import { buildAlert, buildAncestors, generateAlertId } from './build_alert'; import { buildBulkBody } from './build_bulk_body'; import { EqlSequence } from '../../../../../../common/detection_engine/types'; @@ -22,8 +21,13 @@ import { ALERT_BUILDING_BLOCK_TYPE, ALERT_GROUP_ID, ALERT_GROUP_INDEX, - ALERT_ORIGINAL_TIME, } from '../../../../../../common/field_maps/field_names'; +import { + BaseFieldsLatest, + EqlBuildingBlockFieldsLatest, + EqlShellFieldsLatest, + WrappedFieldsLatest, +} from '../../../../../../common/detection_engine/schemas/alerts'; /** * Takes N raw documents from ES that form a sequence and builds them into N+1 signals ready to be indexed - @@ -39,67 +43,70 @@ export const buildAlertGroupFromSequence = ( mergeStrategy: ConfigType['alertMergeStrategy'], spaceId: string | null | undefined, buildReasonMessage: BuildReasonMessage -): WrappedRACAlert[] => { +): Array> => { const ancestors: Ancestor[] = sequence.events.flatMap((event) => buildAncestors(event)); if (ancestors.some((ancestor) => ancestor?.rule === completeRule.alertId)) { return []; } - let buildingBlocks: RACAlert[] = []; + // The "building block" alerts start out as regular BaseFields. We'll add the group ID and index fields + // after creating the shell alert later on, since that's when the group ID is determined. + let baseAlerts: BaseFieldsLatest[] = []; try { - buildingBlocks = sequence.events.map((event) => ({ - ...buildBulkBody(spaceId, completeRule, event, mergeStrategy, [], false, buildReasonMessage), - [ALERT_BUILDING_BLOCK_TYPE]: 'default', - })); + baseAlerts = sequence.events.map((event) => + buildBulkBody(spaceId, completeRule, event, mergeStrategy, [], false, buildReasonMessage) + ); } catch (error) { logger.error(error); return []; } - const buildingBlockIds = generateBuildingBlockIds(buildingBlocks); - const wrappedBuildingBlocks: WrappedRACAlert[] = buildingBlocks.map((block, i) => ({ - _id: buildingBlockIds[i], - _index: '', - _source: { - ...block, - [ALERT_UUID]: buildingBlockIds[i], - }, - })); + // The ID of each building block alert depends on all of the other building blocks as well, + // so we generate the IDs after making all the BaseFields + const buildingBlockIds = generateBuildingBlockIds(baseAlerts); + const wrappedBaseFields: Array> = baseAlerts.map( + (block, i): WrappedFieldsLatest => ({ + _id: buildingBlockIds[i], + _index: '', + _source: { + ...block, + [ALERT_UUID]: buildingBlockIds[i], + }, + }) + ); // Now that we have an array of building blocks for the events in the sequence, // we can build the signal that links the building blocks together // and also insert the group id (which is also the "shell" signal _id) in each building block - const doc = buildAlertRoot(wrappedBuildingBlocks, completeRule, spaceId, buildReasonMessage); - const sequenceAlertId = generateAlertId(doc); - const sequenceAlert = { - _id: sequenceAlertId, + const shellAlert = buildAlertRoot(wrappedBaseFields, completeRule, spaceId, buildReasonMessage); + const sequenceAlert: WrappedFieldsLatest = { + _id: shellAlert[ALERT_UUID], _index: '', - _source: doc, + _source: shellAlert, }; - wrappedBuildingBlocks.forEach((block, i) => { - block._source[ALERT_GROUP_ID] = sequenceAlert._source[ALERT_GROUP_ID]; - block._source[ALERT_GROUP_INDEX] = i; - }); - - sequenceAlert._source[ALERT_UUID] = sequenceAlertId; + // Finally, we have the group id from the shell alert so we can convert the BaseFields into EqlBuildingBlocks + const wrappedBuildingBlocks = wrappedBaseFields.map( + (block, i): WrappedFieldsLatest => ({ + ...block, + _source: { + ...block._source, + [ALERT_BUILDING_BLOCK_TYPE]: 'default', + [ALERT_GROUP_ID]: shellAlert[ALERT_GROUP_ID], + [ALERT_GROUP_INDEX]: i, + }, + }) + ); return [...wrappedBuildingBlocks, sequenceAlert]; }; export const buildAlertRoot = ( - wrappedBuildingBlocks: WrappedRACAlert[], + wrappedBuildingBlocks: Array>, completeRule: CompleteRule, spaceId: string | null | undefined, buildReasonMessage: BuildReasonMessage -): RACAlert => { - const timestamps = wrappedBuildingBlocks - .sort( - (block1, block2) => - (block1._source[ALERT_ORIGINAL_TIME] as number) - - (block2._source[ALERT_ORIGINAL_TIME] as number) - ) - .map((alert) => alert._source[ALERT_ORIGINAL_TIME]); +): EqlShellFieldsLatest => { const mergedAlerts = objectArrayIntersection(wrappedBuildingBlocks.map((alert) => alert._source)); const reason = buildReasonMessage({ name: completeRule.ruleConfig.name, @@ -107,14 +114,12 @@ export const buildAlertRoot = ( mergedDoc: mergedAlerts as SignalSourceHit, }); const doc = buildAlert(wrappedBuildingBlocks, completeRule, spaceId, reason); + const alertId = generateAlertId(doc); return { ...mergedAlerts, - event: { - kind: 'signal', - }, ...doc, - [ALERT_ORIGINAL_TIME]: timestamps[0], - [ALERT_GROUP_ID]: generateAlertId(doc), + [ALERT_UUID]: alertId, + [ALERT_GROUP_ID]: alertId, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts index ef3d76be1df4b..a73471ec83ada 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { EVENT_KIND, TIMESTAMP } from '@kbn/rule-data-utils'; import { flattenWithPrefix } from '@kbn/securitysolution-rules'; import { BaseHit } from '../../../../../../common/detection_engine/types'; @@ -13,13 +12,13 @@ import type { ConfigType } from '../../../../../config'; import { BuildReasonMessage } from '../../../signals/reason_formatters'; import { getMergeStrategy } from '../../../signals/source_fields_merging/strategies'; import { BaseSignalHit, SignalSource, SignalSourceHit, SimpleHit } from '../../../signals/types'; -import { RACAlert } from '../../types'; import { additionalAlertFields, buildAlert } from './build_alert'; import { filterSource } from './filter_source'; import { CompleteRule, RuleParams } from '../../../schemas/rule_schemas'; import { buildRuleNameFromMapping } from '../../../signals/mappings/build_rule_name_from_mapping'; import { buildSeverityFromMapping } from '../../../signals/mappings/build_severity_from_mapping'; import { buildRiskScoreFromMapping } from '../../../signals/mappings/build_risk_score_from_mapping'; +import { BaseFieldsLatest } from '../../../../../../common/detection_engine/schemas/alerts'; const isSourceDoc = ( hit: SignalSourceHit @@ -51,7 +50,7 @@ export const buildBulkBody = ( ignoreFields: ConfigType['alertIgnoreFields'], applyOverrides: boolean, buildReasonMessage: BuildReasonMessage -): RACAlert => { +): BaseFieldsLatest => { const mergedDoc = getMergeStrategy(mergeStrategy)({ doc, ignoreFields }); const eventFields = buildEventTypeAlert(mergedDoc); const filteredSource = filterSource(mergedDoc); @@ -88,8 +87,6 @@ export const buildBulkBody = ( ...eventFields, ...buildAlert([mergedDoc], completeRule, spaceId, reason, overrides), ...additionalAlertFields({ ...mergedDoc, _source: { ...mergedDoc._source, ...eventFields } }), - [EVENT_KIND]: 'signal', - [TIMESTAMP]: new Date().toISOString(), }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts index 473b0da1d58e9..f1fd9d3b83929 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts @@ -7,9 +7,8 @@ import { ALERT_THRESHOLD_RESULT } from '../../../../../../common/field_maps/field_names'; import { SignalSourceHit } from '../../../signals/types'; -import { RACAlert } from '../../types'; -export const filterSource = (doc: SignalSourceHit): Partial => { +export const filterSource = (doc: SignalSourceHit) => { const docSource = doc._source ?? {}; const { event, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/generate_building_block_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/generate_building_block_ids.ts index a603946b03d3b..f0ae5d7cdc6ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/generate_building_block_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/generate_building_block_ids.ts @@ -7,9 +7,8 @@ import { ALERT_RULE_UUID } from '@kbn/rule-data-utils'; import { createHash } from 'crypto'; +import { BaseFieldsLatest } from '../../../../../../common/detection_engine/schemas/alerts'; import { ALERT_ANCESTORS } from '../../../../../../common/field_maps/field_names'; -import { Ancestor } from '../../../signals/types'; -import { RACAlert } from '../../types'; /** * Generates unique doc ids for each building block signal within a sequence. The id of each building block @@ -17,17 +16,17 @@ import { RACAlert } from '../../types'; * (e.g. if multiple rules build sequences that share a common event/signal) will get a unique id per sequence. * @param buildingBlocks The full list of building blocks in the sequence. */ -export const generateBuildingBlockIds = (buildingBlocks: RACAlert[]): string[] => { +export const generateBuildingBlockIds = (buildingBlocks: BaseFieldsLatest[]): string[] => { const baseHashString = buildingBlocks.reduce( (baseString, block) => baseString .concat( - (block[ALERT_ANCESTORS] as Ancestor[]).reduce( + block[ALERT_ANCESTORS].reduce( (acc, ancestor) => acc.concat(ancestor.id, ancestor.index), '' ) ) - .concat(block[ALERT_RULE_UUID] as string), + .concat(block[ALERT_RULE_UUID]), '' ); return buildingBlocks.map((block, idx) => diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts index 81a4af31881fb..2c5c9cca02080 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts @@ -5,14 +5,19 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ALERT_UUID } from '@kbn/rule-data-utils'; import type { ConfigType } from '../../../../config'; -import { filterDuplicateSignals } from '../../signals/filter_duplicate_signals'; -import { SimpleHit, WrapHits } from '../../signals/types'; +import { SignalSource, SimpleHit } from '../../signals/types'; import { CompleteRule, RuleParams } from '../../schemas/rule_schemas'; import { generateId } from '../../signals/utils'; import { buildBulkBody } from './utils/build_bulk_body'; +import { BuildReasonMessage } from '../../signals/reason_formatters'; +import { + BaseFieldsLatest, + WrappedFieldsLatest, +} from '../../../../../common/detection_engine/schemas/alerts'; export const wrapHitsFactory = ({ @@ -25,9 +30,12 @@ export const wrapHitsFactory = ignoreFields: ConfigType['alertIgnoreFields']; mergeStrategy: ConfigType['alertMergeStrategy']; spaceId: string | null | undefined; - }): WrapHits => - (events, buildReasonMessage) => { - const wrappedDocs = events.map((event) => { + }) => + ( + events: Array>, + buildReasonMessage: BuildReasonMessage + ): Array> => { + const wrappedDocs = events.map((event): WrappedFieldsLatest => { const id = generateId( event._index, event._id, @@ -51,6 +59,10 @@ export const wrapHitsFactory = }, }; }); - - return filterDuplicateSignals(completeRule.alertId, wrappedDocs, true); + return wrappedDocs.filter( + (doc) => + !doc._source['kibana.alert.ancestors'].some( + (ancestor) => ancestor.rule === completeRule.alertId + ) + ); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_sequences_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_sequences_factory.ts index 916b7f4801e8e..0aeb8935c6cc0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_sequences_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_sequences_factory.ts @@ -10,8 +10,11 @@ import { Logger } from 'kibana/server'; import { WrapSequences } from '../../signals/types'; import { buildAlertGroupFromSequence } from './utils/build_alert_group_from_sequence'; import { ConfigType } from '../../../../config'; -import { WrappedRACAlert } from '../types'; import { CompleteRule, RuleParams } from '../../schemas/rule_schemas'; +import { + BaseFieldsLatest, + WrappedFieldsLatest, +} from '../../../../../common/detection_engine/schemas/alerts'; export const wrapSequencesFactory = ({ @@ -29,7 +32,7 @@ export const wrapSequencesFactory = }): WrapSequences => (sequences, buildReasonMessage) => sequences.reduce( - (acc: WrappedRACAlert[], sequence) => [ + (acc: Array>, sequence) => [ ...acc, ...buildAlertGroupFromSequence( logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index 2503c799ebc84..2537f7eeeaf72 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -7,9 +7,7 @@ import { Moment } from 'moment'; -import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Logger } from '@kbn/logging'; -import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { AlertExecutorOptions, RuleType } from '../../../../../alerting/server'; @@ -20,10 +18,7 @@ import { WithoutReservedActionGroups, } from '../../../../../alerting/common'; import { ListClient } from '../../../../../lists/server'; -import { TechnicalRuleFieldMap } from '../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map'; -import { TypeOfFieldMap } from '../../../../../rule_registry/common/field_map'; import { PersistenceServices, IRuleDataClient } from '../../../../../rule_registry/server'; -import { BaseHit } from '../../../../common/detection_engine/types'; import { ConfigType } from '../../../config'; import { SetupPlugins } from '../../../plugin'; import { CompleteRule, RuleParams } from '../schemas/rule_schemas'; @@ -36,10 +31,8 @@ import { } from '../signals/types'; import { ExperimentalFeatures } from '../../../../common/experimental_features'; import { IEventLogService } from '../../../../../event_log/server'; -import { AlertsFieldMap, RulesFieldMap } from '../../../../common/field_maps'; import { ITelemetryEventsSender } from '../../telemetry/sender'; import { RuleExecutionLogForExecutorsFactory } from '../rule_execution_log'; -import { commonParamsCamelToSnake } from '../schemas/rule_converters'; export interface SecurityAlertTypeReturnValue { bulkCreateTimes: string[]; @@ -112,18 +105,6 @@ export type CreateSecurityRuleTypeWrapper = ( type: SecurityAlertType ) => RuleType; -export type RACAlertSignal = TypeOfFieldMap & TypeOfFieldMap; -export type RACAlert = Omit< - TypeOfFieldMap & RACAlertSignal, - '@timestamp' | typeof ALERT_RULE_PARAMETERS -> & { - '@timestamp': string; - [ALERT_RULE_PARAMETERS]: ReturnType; -}; - -export type RACSourceHit = SearchHit; -export type WrappedRACAlert = BaseHit; - export interface CreateRuleOptions { experimentalFeatures: ExperimentalFeatures; logger: Logger; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 5f6ce6966fc02..3ed5dccaccdf2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -23,14 +23,63 @@ import { RulesSchema } from '../../../../../common/detection_engine/schemas/resp import { RuleParams } from '../../schemas/rule_schemas'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import { + ALERT_BUILDING_BLOCK_TYPE, ALERT_REASON, + ALERT_RISK_SCORE, + ALERT_RULE_AUTHOR, + ALERT_RULE_CATEGORY, + ALERT_RULE_CONSUMER, + ALERT_RULE_CREATED_AT, + ALERT_RULE_CREATED_BY, + ALERT_RULE_DESCRIPTION, + ALERT_RULE_ENABLED, + ALERT_RULE_EXECUTION_UUID, + ALERT_RULE_FROM, + ALERT_RULE_INTERVAL, + ALERT_RULE_LICENSE, + ALERT_RULE_NAME, + ALERT_RULE_NAMESPACE_FIELD, + ALERT_RULE_NOTE, ALERT_RULE_PARAMETERS, + ALERT_RULE_PRODUCER, + ALERT_RULE_REFERENCES, + ALERT_RULE_RULE_ID, + ALERT_RULE_RULE_NAME_OVERRIDE, + ALERT_RULE_TAGS, + ALERT_RULE_TO, + ALERT_RULE_TYPE, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UPDATED_AT, + ALERT_RULE_UPDATED_BY, ALERT_RULE_UUID, + ALERT_RULE_VERSION, + ALERT_SEVERITY, + ALERT_STATUS, + ALERT_STATUS_ACTIVE, ALERT_UUID, ALERT_WORKFLOW_STATUS, EVENT_KIND, + SPACE_IDS, + TIMESTAMP, } from '@kbn/rule-data-utils'; -import { ALERT_ANCESTORS } from '../../../../../common/field_maps/field_names'; +import { + ALERT_ANCESTORS, + ALERT_DEPTH, + ALERT_ORIGINAL_TIME, + ALERT_RULE_ACTIONS, + ALERT_RULE_EXCEPTIONS_LIST, + ALERT_RULE_FALSE_POSITIVES, + ALERT_RULE_IMMUTABLE, + ALERT_RULE_MAX_SIGNALS, + ALERT_RULE_RISK_SCORE_MAPPING, + ALERT_RULE_SEVERITY_MAPPING, + ALERT_RULE_THREAT, + ALERT_RULE_THROTTLE, + ALERT_RULE_TIMELINE_ID, + ALERT_RULE_TIMELINE_TITLE, + ALERT_RULE_TIMESTAMP_OVERRIDE, +} from '../../../../../common/field_maps/field_names'; +import { SERVER_APP_ID } from '../../../../../common/constants'; export const sampleRuleSO = (params: T): SavedObject> => { return { @@ -212,23 +261,95 @@ export const sampleAlertDocAADNoSortId = ( _id: someUuid, _source: { someKey: 'someValue', - '@timestamp': '2020-04-20T21:27:45+0000', - source: { - ip: ip ?? '127.0.0.1', - }, + + [TIMESTAMP]: '2020-04-20T21:27:45+0000', + [SPACE_IDS]: ['default'], [EVENT_KIND]: 'signal', - [ALERT_UUID]: someUuid, - [ALERT_REASON]: 'reasonable reason', - [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [ALERT_ANCESTORS]: [ { id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', depth: 0, + rule: undefined, + }, + ], + [ALERT_BUILDING_BLOCK_TYPE]: undefined, + [ALERT_ORIGINAL_TIME]: undefined, + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DEPTH]: 1, + [ALERT_REASON]: 'reasonable reason', + [ALERT_SEVERITY]: 'high', + [ALERT_RISK_SCORE]: 50, + [ALERT_RULE_ACTIONS]: [], + [ALERT_RULE_AUTHOR]: ['Elastic'], + [ALERT_RULE_CATEGORY]: 'Custom Query Rule', + [ALERT_RULE_CREATED_AT]: '2020-03-27T22:55:59.577Z', + [ALERT_RULE_CREATED_BY]: 'sample user', + [ALERT_RULE_DESCRIPTION]: 'Descriptive description', + [ALERT_RULE_ENABLED]: true, + [ALERT_RULE_EXCEPTIONS_LIST]: [], + [ALERT_RULE_EXECUTION_UUID]: '97e8f53a-4971-4935-bb54-9b8f86930cc7', + [ALERT_RULE_FALSE_POSITIVES]: [], + [ALERT_RULE_FROM]: 'now-6m', + [ALERT_RULE_IMMUTABLE]: false, + [ALERT_RULE_INTERVAL]: '5m', + [ALERT_RULE_LICENSE]: 'Elastic License', + [ALERT_RULE_MAX_SIGNALS]: 10000, + [ALERT_RULE_NAME]: 'rule-name', + [ALERT_RULE_NAMESPACE_FIELD]: undefined, + [ALERT_RULE_NOTE]: undefined, + [ALERT_RULE_PRODUCER]: 'siem', + [ALERT_RULE_REFERENCES]: ['http://example.com', 'https://example.com'], + [ALERT_RULE_RISK_SCORE_MAPPING]: [], + [ALERT_RULE_RULE_ID]: 'rule-1', + [ALERT_RULE_RULE_NAME_OVERRIDE]: undefined, + [ALERT_RULE_TYPE_ID]: 'siem.queryRule', + [ALERT_RULE_SEVERITY_MAPPING]: [], + [ALERT_RULE_TAGS]: ['some fake tag 1', 'some fake tag 2'], + [ALERT_RULE_THREAT]: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0000', + name: 'test tactic', + reference: 'https://attack.mitre.org/tactics/TA0000/', + }, + technique: [ + { + id: 'T0000', + name: 'test technique', + reference: 'https://attack.mitre.org/techniques/T0000/', + subtechnique: [ + { + id: 'T0000.000', + name: 'test subtechnique', + reference: 'https://attack.mitre.org/techniques/T0000/000/', + }, + ], + }, + ], }, ], + [ALERT_RULE_THROTTLE]: 'no_actions', + [ALERT_RULE_TIMELINE_ID]: 'some-timeline-id', + [ALERT_RULE_TIMELINE_TITLE]: 'some-timeline-title', + [ALERT_RULE_TIMESTAMP_OVERRIDE]: undefined, + [ALERT_RULE_TO]: 'now', + [ALERT_RULE_TYPE]: 'query', + [ALERT_RULE_UPDATED_AT]: '2020-03-27T22:55:59.577Z', + [ALERT_RULE_UPDATED_BY]: 'sample user', [ALERT_RULE_UUID]: '2e051244-b3c6-4779-a241-e1b4f0beceb9', + [ALERT_RULE_VERSION]: 1, + source: { + ip: ip ?? '127.0.0.1', + }, + [ALERT_UUID]: someUuid, + 'kibana.alert.rule.risk_score': 50, + 'kibana.alert.rule.severity': 'high', + 'kibana.alert.rule.building_block_type': undefined, [ALERT_RULE_PARAMETERS]: { description: 'Descriptive description', meta: { someMeta: 'someField' }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index a757e178ea48a..1a35d9f0771fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -20,6 +20,7 @@ import { BuildRuleMessage } from './rule_messages'; import { BulkCreate, WrapHits } from './types'; import { CompleteRule, MachineLearningRuleParams } from '../schemas/rule_schemas'; import { buildReasonMessageForMlAlert } from './reason_formatters'; +import { BaseFieldsLatest } from '../../../../common/detection_engine/schemas/alerts'; interface BulkCreateMlSignalsParams { someResult: AnomalyResults; @@ -86,7 +87,7 @@ const transformAnomalyResultsToEcs = ( export const bulkCreateMlSignals = async ( params: BulkCreateMlSignalsParams -): Promise> => { +): Promise> => { const anomalyResults = params.someResult; const ecsResults = transformAnomalyResultsToEcs(anomalyResults); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts index 5195f40da8010..dd9d5e2938e67 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts @@ -26,7 +26,6 @@ import { WrapSequences, RuleRangeTuple, SearchAfterAndBulkCreateReturnType, - SimpleHit, SignalSource, } from '../types'; import { createSearchAfterReturnType, makeFloatString } from '../utils'; @@ -34,6 +33,10 @@ import { ExperimentalFeatures } from '../../../../../common/experimental_feature import { buildReasonMessageForEqlAlert } from '../reason_formatters'; import { CompleteRule, EqlRuleParams } from '../../schemas/rule_schemas'; import { withSecuritySpan } from '../../../../utils/with_security_span'; +import { + BaseFieldsLatest, + WrappedFieldsLatest, +} from '../../../../../common/detection_engine/schemas/alerts'; export const eqlExecutor = async ({ completeRule, @@ -118,7 +121,7 @@ export const eqlExecutor = async ({ const eqlSearchDuration = makeFloatString(eqlSignalSearchEnd - eqlSignalSearchStart); result.searchAfterTimes = [eqlSearchDuration]; - let newSignals: SimpleHit[] | undefined; + let newSignals: Array> | undefined; if (response.hits.sequences !== undefined) { newSignals = wrapSequences(response.hits.sequences, buildReasonMessageForEqlAlert); } else if (response.hits.events !== undefined) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts deleted file mode 100644 index 0098d50fc01ef..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts +++ /dev/null @@ -1,48 +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 { filterDuplicateSignals } from './filter_duplicate_signals'; -import { sampleWrappedSignalHit } from './__mocks__/es_results'; - -const mockRuleId1 = 'aaaaaaaa'; -const mockRuleId2 = 'bbbbbbbb'; -const mockRuleId3 = 'cccccccc'; - -const createWrappedSignalHitWithRuleId = (ruleId: string) => { - const mockSignal = sampleWrappedSignalHit(); - return { - ...mockSignal, - _source: { - ...mockSignal._source, - signal: { - ...mockSignal._source.signal, - ancestors: [ - { - ...mockSignal._source.signal.ancestors[0], - rule: ruleId, - }, - ], - }, - }, - }; -}; -const mockSignals = [ - createWrappedSignalHitWithRuleId(mockRuleId1), - createWrappedSignalHitWithRuleId(mockRuleId2), -]; - -describe('filterDuplicateSignals', () => { - describe('detection engine implementation', () => { - it('filters duplicate signals', () => { - expect(filterDuplicateSignals(mockRuleId1, mockSignals, false).length).toEqual(1); - }); - - it('does not filter non-duplicate signals', () => { - expect(filterDuplicateSignals(mockRuleId3, mockSignals, false).length).toEqual(2); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts deleted file mode 100644 index 77671167c1cfd..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts +++ /dev/null @@ -1,29 +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 { WrappedRACAlert } from '../rule_types/types'; -import { Ancestor, SimpleHit, WrappedSignalHit } from './types'; - -export const filterDuplicateSignals = ( - ruleId: string, - signals: SimpleHit[], - isRuleRegistryEnabled: boolean -) => { - // TODO: handle alerts-on-legacy-alerts - if (!isRuleRegistryEnabled) { - return (signals as WrappedSignalHit[]).filter( - (doc) => !doc._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleId) - ); - } else { - return (signals as WrappedRACAlert[]).filter( - (doc) => - !(doc._source['kibana.alert.ancestors'] as Ancestor[]).some( - (ancestor) => ancestor.rule === ruleId - ) - ); - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 52d0a04eee1ec..d6332af195fcf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -33,6 +33,20 @@ import { BuildReasonMessage } from './reason_formatters'; import { QueryRuleParams } from '../schemas/rule_schemas'; import { createPersistenceServicesMock } from '../../../../../rule_registry/server/utils/create_persistence_rule_type_wrapper.mock'; import { PersistenceServices } from '../../../../../rule_registry/server'; +import { + ALERT_RULE_CATEGORY, + ALERT_RULE_CONSUMER, + ALERT_RULE_EXECUTION_UUID, + ALERT_RULE_NAME, + ALERT_RULE_PRODUCER, + ALERT_RULE_TAGS, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UUID, + SPACE_IDS, + TIMESTAMP, +} from '@kbn/rule-data-utils'; +import { SERVER_APP_ID } from '../../../../common/constants'; +import { CommonAlertFieldsLatest } from '../../../../../rule_registry/common/schemas'; const buildRuleMessage = mockBuildRuleMessage; @@ -50,6 +64,18 @@ describe('searchAfterAndBulkCreate', () => { const defaultFilter = { match_all: {}, }; + const mockCommonFields: CommonAlertFieldsLatest = { + [ALERT_RULE_CATEGORY]: 'Custom Query Rule', + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, + [ALERT_RULE_EXECUTION_UUID]: '97e8f53a-4971-4935-bb54-9b8f86930cc7', + [ALERT_RULE_NAME]: 'rule-name', + [ALERT_RULE_PRODUCER]: 'siem', + [ALERT_RULE_TYPE_ID]: 'siem.queryRule', + [ALERT_RULE_UUID]: '2e051244-b3c6-4779-a241-e1b4f0beceb9', + [SPACE_IDS]: ['default'], + [ALERT_RULE_TAGS]: [], + [TIMESTAMP]: '2020-04-20T21:27:45+0000', + }; sampleParams.maxSignals = 30; let tuple: RuleRangeTuple; beforeEach(() => { @@ -92,7 +118,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '1', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -103,7 +135,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '2', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -114,7 +152,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '3', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -125,7 +169,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '4', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -178,7 +228,13 @@ describe('searchAfterAndBulkCreate', () => { ) ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '1', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -189,7 +245,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '2', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -200,7 +262,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '3', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -254,10 +322,26 @@ describe('searchAfterAndBulkCreate', () => { mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ createdAlerts: [ - { _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }, + { + _id: '1', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '2', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '3', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '4', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, ], errors: {}, }); @@ -365,10 +449,26 @@ describe('searchAfterAndBulkCreate', () => { test('should return success when empty string sortId present', async () => { mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ createdAlerts: [ - { _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }, + { + _id: '1', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '2', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '3', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '4', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, ], errors: {}, }); @@ -478,10 +578,26 @@ describe('searchAfterAndBulkCreate', () => { mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ createdAlerts: [ - { _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }, + { + _id: '1', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '2', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '3', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '4', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, ], errors: {}, }); @@ -530,10 +646,26 @@ describe('searchAfterAndBulkCreate', () => { mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ createdAlerts: [ - { _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }, - { _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }, + { + _id: '1', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '2', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '3', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + { + _id: '4', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, ], errors: {}, }); @@ -695,7 +827,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '1', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: { 'error on creation': { count: 1, @@ -713,7 +851,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '2', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -724,7 +868,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '3', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -735,7 +885,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '4', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '4', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -777,7 +933,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '1', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '1', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -788,7 +950,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '2', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '2', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); @@ -799,7 +967,13 @@ describe('searchAfterAndBulkCreate', () => { ); mockPersistenceServices.alertWithPersistence.mockResolvedValueOnce({ - createdAlerts: [{ _id: '3', _index: '.internal.alerts-security.alerts-default-000001' }], + createdAlerts: [ + { + _id: '3', + _index: '.internal.alerts-security.alerts-default-000001', + ...mockCommonFields, + }, + ], errors: {}, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts index d251d6fcfcbf2..9fda5205e9dab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts @@ -10,14 +10,14 @@ import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; import { ALERT_ORIGINAL_TIME } from '../../../../../common/field_maps/field_names'; import { SimpleHit, ThresholdSignalHistory } from '../types'; -import { getThresholdTermsHash, isWrappedRACAlert, isWrappedSignalHit } from '../utils'; +import { getThresholdTermsHash, isWrappedDetectionAlert, isWrappedSignalHit } from '../utils'; interface GetThresholdSignalHistoryParams { alerts: Array>; } const getTerms = (alert: SimpleHit) => { - if (isWrappedRACAlert(alert)) { + if (isWrappedDetectionAlert(alert)) { const parameters = alert._source[ALERT_RULE_PARAMETERS] as unknown as Record< string, Record @@ -35,7 +35,7 @@ const getTerms = (alert: SimpleHit) => { }; const getOriginalTime = (alert: SimpleHit) => { - if (isWrappedRACAlert(alert)) { + if (isWrappedDetectionAlert(alert)) { const originalTime = alert._source[ALERT_ORIGINAL_TIME]; return originalTime != null ? new Date(originalTime as string).getTime() : undefined; } else if (isWrappedSignalHit(alert)) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index 2148d4feacdae..49a81a47502d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -30,6 +30,7 @@ import type { WrapHits, } from '../types'; import { CompleteRule, ThresholdRuleParams } from '../../schemas/rule_schemas'; +import { BaseFieldsLatest } from '../../../../../common/detection_engine/schemas/alerts'; interface BulkCreateThresholdSignalsParams { someResult: SignalSearchResponse; @@ -205,7 +206,7 @@ export const transformThresholdResultsToEcs = ( export const bulkCreateThresholdSignals = async ( params: BulkCreateThresholdSignalsParams -): Promise> => { +): Promise> => { const ruleParams = params.completeRule.ruleParams; const thresholdResults = params.someResult; const ecsResults = transformThresholdResultsToEcs( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 44154a8727f38..bbe6adc5c729f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -34,7 +34,11 @@ import { GenericBulkCreateResponse } from '../rule_types/factories'; import { EcsFieldMap } from '../../../../../rule_registry/common/assets/field_maps/ecs_field_map'; import { TypeOfFieldMap } from '../../../../../rule_registry/common/field_map'; import { BuildReasonMessage } from './reason_formatters'; -import { RACAlert } from '../rule_types/types'; +import { + BaseFieldsLatest, + DetectionAlert, + WrappedFieldsLatest, +} from '../../../../common/detection_engine/schemas/alerts'; // used for gap detection code // eslint-disable-next-line @typescript-eslint/naming-convention @@ -179,10 +183,9 @@ export type EventHit = Exclude, '@timestamp'> & { }; export type WrappedEventHit = BaseHit; -export type AlertSearchResponse = estypes.SearchResponse; export type SignalSearchResponse = estypes.SearchResponse; export type SignalSourceHit = estypes.SearchHit; -export type AlertSourceHit = estypes.SearchHit; +export type AlertSourceHit = estypes.SearchHit; export type WrappedSignalHit = BaseHit; export type BaseSignalHit = estypes.SearchHit; @@ -276,8 +279,8 @@ export type BulkResponseErrorAggregation = Record Promise; -export type BulkCreate = >( - docs: Array> +export type BulkCreate = ( + docs: Array> ) => Promise>; export type SimpleHit = BaseHit<{ '@timestamp'?: string }>; @@ -285,12 +288,12 @@ export type SimpleHit = BaseHit<{ '@timestamp'?: string }>; export type WrapHits = ( hits: Array>, buildReasonMessage: BuildReasonMessage -) => SimpleHit[]; +) => Array>; export type WrapSequences = ( sequences: Array>, buildReasonMessage: BuildReasonMessage -) => SimpleHit[]; +) => Array>; export interface SearchAfterAndBulkCreateParams { tuple: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 9cf57ff0018be..7c020a476c41b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -43,7 +43,7 @@ import { getValidDateFromDoc, calculateTotal, getTotalHitsValue, - isRACAlert, + isDetectionAlert, getField, } from './utils'; import type { BulkResponseErrorAggregation, SearchAfterAndBulkCreateReturnType } from './types'; @@ -1538,10 +1538,10 @@ describe('utils', () => { }); }); - describe('isRACAlert', () => { + describe('isDetectionAlert', () => { test('alert with dotted fields returns true', () => { expect( - isRACAlert({ + isDetectionAlert({ [ALERT_UUID]: '123', }) ).toEqual(true); @@ -1549,7 +1549,7 @@ describe('utils', () => { test('alert with nested fields returns true', () => { expect( - isRACAlert({ + isDetectionAlert({ kibana: { alert: { uuid: '123' }, }, @@ -1558,31 +1558,31 @@ describe('utils', () => { }); test('undefined returns false', () => { - expect(isRACAlert(undefined)).toEqual(false); + expect(isDetectionAlert(undefined)).toEqual(false); }); test('null returns false', () => { - expect(isRACAlert(null)).toEqual(false); + expect(isDetectionAlert(null)).toEqual(false); }); test('number returns false', () => { - expect(isRACAlert(5)).toEqual(false); + expect(isDetectionAlert(5)).toEqual(false); }); test('string returns false', () => { - expect(isRACAlert('a')).toEqual(false); + expect(isDetectionAlert('a')).toEqual(false); }); test('array returns false', () => { - expect(isRACAlert([])).toEqual(false); + expect(isDetectionAlert([])).toEqual(false); }); test('empty object returns false', () => { - expect(isRACAlert({})).toEqual(false); + expect(isDetectionAlert({})).toEqual(false); }); test('alert with null value returns false', () => { - expect(isRACAlert({ 'kibana.alert.uuid': null })).toEqual(false); + expect(isDetectionAlert({ 'kibana.alert.uuid': null })).toEqual(false); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index f49eee858d135..0a4618d81081b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -62,10 +62,10 @@ import type { ThreatRuleParams, ThresholdRuleParams, } from '../schemas/rule_schemas'; -import type { RACAlert, WrappedRACAlert } from '../rule_types/types'; -import type { SearchTypes } from '../../../../common/detection_engine/types'; +import type { BaseHit, SearchTypes } from '../../../../common/detection_engine/types'; import type { IRuleExecutionLogForExecutors } from '../rule_execution_log'; import { withSecuritySpan } from '../../../utils/with_security_span'; +import { DetectionAlert } from '../../../../common/detection_engine/schemas/alerts'; import { ENABLE_CCS_READ_WARNING_SETTING } from '../../../../common/constants'; interface SortExceptionsReturn { @@ -971,18 +971,18 @@ export const buildChunkedOrFilter = (field: string, values: string[], chunkSize: }; export const isWrappedEventHit = (event: SimpleHit): event is WrappedEventHit => { - return !isWrappedSignalHit(event) && !isWrappedRACAlert(event); + return !isWrappedSignalHit(event) && !isWrappedDetectionAlert(event); }; export const isWrappedSignalHit = (event: SimpleHit): event is WrappedSignalHit => { return (event as WrappedSignalHit)?._source?.signal != null; }; -export const isWrappedRACAlert = (event: SimpleHit): event is WrappedRACAlert => { - return (event as WrappedRACAlert)?._source?.[ALERT_UUID] != null; +export const isWrappedDetectionAlert = (event: SimpleHit): event is BaseHit => { + return (event as BaseHit)?._source?.[ALERT_UUID] != null; }; -export const isRACAlert = (event: unknown): event is RACAlert => { +export const isDetectionAlert = (event: unknown): event is DetectionAlert => { return get(event, ALERT_UUID) != null; }; @@ -1019,19 +1019,19 @@ export const racFieldMappings: Record = { 'signal.rule.immutable': `${ALERT_RULE_PARAMETERS}.immutable`, }; -export const getField = (event: SimpleHit, field: string): T | undefined => { - if (isWrappedRACAlert(event)) { +export const getField = (event: SimpleHit, field: string): SearchTypes | undefined => { + if (isWrappedDetectionAlert(event)) { const mappedField = racFieldMappings[field] ?? field.replace('signal', 'kibana.alert'); const parts = mappedField.split('.'); if (mappedField.includes(ALERT_RULE_PARAMETERS) && parts[parts.length - 1] !== 'parameters') { const params = get(event._source, ALERT_RULE_PARAMETERS); return get(params, parts[parts.length - 1]); } - return get(event._source, mappedField) as T; + return get(event._source, mappedField) as SearchTypes | undefined; } else if (isWrappedSignalHit(event)) { const mappedField = invert(racFieldMappings)[field] ?? field.replace('kibana.alert', 'signal'); - return get(event._source, mappedField) as T; + return get(event._source, mappedField) as SearchTypes | undefined; } else if (isWrappedEventHit(event)) { - return get(event._source, field) as T; + return get(event._source, field) as SearchTypes | undefined; } }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts index f58dee11c48da..005352a643af1 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts @@ -26,7 +26,7 @@ import { waitForRuleSuccessOrStatus, getRuleForSignalTesting, } from '../../utils'; -import { RACAlert } from '../../../../plugins/security_solution/server/lib/detection_engine/rule_types/types'; +import { DetectionAlert } from '../../../../plugins/security_solution/common/detection_engine/schemas/alerts'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -92,11 +92,12 @@ export default ({ getService }: FtrProviderContext) => { .send(setSignalStatus({ signalIds, status: 'closed' })) .expect(200); - const { body: signalsClosed }: { body: estypes.SearchResponse } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds(signalIds)) - .expect(200); + const { body: signalsClosed }: { body: estypes.SearchResponse } = + await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); expect(signalsClosed.hits.hits.length).to.equal(10); }); @@ -117,11 +118,12 @@ export default ({ getService }: FtrProviderContext) => { .send(setSignalStatus({ signalIds, status: 'closed' })) .expect(200); - const { body: signalsClosed }: { body: estypes.SearchResponse } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds(signalIds)) - .expect(200); + const { body: signalsClosed }: { body: estypes.SearchResponse } = + await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); const everySignalClosed = signalsClosed.hits.hits.every( (hit) => hit._source?.[ALERT_WORKFLOW_STATUS] === 'closed' diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts index 18207f9258059..d0c839fcb1b62 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts @@ -23,7 +23,7 @@ import { waitForRuleSuccessOrStatus, getRuleForSignalTesting, } from '../../utils'; -import { RACAlert } from '../../../../plugins/security_solution/server/lib/detection_engine/rule_types/types'; +import { DetectionAlert } from '../../../../plugins/security_solution/common/detection_engine/schemas/alerts'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -86,11 +86,12 @@ export default ({ getService }: FtrProviderContext) => { .send({ ids: signalIds, status: 'closed', index: '.siem-signals-default' }) .expect(200); - const { body: signalsClosed }: { body: estypes.SearchResponse } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds(signalIds)) - .expect(200); + const { body: signalsClosed }: { body: estypes.SearchResponse } = + await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); expect(signalsClosed.hits.hits.length).to.equal(10); }); @@ -111,11 +112,12 @@ export default ({ getService }: FtrProviderContext) => { .send({ ids: signalIds, status: 'closed', index: '.siem-signals-default' }) .expect(200); - const { body: signalsClosed }: { body: estypes.SearchResponse } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds(signalIds)) - .expect(200); + const { body: signalsClosed }: { body: estypes.SearchResponse } = + await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); const everySignalClosed = signalsClosed.hits.hits.every( (hit) => hit._source?.['kibana.alert.workflow_status'] === 'closed' @@ -140,7 +142,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ ids: signalIds, status: 'acknowledged', index: '.siem-signals-default' }) .expect(200); - const { body: acknowledgedSignals }: { body: estypes.SearchResponse } = + const { body: acknowledgedSignals }: { body: estypes.SearchResponse } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts index 6138995bdc02f..18edf09727d3a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts @@ -29,7 +29,7 @@ import { } from '../../utils'; import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution'; import { ROLES } from '../../../../plugins/security_solution/common/test'; -import { RACAlert } from '../../../../plugins/security_solution/server/lib/detection_engine/rule_types/types'; +import { DetectionAlert } from '../../../../plugins/security_solution/common/detection_engine/schemas/alerts'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -126,7 +126,7 @@ export default ({ getService }: FtrProviderContext) => { .send(setSignalStatus({ signalIds, status: 'closed' })) .expect(200); - const { body: signalsClosed }: { body: estypes.SearchResponse } = + const { body: signalsClosed }: { body: estypes.SearchResponse } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') @@ -152,7 +152,7 @@ export default ({ getService }: FtrProviderContext) => { .send(setSignalStatus({ signalIds, status: 'closed' })) .expect(200); - const { body: signalsClosed }: { body: estypes.SearchResponse } = + const { body: signalsClosed }: { body: estypes.SearchResponse } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') @@ -186,7 +186,7 @@ export default ({ getService }: FtrProviderContext) => { // query for the signals with the superuser // to allow a check that the signals were NOT closed with t1 analyst - const { body: signalsClosed }: { body: estypes.SearchResponse } = + const { body: signalsClosed }: { body: estypes.SearchResponse } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') @@ -221,7 +221,7 @@ export default ({ getService }: FtrProviderContext) => { .send(setSignalStatus({ signalIds, status: 'closed' })) .expect(200); - const { body: signalsClosed }: { body: estypes.SearchResponse } = + const { body: signalsClosed }: { body: estypes.SearchResponse } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 594f380199dd6..2fc5cdadecb2a 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -58,8 +58,8 @@ import { SECURITY_TELEMETRY_URL, UPDATE_OR_CREATE_LEGACY_ACTIONS, } from '../../plugins/security_solution/common/constants'; -import { RACAlert } from '../../plugins/security_solution/server/lib/detection_engine/rule_types/types'; import { DetectionMetrics } from '../../plugins/security_solution/server/usage/detections/types'; +import { DetectionAlert } from '../../plugins/security_solution/common/detection_engine/schemas/alerts'; /** * This will remove server generated properties such as date times, etc... @@ -1593,7 +1593,7 @@ export const getSignalsByRuleIds = async ( supertest: SuperTest.SuperTest, log: ToolingLog, ruleIds: string[] -): Promise> => { +): Promise> => { const response = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') @@ -1607,7 +1607,7 @@ export const getSignalsByRuleIds = async ( ); } - const { body: signalsOpen }: { body: estypes.SearchResponse } = response; + const { body: signalsOpen }: { body: estypes.SearchResponse } = response; return signalsOpen; }; @@ -1622,7 +1622,7 @@ export const getSignalsByIds = async ( log: ToolingLog, ids: string[], size?: number -): Promise> => { +): Promise> => { const response = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') @@ -1635,7 +1635,7 @@ export const getSignalsByIds = async ( )}, status: ${JSON.stringify(response.status)}` ); } - const { body: signalsOpen }: { body: estypes.SearchResponse } = response; + const { body: signalsOpen }: { body: estypes.SearchResponse } = response; return signalsOpen; }; @@ -1648,7 +1648,7 @@ export const getSignalsById = async ( supertest: SuperTest.SuperTest, log: ToolingLog, id: string -): Promise> => { +): Promise> => { const response = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') @@ -1661,7 +1661,7 @@ export const getSignalsById = async ( )}, status: ${JSON.stringify(response.status)}` ); } - const { body: signalsOpen }: { body: estypes.SearchResponse } = response; + const { body: signalsOpen }: { body: estypes.SearchResponse } = response; return signalsOpen; }; From 2688cb21f9f29f57beea6b1e91a7e9d45a0ee517 Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 29 Mar 2022 20:44:44 -0700 Subject: [PATCH 055/108] Upgrade EUI to v52.2.0 (#128841) * Updgraded EUI packages in package.json and src/dev/license_checker/config.js * Resolved Jest test failures for Jest test suites 1 and 2. Updated snapshots, and updated equality conditions for specific test cases * Resolve Jest test cases for Jest test suite 3. Updated snapshots for required tests * Resolved failing Jest test cases in Jest suite 3. Updated tests checking for strict text equality to account for text coming from the EuiScreenReaderOnly component. Also updated tests to account for EuiIcon text that is now rendered when the icon is imported from .testenv (PR 5709 - https://github.com/elastic/eui/pull/5709/). * type fixes * eui to 52.2.0 * Resolved test cases for Jest test suites 1 and 2. Updated required snapshots. Updated tests using getAllByLabelText and getByLabelText to getAllByText and getByText respectively as the former have been deprecated * Updated Jest tests for Jest test suites 5 and 6. Updated required snapshots. Updated instances of getByLabelText and getAllByLabelText to getByText and getAllByText as the former are now deprecated. * Updated Jest tests for Jest test suite 7. Updated required snapshots. * Completed test case revisions for Jest test suites 1, 3, 6, 7, and 8. Updated required snapshots. Updated various tests to account for text rendering of the EuiIcon text. * removed unused test utils * use .contains for euiicon content * storyshots updates * linting * Fix failing a11y violations tests * Fix Jest failures caused by #eui/5709 - these changes should be reverted if we opt to revert the above PR Co-authored-by: Bree Hall Co-authored-by: Greg Thompson --- package.json | 2 +- .../header/__snapshots__/header.test.tsx.snap | 5 +- src/dev/license_checker/config.ts | 2 +- .../url/__snapshots__/url.test.tsx.snap | 10 +-- .../table/__snapshots__/table.test.tsx.snap | 5 +- .../__snapshots__/table_header.test.tsx.snap | 5 +- .../__snapshots__/field_name.test.tsx.snap | 20 +++--- .../not_found_errors.test.tsx.snap | 20 +++--- .../components/not_found_errors.test.tsx | 8 +-- .../link_preview.test.tsx | 6 +- .../custom_link_toolbar.test.tsx | 4 +- .../dropdown_filter.stories.storyshot | 20 ++++-- .../time_filter.stories.storyshot | 60 ++++++++++++----- .../extended_template.stories.storyshot | 8 ++- .../date_format.stories.storyshot | 12 +++- .../number_format.stories.storyshot | 12 +++- .../__snapshots__/asset.stories.storyshot | 32 +++++++--- .../asset_manager.stories.storyshot | 52 +++++++++++---- .../__snapshots__/color_dot.stories.storyshot | 16 +++-- .../color_manager.stories.storyshot | 32 +++++++--- .../color_palette.stories.storyshot | 12 +++- .../color_picker.stories.storyshot | 40 +++++++++--- .../custom_element_modal.stories.storyshot | 44 +++++++++---- .../datasource_component.stories.storyshot | 16 +++-- .../element_card.stories.storyshot | 12 +++- .../file_upload.stories.storyshot | 4 +- .../home/__snapshots__/home.stories.storyshot | 4 +- .../empty_prompt.stories.storyshot | 4 +- .../workpad_table.stories.storyshot | 52 +++++++++++---- .../__snapshots__/item_grid.stories.storyshot | 24 +++++-- .../element_controls.stories.storyshot | 8 ++- .../element_grid.stories.storyshot | 24 +++++-- .../saved_elements_modal.stories.storyshot | 64 ++++++++++++++----- .../sidebar_header.stories.storyshot | 16 +++-- .../__snapshots__/tag.stories.storyshot | 8 ++- .../__snapshots__/tag_list.stories.storyshot | 12 +++- .../text_style_picker.stories.storyshot | 56 ++++++++++++---- .../delete_var.stories.storyshot | 8 ++- .../__snapshots__/edit_var.stories.storyshot | 44 +++++++++---- .../var_config.stories.storyshot | 8 ++- .../filters_group.component.stories.storyshot | 8 ++- ...orkpad_filters.component.stories.storyshot | 40 +++++++++--- .../editor_menu.stories.storyshot | 8 ++- .../element_menu.stories.storyshot | 4 +- .../extended_template.stories.storyshot | 24 +++++-- .../extended_template.stories.storyshot | 16 +++-- .../simple_template.stories.storyshot | 5 +- .../__snapshots__/canvas.stories.storyshot | 48 ++++++++++---- .../__snapshots__/footer.stories.storyshot | 32 +++++++--- .../page_controls.stories.storyshot | 24 +++++-- .../__snapshots__/title.stories.storyshot | 12 +++- .../autoplay_settings.stories.storyshot | 24 +++++-- .../__snapshots__/settings.stories.storyshot | 8 ++- .../toolbar_settings.stories.storyshot | 24 +++++-- .../connectors_dropdown.test.tsx | 2 +- .../markdown_editor/renderer.test.tsx | 2 +- .../components/csp_page_template.test.tsx | 4 +- .../components/table/table.test.tsx | 2 +- .../__jest__/a11y/indices_tab.a11y.test.ts | 43 ++----------- .../index_setup_dataset_filter.tsx | 1 - .../__jest__/test_pipeline.test.tsx | 14 ++-- .../components/table_basic.test.tsx | 4 +- .../field_item.test.tsx | 5 +- .../request_trial_extension.test.js.snap | 8 +-- .../revert_to_basic.test.js.snap | 6 +- .../__snapshots__/start_trial.test.js.snap | 8 +-- .../__snapshots__/exporters.test.js.snap | 15 +++-- .../__snapshots__/reason_found.test.js.snap | 15 +++-- .../list/remote_clusters_list.test.js | 6 +- .../feature_table_cell.test.tsx | 2 +- .../privilege_summary/__fixtures__/index.ts | 2 +- .../exception_item/exception_entries.test.tsx | 2 +- .../common/components/inspect/modal.test.tsx | 6 +- .../common/components/links/index.test.tsx | 10 +-- .../markdown_editor/renderer.test.tsx | 2 +- .../network/components/port/index.test.tsx | 6 +- .../source_destination/index.test.tsx | 4 +- .../source_destination_ip.test.tsx | 8 +-- .../certificate_fingerprint/index.test.tsx | 2 +- .../components/ja3_fingerprint/index.test.tsx | 2 +- .../components/netflow/index.test.tsx | 10 +-- .../__snapshots__/index.test.tsx.snap | 5 +- .../body/renderers/get_row_renderer.test.tsx | 12 +++- .../suricata/suricata_details.test.tsx | 6 +- .../suricata/suricata_row_renderer.test.tsx | 7 +- .../system/generic_row_renderer.test.tsx | 26 +++++--- .../body/renderers/zeek/zeek_details.test.tsx | 16 +++-- .../renderers/zeek/zeek_row_renderer.test.tsx | 7 +- .../renderers/zeek/zeek_signature.test.tsx | 6 +- .../__jest__/client_integration/home.test.ts | 2 +- .../public/components/inspect/modal.test.tsx | 6 +- .../components/health_check.test.tsx | 14 ++-- .../rule_details/components/rule.test.tsx | 9 ++- .../drilldown_table/drilldown_table.test.tsx | 2 +- .../__snapshots__/expanded_row.test.tsx.snap | 5 +- .../monitor_status.bar.test.tsx.snap | 5 +- .../network_requests_total.test.tsx | 4 +- .../components/waterfall_marker_icon.test.tsx | 4 +- .../ux_metrics/key_ux_metrics.test.tsx | 12 ++-- yarn.lock | 8 +-- 100 files changed, 970 insertions(+), 445 deletions(-) diff --git a/package.json b/package.json index 9f2e28774d0de..735f388881999 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.1", "@elastic/ems-client": "8.2.0", - "@elastic/eui": "51.1.0", + "@elastic/eui": "52.2.0", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index d2b1078641437..ad0f27bbf08ce 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -3108,12 +3108,13 @@ exports[`Header renders 1`] = ` type="logoElastic" > + > + Elastic Logo + + > + External link + @@ -243,10 +244,11 @@ exports[`UrlFormatEditor should render normally 1`] = ` Label template help + > + External link + diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap index 2b6cf62baf221..c054b42f51ac7 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -320,10 +320,11 @@ exports[`Table should render the boolean template (false) 1`] = ``; exports[`Table should render the boolean template (true) 1`] = ` +> + Is searchable + `; exports[`Table should render timestamp field name 1`] = ` diff --git a/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap index 3f72349f3e2a0..612973fe37a48 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap +++ b/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap @@ -20,10 +20,11 @@ exports[`TableHeader with time column renders correctly 1`] = ` class="euiToolTipAnchor" > + > + Primary time field. + + > + Geo point field + ,
+ > + Number field +
,
+ > + String field +
,
+ > + Unknown field +
,
+ > + External link + + > + External link + + > + External link + + > + External link + { const callOut = mounted.find('EuiCallOut'); expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectThe saved search associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectThe saved search associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIsExternal link(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); @@ -34,7 +34,7 @@ describe('NotFoundErrors component', () => { const callOut = mounted.find('EuiCallOut'); expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectThe data view associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectThe data view associated with this object no longer exists.If you know what this error means, you can use the Saved objects APIsExternal link(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); @@ -43,7 +43,7 @@ describe('NotFoundErrors component', () => { const callOut = mounted.find('EuiCallOut'); expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectA field associated with this object no longer exists in the data view.If you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectA field associated with this object no longer exists in the data view.If you know what this error means, you can use the Saved objects APIsExternal link(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); @@ -52,7 +52,7 @@ describe('NotFoundErrors component', () => { const callOut = mounted.find('EuiCallOut'); expect(callOut).toMatchSnapshot(); expect(mounted.text()).toMatchInlineSnapshot( - `"There is a problem with this saved objectIf you know what this error means, you can use the Saved objects APIs(opens in a new tab or window) to fix it — otherwise click the delete button above."` + `"There is a problem with this saved objectIf you know what this error means, you can use the Saved objects APIsExternal link(opens in a new tab or window) to fix it — otherwise click the delete button above."` ); }); }); diff --git a/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx index f44b4d1c1205d..4c8a5bc00285e 100644 --- a/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx @@ -56,7 +56,7 @@ describe('LinkPreview', () => { removeExternalLinkText( (getByTestId(container, 'preview-link') as HTMLAnchorElement).text ) - ).toEqual('https://baz.co'); + ).toContain('https://baz.co'); }); }); @@ -74,7 +74,7 @@ describe('LinkPreview', () => { removeExternalLinkText( (getByTestId(container, 'preview-link') as HTMLAnchorElement).text ) - ).toEqual('https://baz.co?service.name={{invalid}'); + ).toContain('https://baz.co?service.name={{invalid}'); expect(getByTestId(container, 'preview-warning')).toBeInTheDocument(); }); }); @@ -94,7 +94,7 @@ describe('LinkPreview', () => { removeExternalLinkText( (getByTestId(container, 'preview-link') as HTMLAnchorElement).text ) - ).toEqual('https://baz.co?transaction=0'); + ).toContain('https://baz.co?transaction=0'); }); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx index 4d92f5a1ae34a..42ca08cc3d225 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_toolbar.test.tsx @@ -45,7 +45,7 @@ describe('CustomLinkToolbar', () => { wrapper: Wrapper, }); expect( - component.getByLabelText('Custom links settings page') + component.getByText('Custom links settings page') ).toBeInTheDocument(); expectTextsInDocument(component, ['Create']); }); @@ -56,7 +56,7 @@ describe('CustomLinkToolbar', () => { { wrapper: Wrapper } ); expect( - component.getByLabelText('Custom links settings page') + component.getByText('Custom links settings page') ).toBeInTheDocument(); expectTextsNotInDocument(component, ['Create']); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot index 52694d3b04089..0ded42439fb95 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot @@ -35,7 +35,9 @@ exports[`Storyshots renderers/DropdownFilter default 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
@@ -96,7 +98,9 @@ exports[`Storyshots renderers/DropdownFilter with choices 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
@@ -157,7 +161,9 @@ exports[`Storyshots renderers/DropdownFilter with choices and new value 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
@@ -218,7 +224,9 @@ exports[`Storyshots renderers/DropdownFilter with choices and value 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
@@ -261,7 +269,9 @@ exports[`Storyshots renderers/DropdownFilter with new value 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + +
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot index 5abd1e9fd05b6..e82b6bf082b05 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot @@ -42,13 +42,17 @@ exports[`Storyshots renderers/TimeFilter default 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + +
+ > + +
@@ -91,7 +95,9 @@ exports[`Storyshots renderers/TimeFilter default 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + +
@@ -150,13 +156,17 @@ exports[`Storyshots renderers/TimeFilter with absolute time bounds 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + + > + + @@ -235,7 +245,9 @@ exports[`Storyshots renderers/TimeFilter with absolute time bounds 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + + @@ -294,13 +306,17 @@ exports[`Storyshots renderers/TimeFilter with commonlyUsedRanges 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + + > + + @@ -343,7 +359,9 @@ exports[`Storyshots renderers/TimeFilter with commonlyUsedRanges 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + + @@ -402,13 +420,17 @@ exports[`Storyshots renderers/TimeFilter with dateFormat 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + + > + + @@ -451,7 +473,9 @@ exports[`Storyshots renderers/TimeFilter with dateFormat 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + + @@ -510,13 +534,17 @@ exports[`Storyshots renderers/TimeFilter with relative time bounds 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + + > + + @@ -595,7 +623,9 @@ exports[`Storyshots renderers/TimeFilter with relative time bounds 1`] = ` color="inherit" data-euiicon-type="timeRefresh" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot index e3badfa833090..7c0a2ad18c3dc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot @@ -65,7 +65,9 @@ exports[`Storyshots arguments/AxisConfig extended 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -143,7 +145,9 @@ exports[`Storyshots arguments/AxisConfig/components extended 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot index 238fe7c259c6e..9755e1b53b868 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot @@ -47,7 +47,9 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -120,7 +122,9 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -192,7 +196,9 @@ exports[`Storyshots arguments/DateFormat with preset format 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot index 2159e49e2bcf1..ecd8e53ce1d25 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot @@ -57,7 +57,9 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -140,7 +142,9 @@ Array [ className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -222,7 +226,9 @@ exports[`Storyshots arguments/NumberFormat with preset format 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot index 6db24bd0b984c..587b07ca4f932 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot @@ -81,7 +81,9 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - /> + > + + @@ -111,7 +113,9 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + + @@ -142,7 +146,9 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - /> + > + + @@ -169,7 +175,9 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -260,7 +268,9 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - /> + > + + @@ -290,7 +300,9 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + + @@ -321,7 +333,9 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - /> + > + + @@ -348,7 +362,9 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot index dd650e9f4c697..5409f9c444df0 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot @@ -26,7 +26,9 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
@@ -122,7 +126,9 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` color="subdued" data-euiicon-type="importAction" size="xxl" - /> + > + +
+ > + +
+ > + +
@@ -380,7 +390,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - /> + > + +
@@ -410,7 +422,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + +
@@ -441,7 +455,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - /> + > + +
@@ -468,7 +484,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + +
@@ -548,7 +566,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="vector" size="m" - /> + > + + @@ -578,7 +598,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + + @@ -609,7 +631,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="copyClipboard" size="m" - /> + > + + @@ -636,7 +660,9 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot b/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot index 056b87294f245..5d83b2718f916 100644 --- a/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_dot/__stories__/__snapshots__/color_dot.stories.storyshot @@ -129,7 +129,9 @@ Array [ + > + + ,
+ > + +
,
+ > + +
,
+ > + +
, ] diff --git a/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot index cb3598430c7ef..057bd37b71c20 100644 --- a/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_manager/__stories__/__snapshots__/color_manager.stories.storyshot @@ -394,7 +394,9 @@ exports[`Storyshots components/Color/ColorManager interactive 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + @@ -805,7 +809,9 @@ Array [ color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + , @@ -886,7 +894,9 @@ Array [ color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + , @@ -965,7 +977,9 @@ Array [ color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + , diff --git a/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot b/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot index a0d27eafb23dc..53651c8fe33f2 100644 --- a/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_palette/__stories__/__snapshots__/color_palette.stories.storyshot @@ -393,7 +393,9 @@ Array [ className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + @@ -758,7 +760,9 @@ exports[`Storyshots components/Color/ColorPalette six colors, wrap at 4 1`] = ` className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + @@ -1040,7 +1044,9 @@ Array [ className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot index 6ef3eec47e701..557f94c26fac9 100644 --- a/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/color_picker/__stories__/__snapshots__/color_picker.stories.storyshot @@ -237,7 +237,9 @@ exports[`Storyshots components/Color/ColorPicker interactive 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + @@ -318,7 +322,9 @@ exports[`Storyshots components/Color/ColorPicker six colors 1`] = ` className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + @@ -526,7 +532,9 @@ exports[`Storyshots components/Color/ColorPicker six colors 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + @@ -786,7 +796,9 @@ exports[`Storyshots components/Color/ColorPicker six colors, value missing 1`] = color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + @@ -846,7 +860,9 @@ exports[`Storyshots components/Color/ColorPicker three colors 1`] = ` className="selected-color" color="#333" data-euiicon-type="check" - /> + > + + @@ -970,7 +986,9 @@ exports[`Storyshots components/Color/ColorPicker three colors 1`] = ` color="inherit" data-euiicon-type="plusInCircle" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot index d8c660923e3d7..feb04e68ca1d3 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot @@ -27,7 +27,9 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
@@ -224,7 +228,9 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - /> + > + +
+ > + +
+ > + +
@@ -646,7 +656,9 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
@@ -843,7 +857,9 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - /> + > + +
+ > + +
+ > + +
@@ -1146,7 +1166,9 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = ` className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - /> + > + +
+ > + + + > + + Test Datasource @@ -70,14 +74,18 @@ exports[`Storyshots components/datasource/DatasourceComponent simple datasource color="inherit" data-euiicon-type="arrowRight" size="m" - /> + > + + + > + + Test Datasource diff --git a/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot b/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot index 14640fe266839..05cec59522ae7 100644 --- a/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/element_card/__stories__/__snapshots__/element_card.stories.storyshot @@ -19,7 +19,9 @@ exports[`Storyshots components/Elements/ElementCard with click handler 1`] = ` className="euiCard__icon" data-euiicon-type="canvasApp" size="xxl" - /> + > + +
+ > + +
+ > + +
+ > + +
diff --git a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot index d3ab369dcc32c..0863fd13af607 100644 --- a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot @@ -59,7 +59,9 @@ exports[`Storyshots Home Home Page 1`] = ` color="inherit" data-euiicon-type="plusInCircleFilled" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot index 8f00060a1dd1c..fa3789124ce81 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot @@ -28,7 +28,9 @@ exports[`Storyshots Home/Components/Empty Prompt Empty Prompt 1`] = ` color="subdued" data-euiicon-type="importAction" size="xxl" - /> + > + +
+ > + +
@@ -73,7 +75,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiFilePicker__icon" data-euiicon-type="importAction" size="m" - /> + > + +
@@ -150,7 +154,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -296,7 +302,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiTableSortIcon" data-euiicon-type="sortDown" size="m" - /> + > + + @@ -460,7 +468,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="exportAction" size="m" - /> + > + +
@@ -486,7 +496,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="copy" size="m" - /> + > + +
@@ -632,7 +644,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="exportAction" size="m" - /> + > + +
@@ -658,7 +672,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="copy" size="m" - /> + > + +
@@ -804,7 +820,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="exportAction" size="m" - /> + > + +
@@ -830,7 +848,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="copy" size="m" - /> + > + +
@@ -873,7 +893,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -915,7 +937,9 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - /> + > + +
    + > + +
diff --git a/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot index dbb591582e909..e96302525aea4 100644 --- a/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot @@ -73,7 +73,9 @@ exports[`Storyshots components/ItemGrid complex grid 1`] = ` + > + +
+ > + +
+ > + +
@@ -125,13 +131,19 @@ exports[`Storyshots components/ItemGrid icon grid 1`] = ` > + > + + + > + + + > + + `; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot index 6f139df7c8773..9f462d9a4d6cd 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot @@ -34,7 +34,9 @@ exports[`Storyshots components/SavedElementsModal/ElementControls has two button color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -61,7 +63,9 @@ exports[`Storyshots components/SavedElementsModal/ElementControls has two button color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot index 70ee9f543d768..fbab31e5c8c5b 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot @@ -85,7 +85,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -112,7 +114,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -192,7 +196,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -219,7 +225,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -299,7 +307,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -326,7 +336,9 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot index fd6f29178aa91..e0b7f40657cf8 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot @@ -26,7 +26,9 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
@@ -99,7 +103,9 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` color="subdued" data-euiicon-type="vector" size="xxl" - /> + > + +
+ > + +
+ > + +
@@ -327,7 +337,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -354,7 +366,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -434,7 +448,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -461,7 +477,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -541,7 +559,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -568,7 +588,9 @@ exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + @@ -634,7 +656,9 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` color="inherit" data-euiicon-type="cross" size="m" - /> + > + +
+ > + +
+ > + +
@@ -787,7 +815,9 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` color="inherit" data-euiicon-type="pencil" size="m" - /> + > + + @@ -814,7 +844,9 @@ exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` color="inherit" data-euiicon-type="trash" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot index 6bf2535131afc..d5e5af856909b 100644 --- a/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot @@ -72,7 +72,9 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="sortUp" size="m" - /> + > + + @@ -98,7 +100,9 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="arrowUp" size="m" - /> + > + + @@ -124,7 +128,9 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="arrowDown" size="m" - /> + > + + @@ -150,7 +156,9 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` color="inherit" data-euiicon-type="sortDown" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot index f21ffcf1a70ea..2a1e12c1e0b74 100644 --- a/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot @@ -57,7 +57,9 @@ exports[`Storyshots components/Tags/Tag as health 1`] = ` + > + +
+ > + +
+ > + +
+ > + +
+ > + +
+ > + +
@@ -244,7 +246,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorBold" size="m" - /> + > + + @@ -274,7 +278,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorItalic" size="m" - /> + > + + @@ -304,7 +310,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorUnderline" size="m" - /> + > + + @@ -348,7 +356,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorAlignLeft" size="m" - /> + > + + @@ -384,7 +394,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorAlignCenter" size="m" - /> + > + + @@ -420,7 +432,9 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` color="inherit" data-euiicon-type="editorAlignRight" size="m" - /> + > + + @@ -598,7 +612,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` className="euiFormControlLayoutCustomIcon__icon" data-euiicon-type="arrowDown" size="s" - /> + > + + @@ -690,7 +706,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorBold" size="m" - /> + > + + @@ -720,7 +738,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorItalic" size="m" - /> + > + + @@ -750,7 +770,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorUnderline" size="m" - /> + > + + @@ -794,7 +816,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorAlignLeft" size="m" - /> + > + + @@ -830,7 +854,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorAlignCenter" size="m" - /> + > + + @@ -866,7 +892,9 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` color="inherit" data-euiicon-type="editorAlignRight" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot index f5351b0d8ea5f..0d8a5c0cf4e5d 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot @@ -20,7 +20,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot index 6c70364f9679c..72e1b4d6ef909 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot @@ -20,7 +20,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + @@ -255,7 +259,9 @@ Array [ color="inherit" data-euiicon-type="save" size="m" - /> + > + + @@ -310,7 +316,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + @@ -486,7 +496,9 @@ Array [ color="inherit" data-euiicon-type="save" size="m" - /> + > + + @@ -541,7 +553,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + @@ -717,7 +733,9 @@ Array [ color="inherit" data-euiicon-type="save" size="m" - /> + > + + @@ -772,7 +790,9 @@ Array [ "verticalAlign": "top", } } - /> + > + + + > + + diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot index 7d43840e431ab..ac27b0443585a 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot @@ -28,7 +28,9 @@ exports[`Storyshots components/Variables/VarConfig default 1`] = ` color="inherit" data-euiicon-type="arrowRight" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot index b6d842ac44e21..57fbd4c2109cd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot @@ -33,7 +33,9 @@ exports[`Storyshots components/WorkpadFilters/FiltersGroupComponent default 1`] color="inherit" data-euiicon-type="arrowRight" size="m" - /> + > + +
+ > + +
@@ -1467,7 +1473,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + @@ -2809,7 +2817,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` + > + +
+ > + +
+ > + +
@@ -2949,7 +2963,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + @@ -3107,7 +3123,9 @@ exports[`Storyshots shareables/Canvas contextual: hello 1`] = ` + > + +
+ > + +
+ > + +
@@ -3247,7 +3269,9 @@ exports[`Storyshots shareables/Canvas contextual: hello 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot index 90ebc1900d731..6a8d67a70ad1a 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot @@ -1280,7 +1280,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` + > + +
+ > + +
+ > + +
@@ -1420,7 +1426,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + @@ -1532,7 +1540,9 @@ exports[`Storyshots shareables/Footer contextual: hello 1`] = ` + > + +
+ > + +
+ > + +
@@ -1672,7 +1686,9 @@ exports[`Storyshots shareables/Footer contextual: hello 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot index 9edb6f1fda62f..f2b92754b6d6f 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot @@ -34,7 +34,9 @@ exports[`Storyshots shareables/Footer/PageControls component 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - /> + > + +
+ > + +
@@ -131,7 +135,9 @@ exports[`Storyshots shareables/Footer/PageControls contextual: austin 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - /> + > + +
+ > + +
@@ -228,7 +236,9 @@ exports[`Storyshots shareables/Footer/PageControls contextual: hello 1`] = ` color="inherit" data-euiicon-type="arrowLeft" size="m" - /> + > + +
+ > + +
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot index 2b326fd0ec51a..ea19100f6da87 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot @@ -34,7 +34,9 @@ exports[`Storyshots shareables/Footer/Title component 1`] = ` + > + +
+ > + +
+ > + +
+ > + + + > + + @@ -212,12 +216,16 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: on, 5 className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + @@ -376,12 +384,16 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings contextual 1`] = className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot index 265cbe460607d..3c3f26bce7e9e 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot @@ -39,7 +39,9 @@ exports[`Storyshots shareables/Footer/Settings component 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + +
@@ -87,7 +89,9 @@ exports[`Storyshots shareables/Footer/Settings contextual 1`] = ` color="inherit" data-euiicon-type="gear" size="m" - /> + > + + diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot index 1aafb9cc6b664..d07e5a9edc8ad 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot @@ -59,12 +59,16 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: off 1` className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + @@ -147,12 +151,16 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: on 1`] className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + @@ -235,12 +243,16 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings contextual 1`] = className="euiSwitch__icon" data-euiicon-type="cross" size="m" - /> + > + + + > + + diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index 4fd56525541a6..63fc2e2695a3a 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -264,7 +264,7 @@ describe('ConnectorsDropdown', () => { wrapper: ({ children }) => {children}, }); - const tooltips = screen.getAllByLabelText( + const tooltips = screen.getAllByText( 'This connector is deprecated. Update it, or create a new one.' ); expect(tooltips[0]).toBeInTheDocument(); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx index af803cfc14e05..8cb8b7f23b439 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx @@ -25,7 +25,7 @@ describe('Markdown', () => { test('it renders the expected link text', () => { const result = appMockRender.render({markdownWithLink}); - expect(removeExternalLinkText(result.getByTestId('markdown-link').textContent)).toEqual( + expect(removeExternalLinkText(result.getByTestId('markdown-link').textContent)).toContain( 'External Site' ); }); diff --git a/x-pack/plugins/cloud_security_posture/public/components/csp_page_template.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/csp_page_template.test.tsx index ef097f224ab5f..8cc8c750b8240 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/csp_page_template.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/csp_page_template.test.tsx @@ -102,7 +102,7 @@ describe('', () => { renderCspPageTemplate({ children }); Object.values(PACKAGE_NOT_INSTALLED_TEXT).forEach((text) => - expect(screen.getByText(text)).toBeInTheDocument() + expect(screen.getAllByText(text)[0]).toBeInTheDocument() ); expect(screen.queryByText(children)).not.toBeInTheDocument(); expect(screen.queryByText(LOADING)).not.toBeInTheDocument(); @@ -247,7 +247,7 @@ describe('', () => { }); expect(screen.getByText(pageTitle)).toBeInTheDocument(); - expect(screen.getByText(solution, { exact: false })).toBeInTheDocument(); + expect(screen.getAllByText(solution, { exact: false })[0]).toBeInTheDocument(); expect(screen.queryByText(LOADING)).not.toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); expect(screen.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).not.toBeInTheDocument(); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx index 863e5e85d9ef3..eb1f82cc01e37 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -138,7 +138,7 @@ describe('Background Search Session Management Table', () => { expect(table.find('tbody td').map((node) => node.text())).toMatchInlineSnapshot(` Array [ "App", - "Namevery background search ", + "Namevery background search Info", "# Searches0", "StatusExpired", "Created2 Dec, 2020, 00:19:32", diff --git a/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts b/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts index dada1c0fc91c5..e6c071225316f 100644 --- a/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts +++ b/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts @@ -6,8 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -// import { expectToBeAccessible } from '@kbn/test-jest-helpers'; -import { getA11yViolations } from '@kbn/test-jest-helpers'; +import { expectToBeAccessible } from '@kbn/test-jest-helpers'; import { IndicesTestBed, setup } from '../client_integration/home/indices_tab.helpers'; import { indexMappings, @@ -38,13 +37,7 @@ describe('A11y Indices tab', () => { }); const { component } = testBed; component.update(); - // this is expected to fail and needs to be updated when EUI 52.0.0 is available in Kibana - // await expectToBeAccessible(component); - // until then check that only 1 expected violation is found - const violations = await getA11yViolations(component); - expect(violations).toHaveLength(1); - const { id } = violations[0]; - expect(id).toEqual('aria-allowed-attr'); + await expectToBeAccessible(component); }); it('when there are indices', async () => { @@ -57,13 +50,7 @@ describe('A11y Indices tab', () => { }); const { component } = testBed; component.update(); - // this is expected to fail and needs to be updated when EUI 52.0.0 is available in Kibana - // await expectToBeAccessible(component); - // until then check that only 1 expected violation is found - const violations = await getA11yViolations(component); - expect(violations).toHaveLength(1); - const { id } = violations[0]; - expect(id).toEqual('aria-allowed-attr'); + await expectToBeAccessible(component); }); describe('index details flyout', () => { @@ -86,26 +73,14 @@ describe('A11y Indices tab', () => { it('summary tab', async () => { const { component, find } = testBed; expect(find('detailPanelTabSelected').text()).toEqual('Summary'); - // this is expected to fail and needs to be updated when EUI 52.0.0 is available in Kibana - // await expectToBeAccessible(component); - // until then check that only 1 expected violation is found - const violations = await getA11yViolations(component); - expect(violations).toHaveLength(1); - const { id } = violations[0]; - expect(id).toEqual('aria-allowed-attr'); + await expectToBeAccessible(component); }); ['settings', 'mappings', 'stats'].forEach((tab) => { it(`${tab} tab`, async () => { const { component, find, actions } = testBed; await actions.selectIndexDetailsTab(tab as 'settings'); expect(find('detailPanelTabSelected').text().toLowerCase()).toEqual(tab); - // this is expected to fail and needs to be updated when EUI 52.0.0 is available in Kibana - // await expectToBeAccessible(component); - // until then check that only 1 expected violation is found - const violations = await getA11yViolations(component); - expect(violations).toHaveLength(1); - const { id } = violations[0]; - expect(id).toEqual('aria-allowed-attr'); + await expectToBeAccessible(component); }); }); @@ -113,13 +88,7 @@ describe('A11y Indices tab', () => { const { component, find, actions } = testBed; await actions.selectIndexDetailsTab('edit_settings'); expect(find('detailPanelTabSelected').text()).toEqual('Edit settings'); - // this is expected to fail and needs to be updated when EUI 52.0.0 is available in Kibana - // await expectToBeAccessible(component); - // until then check that only 1 expected violation is found - const violations = await getA11yViolations(component); - expect(violations).toHaveLength(1); - const { id } = violations[0]; - expect(id).toEqual('aria-allowed-attr'); + await expectToBeAccessible(component); }); }); }); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx index 0626a946f8848..d44625b1641ac 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx @@ -65,7 +65,6 @@ export const IndexSetupDatasetFilter: React.FC<{ isSelected={isVisible} onClick={show} iconType="arrowDown" - size="s" > { // Dropdown should be visible and processor status should equal "success" expect(exists('documentsDropdown')).toBe(true); - const initialProcessorStatusLabel = find('processors>0.processorStatusIcon').props()[ - 'aria-label' - ]; + const initialProcessorStatusLabel = find('processors>0.processorStatusIcon').props() + .children; expect(initialProcessorStatusLabel).toEqual('Success'); // Open flyout and click clear all button @@ -320,9 +319,8 @@ describe('Test pipeline', () => { // Verify documents and processors were reset expect(exists('documentsDropdown')).toBe(false); expect(exists('addDocumentsButton')).toBe(true); - const resetProcessorStatusIconLabel = find('processors>0.processorStatusIcon').props()[ - 'aria-label' - ]; + const resetProcessorStatusIconLabel = find('processors>0.processorStatusIcon').props() + .children; expect(resetProcessorStatusIconLabel).toEqual('Not run'); }); }); @@ -332,7 +330,7 @@ describe('Test pipeline', () => { it('should show "inactive" processor status by default', async () => { const { find } = testBed; - const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; + const statusIconLabel = find('processors>0.processorStatusIcon').props().children; expect(statusIconLabel).toEqual('Not run'); }); @@ -352,7 +350,7 @@ describe('Test pipeline', () => { actions.closeTestPipelineFlyout(); // Verify status - const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; + const statusIconLabel = find('processors>0.processorStatusIcon').props().children; expect(statusIconLabel).toEqual('Success'); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 36fd1581cb9b6..2ad20bf0a43e2 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -617,7 +617,9 @@ describe('DatatableComponent', () => { wrapper.setProps({ data: newData }); wrapper.update(); - expect(wrapper.find('[data-test-subj="dataGridHeader"]').children().first().text()).toEqual( + // Using .toContain over .toEqual because this element includes text from + // which can't be seen, but shows in the text content + expect(wrapper.find('[data-test-subj="dataGridHeader"]').children().first().text()).toContain( 'new a' ); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index c86fdcc33b15f..c20f1c37c6c67 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -120,7 +120,10 @@ describe('IndexPattern Field Item', () => { it('should display displayName of a field', () => { const wrapper = mountWithIntl(); - expect(wrapper.find('[data-test-subj="lnsFieldListPanelField"]').first().text()).toEqual( + + // Using .toContain over .toEqual because this element includes text from + // which can't be seen, but shows in the text content + expect(wrapper.find('[data-test-subj="lnsFieldListPanelField"]').first().text()).toContain( 'bytesLabel' ); }); diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index fda479f2888ce..0fd589e4886e3 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription featuresExternal link(opens in a new tab or window), request an extension now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap index 4fa45c4bec5ce..cf977731ee452 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription featuresExternal link(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription featuresExternal link(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription featuresExternal link(opens in a new tab or window).

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap index 622bff86ead16..0880eddcc1683 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription featuresExternal link(opens in a new tab or window) have to offer.

"`; diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap index c5b5e5e65ab38..41501a7eedb62 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap @@ -87,10 +87,11 @@ Array [ > Elasticsearch Service Console + > + External link + @@ -106,10 +107,11 @@ Array [ > Logs and metrics + > + External link + @@ -125,10 +127,11 @@ Array [ > the documentation page. + > + External link + diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap index dda853a28239f..faab608e7af14 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap @@ -158,10 +158,11 @@ Array [ > Elasticsearch Service Console + > + External link + @@ -177,10 +178,11 @@ Array [ > Logs and metrics + > + External link + @@ -196,10 +198,11 @@ Array [ > the documentation page. + > + External link + diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js index a6987fa19d1ee..26af30ba17c04 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js @@ -252,7 +252,7 @@ describe('', () => { ], [ '', - remoteCluster2.name, + remoteCluster2.name.concat('Info'), //Tests include the word "info" to account for the rendered text coming from EuiIcon 'Not connected', PROXY_MODE, remoteCluster2.proxyAddress, @@ -261,7 +261,7 @@ describe('', () => { ], [ '', - remoteCluster3.name, + remoteCluster3.name.concat('Info'), //Tests include the word "info" to account for the rendered text coming from EuiIcon 'Not connected', PROXY_MODE, remoteCluster2.proxyAddress, @@ -360,7 +360,7 @@ describe('', () => { ({ rows } = table.getMetaData('remoteClusterListTable')); expect(rows.length).toBe(2); - expect(rows[0].columns[1].value).toEqual(remoteCluster2.name); + expect(rows[0].columns[1].value).toContain(remoteCluster2.name); }); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx index 7052f724cd1cc..006ae053940d8 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.test.tsx @@ -40,7 +40,7 @@ describe('FeatureTableCell', () => { ); - expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature "`); + expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature Info"`); expect(wrapper.find(EuiIconTip).props().content).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts index 3a70ff5713bd9..f375263c960c3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/__fixtures__/index.ts @@ -60,7 +60,7 @@ export function getDisplayedFeaturePrivileges( acc[feature.id][key] = { ...acc[feature.id][key], - primaryFeaturePrivilege: primary.text().trim(), + primaryFeaturePrivilege: primary.text().replaceAll('Info', '').trim(), // Removing the word "info" to account for the rendered text coming from EuiIcon hasCustomizedSubFeaturePrivileges: findTestSubject(primary, 'additionalPrivilegesGranted').length > 0, }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx index 6070924523f63..a53be08380698 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx @@ -158,7 +158,7 @@ describe('ExceptionEntries', () => { expect(parentValue.text()).toEqual(getEmptyValue()); expect(nestedField.exists('.euiToolTipAnchor')).toBeTruthy(); - expect(nestedField.text()).toEqual('host.name'); + expect(nestedField.text()).toContain('host.name'); expect(nestedOperator.text()).toEqual('is'); expect(nestedValue.text()).toEqual('some name'); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx index 7a9c36a986afd..9796ae2624a73 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx @@ -58,7 +58,7 @@ describe('Modal Inspect', () => { expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="index-pattern-title"]').text() - ).toBe('Index pattern '); + ).toContain('Index pattern '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="index-pattern-description"]') @@ -66,7 +66,7 @@ describe('Modal Inspect', () => { ).toBe('auditbeat-*, filebeat-*, packetbeat-*, winlogbeat-*'); expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="query-time-title"]').text() - ).toBe('Query time '); + ).toContain('Query time '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="query-time-description"]') @@ -76,7 +76,7 @@ describe('Modal Inspect', () => { wrapper .find('.euiDescriptionList__title span[data-test-subj="request-timestamp-title"]') .text() - ).toBe('Request timestamp '); + ).toContain('Request timestamp '); }); test('Click on request Tab', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index 97f93b9732c02..adab4db904d6a 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -105,7 +105,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders props passed in as link', () => { @@ -463,7 +463,7 @@ describe('Custom Links', () => { describe('WhoisLink', () => { test('it renders ip passed in as domain', () => { const wrapper = mountWithIntl({'Example Link'}); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders correct href', () => { @@ -488,7 +488,7 @@ describe('Custom Links', () => { {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders correct href', () => { @@ -519,7 +519,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders correct href', () => { @@ -548,7 +548,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toContain('Example Link'); }); test('it renders correct href when port is a number', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx index da3785648de62..68588c9338b4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx @@ -20,7 +20,7 @@ describe('Markdown', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="markdown-link"]').first().text()) - ).toEqual('External Site'); + ).toContain('External Site'); }); test('it renders the expected href', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx index 480d200c6756f..ec56dd6934463 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx @@ -54,9 +54,9 @@ describe('Port', () => { ); - expect(removeExternalLinkText(wrapper.find('[data-test-subj="port"]').first().text())).toEqual( - '443' - ); + expect( + removeExternalLinkText(wrapper.find('[data-test-subj="port"]').first().text()) + ).toContain('443'); }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx index 3332111d14f8b..bb8b4683c9d30 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx @@ -205,7 +205,7 @@ describe('SourceDestination', () => { removeExternalLinkText( wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() ) - ).toEqual('10.1.2.3:80'); + ).toContain('10.1.2.3:80'); }); test('it renders destination.packets', () => { @@ -329,7 +329,7 @@ describe('SourceDestination', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) - ).toEqual('192.168.1.2:9987'); + ).toContain('192.168.1.2:9987'); }); test('it renders source.packets', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx index f16cd7dbb109f..6168d98765253 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx @@ -984,7 +984,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() ) - ).toEqual('9987'); + ).toContain('9987'); }); test('it renders the expected destination port when type is `destination`, and both destinationIp and destinationPort are populated', () => { @@ -1038,7 +1038,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() ) - ).toEqual('80'); + ).toContain('80'); }); test('it renders the expected source port when type is `source`, but only sourcePort is populated', () => { @@ -1092,7 +1092,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() ) - ).toEqual('9987'); + ).toContain('9987'); }); test('it renders the expected destination port when type is `destination`, and only destinationPort is populated', () => { @@ -1147,7 +1147,7 @@ describe('SourceDestinationIp', () => { removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() ) - ).toEqual('80'); + ).toContain('80'); }); test('it does NOT render the badge when type is `source`, but both sourceIp and sourcePort are undefined', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx index 4ebb804eab8a4..8b3f0bfdb107a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx @@ -51,7 +51,7 @@ describe('CertificateFingerprint', () => { removeExternalLinkText( wrapper.find('[data-test-subj="certificate-fingerprint-link"]').first().text() ) - ).toEqual('3f4c57934e089f02ae7511200aee2d7e7aabd272'); + ).toContain('3f4c57934e089f02ae7511200aee2d7e7aabd272'); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx index 31f2fec942490..ddbba7f2bc9f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx @@ -48,7 +48,7 @@ describe('Ja3Fingerprint', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) - ).toEqual('fff799d91b7c01ae3fe6787cfc895552'); + ).toContain('fff799d91b7c01ae3fe6787cfc895552'); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx index 8a88a7182af03..9ccabf2f47d44 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx @@ -204,7 +204,7 @@ describe('Netflow', () => { removeExternalLinkText( wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() ) - ).toEqual('10.1.2.3:80'); + ).toContain('10.1.2.3:80'); }); test('it renders destination.packets', () => { @@ -340,7 +340,7 @@ describe('Netflow', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) - ).toEqual('192.168.1.2:9987'); + ).toContain('192.168.1.2:9987'); }); test('it renders source.packets', () => { @@ -374,7 +374,7 @@ describe('Netflow', () => { .first() .text() ) - ).toEqual('tls.client_certificate.fingerprint.sha1-value'); + ).toContain('tls.client_certificate.fingerprint.sha1-value'); }); test('it hyperlinks tls.fingerprints.ja3.hash site to compare the fingerprint against a known set of signatures', () => { @@ -390,7 +390,7 @@ describe('Netflow', () => { expect( removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) - ).toEqual('tls.fingerprints.ja3.hash-value'); + ).toContain('tls.fingerprints.ja3.hash-value'); }); test('it hyperlinks tls.server_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { @@ -418,7 +418,7 @@ describe('Netflow', () => { .first() .text() ) - ).toEqual('tls.server_certificate.fingerprint.sha1-value'); + ).toContain('tls.server_certificate.fingerprint.sha1-value'); }); test('it renders network.transport', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 6ea24e5ca57f6..ebb807a590124 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -2686,11 +2686,12 @@ exports[`Details Panel Component DetailsPanel:NetworkDetails: rendering it shoul type="popout" > + > + External link + { + return str.replaceAll('External link', ''); +}; + jest.mock('../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { @@ -90,7 +96,7 @@ describe('get_column_renderer', () => { {row} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -109,7 +115,7 @@ describe('get_column_renderer', () => { {row} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -128,7 +134,7 @@ describe('get_column_renderer', () => { {row} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 2d06c040c5b00..f8693d4a4f8ea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -53,7 +53,11 @@ describe('SuricataDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + const removeEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( + 'External link', + '' + ); + expect(removeEuiIconText).toEqual( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index 61ea659964e4d..2022904e548aa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -72,7 +72,12 @@ describe('suricata_row_renderer', () => { {children} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + + const extractEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( + 'External link', + '' + ); + expect(extractEuiIconText).toContain( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index ae2caa8ce8401..4b93c5accb590 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -83,6 +83,12 @@ import { import * as i18n from './translations'; import { RowRenderer } from '../../../../../../../common/types'; +// EuiIcons coming from .testenv render the icon's aria-label as a span +// extractEuiIcon removes the aria-label before checking for equality +const extractEuiIconText = (str: string) => { + return str.replaceAll('External link', ''); +}; + jest.mock('../../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { @@ -1130,7 +1136,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'NETWORK SERVICE\\NT AUTHORITY@windows-endpoint-1accepted a connection viasvchost.exe(328)with resultsuccessEndpoint network eventincomingtcpSource10.1.2.3:64557North AmericaUnited States🇺🇸USNorth CarolinaConcordDestination10.50.60.70:3389' ); }); @@ -1214,7 +1220,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'NETWORK SERVICE\\NT AUTHORITY@win2019-endpoint-1made a http request viasvchost.exe(2232)Endpoint network eventoutgoinghttptcpSource10.1.2.3:51570Destination10.11.12.13:80North AmericaUnited States🇺🇸USArizonaPhoenix' ); }); @@ -1243,7 +1249,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' ); }); @@ -1272,7 +1278,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' ); }); @@ -1298,7 +1304,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'NETWORK SERVICE\\NT AUTHORITY@windows-endpoint-1disconnected viasvchost.exe(328)Endpoint network eventincomingtcpSource10.20.30.40:64557North AmericaUnited States🇺🇸USNorth CarolinaConcord(42.47%)1.2KB(57.53%)1.6KBDestination10.11.12.13:3389' ); }); @@ -1327,7 +1333,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' ); }); @@ -1356,7 +1362,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' ); }); @@ -1385,7 +1391,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' ); }); @@ -1414,7 +1420,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' ); }); @@ -1722,7 +1728,7 @@ describe('GenericRowRenderer', () => { ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toBe( 'iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 62836cbffb2b5..9af22fca0c707 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -14,6 +14,12 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ZeekDetails } from './zeek_details'; +// EuiIcons coming from .testenv render the icon's aria-label as a span +// extractEuiIcon removes the aria-label before checking for equality +const extractEuiIconText = (str: string) => { + return str.replaceAll('External link', ''); +}; + jest.mock('../../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { @@ -53,7 +59,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); @@ -68,7 +74,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'CyIrMA1L1JtLqdIuoldnsudpSource206.189.35.240:57475Destination67.207.67.3:53' ); }); @@ -83,7 +89,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'CZLkpC22NquQJOpkwehttp302Source206.189.35.240:36220Destination192.241.164.26:80' ); }); @@ -98,7 +104,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'noticeDropped:falseScan::Port_Scan8.42.77.171 scanned at least 15 unique ports of host 207.154.238.205 in 0m0sSource8.42.77.171' ); }); @@ -113,7 +119,7 @@ describe('ZeekDetails', () => { /> ); - expect(removeExternalLinkText(wrapper.text())).toEqual( + expect(extractEuiIconText(removeExternalLinkText(wrapper.text()))).toEqual( 'CmTxzt2OVXZLkGDaResslTLSv12TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256Source188.166.66.184:34514Destination91.189.95.15:443' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index b60a2965bfd70..fda83c0ade12b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -71,7 +71,12 @@ describe('zeek_row_renderer', () => { {children} ); - expect(removeExternalLinkText(wrapper.text())).toContain( + + const extractEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( + 'External link', + '' + ); + expect(extractEuiIconText).toContain( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index 3f27b80359131..726716c7f53ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -101,7 +101,11 @@ describe('ZeekSignature', () => { test('should render value', () => { const wrapper = mount(); - expect(removeExternalLinkText(wrapper.text())).toEqual('abc'); + const extractEuiIconText = removeExternalLinkText(wrapper.text()).replaceAll( + 'External link', + '' + ); + expect(extractEuiIconText).toEqual('abc'); }); test('should render value and link', () => { diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index 60fe9d2bd7128..a99a6fdb81167 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -769,7 +769,7 @@ describe('', () => { const stateMessage = find('snapshotDetail.state.value').text(); try { - expect(stateMessage).toBe(expectedMessage); + expect(stateMessage).toContain(expectedMessage); // Messages may include the word "Info" to account for the rendered text coming from EuiIcon } catch { throw new Error( `Expected snapshot state message "${expectedMessage}" for state "${state}, but got "${stateMessage}".` diff --git a/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx b/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx index 5ac75f92ea45f..f3846cd784ccc 100644 --- a/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx +++ b/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx @@ -110,7 +110,7 @@ describe('Modal Inspect', () => { expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="index-pattern-title"]').text() - ).toBe('Index pattern '); + ).toContain('Index pattern '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="index-pattern-description"]') @@ -118,7 +118,7 @@ describe('Modal Inspect', () => { ).toBe('auditbeat-*, filebeat-*, packetbeat-*, winlogbeat-*'); expect( wrapper.find('.euiDescriptionList__title span[data-test-subj="query-time-title"]').text() - ).toBe('Query time '); + ).toContain('Query time '); expect( wrapper .find('.euiDescriptionList__description span[data-test-subj="query-time-description"]') @@ -128,7 +128,7 @@ describe('Modal Inspect', () => { wrapper .find('.euiDescriptionList__title span[data-test-subj="request-timestamp-title"]') .text() - ).toBe('Request timestamp '); + ).toContain('Request timestamp '); }); test('Click on request Tab', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index d23f1cfacf94b..6942a7708db78 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -107,10 +107,12 @@ describe('health check', () => { const [action] = queryAllByText(/Learn more/i); expect(description.textContent).toMatchInlineSnapshot( - `"You must enable API keys to use Alerting. Learn more.(opens in a new tab or window)"` + `"You must enable API keys to use Alerting. Learn more.External link(opens in a new tab or window)"` ); - expect(action.textContent).toMatchInlineSnapshot(`"Learn more.(opens in a new tab or window)"`); + expect(action.textContent).toMatchInlineSnapshot( + `"Learn more.External link(opens in a new tab or window)"` + ); expect(action.getAttribute('href')).toMatchInlineSnapshot( `"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-settings.html#api-key-service-settings"` @@ -141,12 +143,12 @@ describe('health check', () => { const description = queryByRole(/banner/i); expect(description!.textContent).toMatchInlineSnapshot( - `"You must configure an encryption key to use Alerting. Learn more.(opens in a new tab or window)"` + `"You must configure an encryption key to use Alerting. Learn more.External link(opens in a new tab or window)"` ); const action = queryByText(/Learn/i); expect(action!.textContent).toMatchInlineSnapshot( - `"Learn more.(opens in a new tab or window)"` + `"Learn more.External link(opens in a new tab or window)"` ); expect(action!.getAttribute('href')).toMatchInlineSnapshot( `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alert-action-settings-kb.html#general-alert-action-settings"` @@ -179,12 +181,12 @@ describe('health check', () => { const description = queryByText(/You must enable/i); expect(description!.textContent).toMatchInlineSnapshot( - `"You must enable API keys and configure an encryption key to use Alerting. Learn more.(opens in a new tab or window)"` + `"You must enable API keys and configure an encryption key to use Alerting. Learn more.External link(opens in a new tab or window)"` ); const action = queryByText(/Learn/i); expect(action!.textContent).toMatchInlineSnapshot( - `"Learn more.(opens in a new tab or window)"` + `"Learn more.External link(opens in a new tab or window)"` ); expect(action!.getAttribute('href')).toMatchInlineSnapshot( `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alerting-setup.html#alerting-prerequisites"` diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx index 737501f444300..e7cafb23ee0fa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx @@ -373,9 +373,12 @@ describe('execution duration overview', () => { const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]'); expect(avgExecutionDurationPanel.exists()).toBeTruthy(); expect(avgExecutionDurationPanel.first().prop('color')).toEqual('warning'); - expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual( - 'Average duration16:44:44.345' - ); + + const avgExecutionDurationStat = wrapper + .find('EuiStat[data-test-subj="avgExecutionDurationStat"]') + .text() + .replaceAll('Info', ''); + expect(avgExecutionDurationStat).toEqual('Average duration16:44:44.345'); expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeTruthy(); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx index ee485f8aee0c0..3576d7e34fd0b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx @@ -63,5 +63,5 @@ test('Can delete drilldowns', () => { test('Error is displayed', () => { const screen = render(); - expect(screen.getByLabelText('an error')).toBeInTheDocument(); + expect(screen.getByText('an error')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap index 51753d2ce8bb3..bf25513a6bc2c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap @@ -179,10 +179,11 @@ exports[`PingListExpandedRow renders link to docs if body is not recorded but it > docs + > + External link + diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap index 80b751d8e243b..29d1ba922de8f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__snapshots__/monitor_status.bar.test.tsx.snap @@ -72,10 +72,11 @@ Array [ > Set tags + > + External link + diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx index 63b4d2945a51c..671371093c819 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx @@ -11,7 +11,7 @@ import { render } from '../../../../../lib/helper/rtl_helpers'; describe('NetworkRequestsTotal', () => { it('message in case total is greater than fetched', () => { - const { getByText, getByLabelText } = render( + const { getByText } = render( { ); expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); - expect(getByLabelText('Info')).toBeInTheDocument(); + expect(getByText('Info')).toBeInTheDocument(); }); it('message in case total is equal to fetched requests', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx index 7558a82e45df4..4241a7238ecd6 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx @@ -13,8 +13,8 @@ import { TestWrapper } from './waterfall_marker_test_helper'; describe('', () => { it('renders a dot icon when `field` is an empty string', () => { - const { getByLabelText } = render(); - expect(getByLabelText('An icon indicating that this marker has no field associated with it')); + const { getByText } = render(); + expect(getByText('An icon indicating that this marker has no field associated with it')); }); it('renders an embeddable when opened', async () => { diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx index 2b899aad783d7..d232b12f3a47b 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/key_ux_metrics.test.tsx @@ -45,20 +45,22 @@ describe('KeyUXMetrics', () => { }; }; + // Tests include the word "info" between the task and time to account for the rendered text coming from + // the EuiIcon (tooltip) embedded within each stat description expect( - getAllByText(checkText('Longest long task duration271 ms'))[0] + getAllByText(checkText('Longest long task durationInfo271 ms'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('Total long tasks duration520 ms'))[0] + getAllByText(checkText('Total long tasks durationInfo520 ms'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('No. of long tasks3'))[0] + getAllByText(checkText('No. of long tasksInfo3'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('Total blocking time271 ms'))[0] + getAllByText(checkText('Total blocking timeInfo271 ms'))[0] ).toBeInTheDocument(); expect( - getAllByText(checkText('First contentful paint1.27 s'))[0] + getAllByText(checkText('First contentful paintInfo1.27 s'))[0] ).toBeInTheDocument(); }); }); diff --git a/yarn.lock b/yarn.lock index c13acd20ed888..7168c8af39761 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1516,10 +1516,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@51.1.0": - version "51.1.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-51.1.0.tgz#338b710ae7a819bb7c3b8e1916080610e0b8e691" - integrity sha512-pjbBSkfDPAjXBRCMk4zsyZ3sPpf70XVcbOzr4BzT0MW38uKjEgEh6nu1aCdnOi+jVSHRtziJkX9rD8BRDWfsnw== +"@elastic/eui@52.2.0": + version "52.2.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-52.2.0.tgz#761101a29b96a4b5270ef93541dab7bb27f5ca50" + integrity sha512-XboYerntCOTHWHYMWJGzJtu5JYO6pk5IWh0ZHJEQ4SEjmLbTV2bFrVBTO/8uaU7GhV9/RNIo7BU5wHRyYP7z1g== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" From 118321fff993ad0a802237dee9f43e133b9af5bd Mon Sep 17 00:00:00 2001 From: Nick Peihl Date: Wed, 30 Mar 2022 00:17:55 -0400 Subject: [PATCH 056/108] [Maps] Support custom icons in maps (#113144) Adds support for users to upload their own SVG icons for styling geo_points or clusters in Elastic Maps. Because Elastic Maps uses WebGL, dynamic styling requires rendering SVG icons as monochromatic PNGs using a Signed Distance Function algorithm. As a result, highly detailed designs and any color definitions in SVGs uploaded to Maps are not retained. Monochromatic SVG icons work best. Custom icons are appended to the map saved object and are not accessible in other maps. Using the custom icon in another map requires creating a copy of the existing map or uploading the icon to the new map. Custom icons can be added, edited, or deleted in Map Settings. Custom icons can also be added from within the Icon select menu. --- docs/maps/vector-style-properties.asciidoc | 1 - x-pack/plugins/maps/common/constants.ts | 11 + .../style_property_descriptor_types.ts | 13 + .../maps/public/actions/layer_actions.test.ts | 5 + .../maps/public/actions/layer_actions.ts | 4 +- .../maps/public/actions/map_actions.ts | 58 ++- .../maps/public/classes/layers/layer.tsx | 9 +- .../blended_vector_layer.test.tsx | 8 + .../blended_vector_layer.ts | 3 + .../mvt_vector_layer.test.tsx | 2 +- .../mvt_vector_layer/mvt_vector_layer.tsx | 4 +- .../layers/vector_layer/vector_layer.test.tsx | 2 + .../layers/vector_layer/vector_layer.tsx | 4 + .../maps/public/classes/styles/_index.scss | 2 + .../maps/public/classes/styles/style.ts | 5 +- .../__snapshots__/vector_icon.test.tsx.snap | 12 + .../components/legend/breaked_legend.tsx | 10 +- .../vector/components/legend/category.tsx | 12 +- .../vector/components/legend/symbol_icon.tsx | 8 +- .../components/legend/vector_icon.test.tsx | 16 + .../vector/components/legend/vector_icon.tsx | 25 +- .../components/legend/vector_style_legend.tsx | 4 +- .../vector/components/style_prop_editor.tsx | 3 + .../custom_icon_modal.test.tsx.snap | 311 ++++++++++++++ .../icon_map_select.test.tsx.snap | 49 ++- .../__snapshots__/icon_select.test.js.snap | 254 ++++++++--- .../components/symbol/_custom_icon_modal.scss | 8 + .../components/symbol/_icon_preview.scss | 3 + .../components/symbol/_icon_select.scss | 2 +- .../symbol/custom_icon_modal.test.tsx | 42 ++ .../components/symbol/custom_icon_modal.tsx | 393 ++++++++++++++++++ .../components/symbol/dynamic_icon_form.js | 4 + .../symbol/icon_map_select.test.tsx | 55 ++- .../components/symbol/icon_map_select.tsx | 17 +- .../vector/components/symbol/icon_preview.tsx | 223 ++++++++++ .../vector/components/symbol/icon_select.js | 140 ++++++- .../components/symbol/icon_select.test.js | 46 +- .../vector/components/symbol/icon_stops.js | 229 +++++----- .../components/symbol/static_icon_form.js | 21 +- .../components/vector_style_editor.test.tsx | 6 +- .../vector/components/vector_style_editor.tsx | 5 + .../classes/styles/vector/maki_icons.ts | 2 +- .../dynamic_icon_property.test.tsx.snap | 92 ++++ .../properties/dynamic_color_property.tsx | 14 +- .../properties/dynamic_icon_property.test.tsx | 37 +- .../properties/dynamic_icon_property.tsx | 9 +- .../properties/dynamic_size_property.tsx | 7 +- .../vector/properties/static_icon_property.ts | 2 +- .../vector/properties/static_size_property.ts | 6 +- .../vector/properties/style_property.ts | 1 + .../properties/test_helpers/test_util.ts | 8 + .../classes/styles/vector/style_util.test.ts | 60 ++- .../classes/styles/vector/style_util.ts | 8 +- .../classes/styles/vector/symbol_utils.js | 50 ++- .../styles/vector/symbol_utils.test.js | 11 +- .../styles/vector/vector_style.test.js | 24 +- .../classes/styles/vector/vector_style.tsx | 76 +++- .../edit_layer_panel/style_settings/index.ts | 7 +- .../style_settings/style_settings.tsx | 7 +- .../custom_icons_panel.test.tsx.snap | 125 ++++++ .../custom_icons_panel.test.tsx | 52 +++ .../map_settings_panel/custom_icons_panel.tsx | 202 +++++++++ .../map_settings_panel/index.ts | 17 +- .../map_settings_panel/map_settings_panel.tsx | 15 +- .../connected_components/mb_map/index.ts | 2 + .../connected_components/mb_map/mb_map.tsx | 36 +- .../reducers/map/default_map_settings.ts | 1 + .../plugins/maps/public/reducers/map/types.ts | 2 + .../public/selectors/map_selectors.test.ts | 16 +- .../maps/public/selectors/map_selectors.ts | 37 +- yarn.lock | 24 ++ 71 files changed, 2646 insertions(+), 333 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/custom_icon_modal.test.tsx.snap create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_custom_icon_modal.scss create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_preview.scss create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/custom_icon_modal.test.tsx create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/custom_icon_modal.tsx create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_preview.tsx create mode 100644 x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/custom_icons_panel.test.tsx.snap create mode 100644 x-pack/plugins/maps/public/connected_components/map_settings_panel/custom_icons_panel.test.tsx create mode 100644 x-pack/plugins/maps/public/connected_components/map_settings_panel/custom_icons_panel.tsx diff --git a/docs/maps/vector-style-properties.asciidoc b/docs/maps/vector-style-properties.asciidoc index be1b2c5e25285..ac06ba32e6e40 100644 --- a/docs/maps/vector-style-properties.asciidoc +++ b/docs/maps/vector-style-properties.asciidoc @@ -61,7 +61,6 @@ Available icons [role="screenshot"] image::maps/images/maki-icons.png[] - [float] [[polygon-style-properties]] ==== Polygon style properties diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index e02fead277f60..b51259307f3a1 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -218,6 +218,17 @@ export enum LABEL_BORDER_SIZES { } export const DEFAULT_ICON = 'marker'; +export const DEFAULT_CUSTOM_ICON_CUTOFF = 0.25; +export const DEFAULT_CUSTOM_ICON_RADIUS = 0.25; +export const CUSTOM_ICON_SIZE = 64; +export const CUSTOM_ICON_PREFIX_SDF = '__kbn__custom_icon_sdf__'; +export const MAKI_ICON_SIZE = 16; +export const HALF_MAKI_ICON_SIZE = MAKI_ICON_SIZE / 2; + +export enum ICON_SOURCE { + CUSTOM = 'CUSTOM', + MAKI = 'MAKI', +} export enum VECTOR_STYLES { SYMBOLIZE_AS = 'symbolizeAs', diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts index 1a334448e9208..dce4d0f9df50e 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts @@ -10,6 +10,7 @@ import { COLOR_MAP_TYPE, FIELD_ORIGIN, + ICON_SOURCE, LABEL_BORDER_SIZES, SYMBOLIZE_AS_TYPES, VECTOR_STYLES, @@ -60,6 +61,7 @@ export type CategoryColorStop = { export type IconStop = { stop: string | null; icon: string; + iconSource?: ICON_SOURCE; }; export type ColorDynamicOptions = { @@ -108,6 +110,9 @@ export type IconDynamicOptions = { export type IconStaticOptions = { value: string; // icon id + label?: string; + svg?: string; + iconSource?: ICON_SOURCE; }; export type IconStylePropertyDescriptor = @@ -178,6 +183,14 @@ export type SizeStylePropertyDescriptor = options: SizeDynamicOptions; }; +export type CustomIcon = { + symbolId: string; + svg: string; // svg string + label: string; // user given label + cutoff: number; + radius: number; +}; + export type VectorStylePropertiesDescriptor = { [VECTOR_STYLES.SYMBOLIZE_AS]: SymbolizeAsStylePropertyDescriptor; [VECTOR_STYLES.FILL_COLOR]: ColorStylePropertyDescriptor; diff --git a/x-pack/plugins/maps/public/actions/layer_actions.test.ts b/x-pack/plugins/maps/public/actions/layer_actions.test.ts index 06adbed92c0cf..cbec68e5108f8 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.test.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.test.ts @@ -39,6 +39,11 @@ describe('layer_actions', () => { return true; }; + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../selectors/map_selectors').getCustomIcons = () => { + return []; + }; + // eslint-disable-next-line @typescript-eslint/no-var-requires require('../selectors/map_selectors').createLayerInstance = () => { return { diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index b081ed6d34979..a89172f8ce340 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -11,6 +11,7 @@ import { Query } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { createLayerInstance, + getCustomIcons, getEditState, getLayerById, getLayerList, @@ -174,8 +175,7 @@ export function addLayer(layerDescriptor: LayerDescriptor) { layer: layerDescriptor, }); dispatch(syncDataForLayerId(layerDescriptor.id, false)); - - const layer = createLayerInstance(layerDescriptor); + const layer = createLayerInstance(layerDescriptor, getCustomIcons(getState())); const features = await layer.getLicensedFeatures(); features.forEach(notifyLicensedFeatureUsage); }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index cccb49f360622..24a378335bc56 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -14,10 +14,11 @@ import turfBooleanContains from '@turf/boolean-contains'; import { Filter } from '@kbn/es-query'; import { Query, TimeRange } from 'src/plugins/data/public'; import { Geometry, Position } from 'geojson'; -import { asyncForEach } from '@kbn/std'; -import { DRAW_MODE, DRAW_SHAPE } from '../../common/constants'; +import { asyncForEach, asyncMap } from '@kbn/std'; +import { DRAW_MODE, DRAW_SHAPE, LAYER_STYLE_TYPE } from '../../common/constants'; import type { MapExtentState, MapViewContext } from '../reducers/map/types'; import { MapStoreState } from '../reducers/store'; +import { IVectorStyle } from '../classes/styles/vector/vector_style'; import { getDataFilters, getFilters, @@ -60,7 +61,13 @@ import { } from './data_request_actions'; import { addLayer, addLayerWithoutDataSync } from './layer_actions'; import { MapSettings } from '../reducers/map'; -import { DrawState, MapCenterAndZoom, MapExtent, Timeslice } from '../../common/descriptor_types'; +import { + CustomIcon, + DrawState, + MapCenterAndZoom, + MapExtent, + Timeslice, +} from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; import { updateTooltipStateForLayer } from './tooltip_actions'; import { isVectorLayer, IVectorLayer } from '../classes/layers/vector_layer'; @@ -108,6 +115,51 @@ export function updateMapSetting( }; } +export function updateCustomIcons(customIcons: CustomIcon[]) { + return { + type: UPDATE_MAP_SETTING, + settingKey: 'customIcons', + settingValue: customIcons.map((icon) => { + return { ...icon, svg: Buffer.from(icon.svg).toString('base64') }; + }), + }; +} + +export function deleteCustomIcon(value: string) { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { + const layersContainingCustomIcon = getLayerList(getState()).filter((layer) => { + const style = layer.getCurrentStyle(); + if (!style || style.getType() !== LAYER_STYLE_TYPE.VECTOR) { + return false; + } + return (style as IVectorStyle).isUsingCustomIcon(value); + }); + + if (layersContainingCustomIcon.length > 0) { + const layerList = await asyncMap(layersContainingCustomIcon, async (layer) => { + return await layer.getDisplayName(); + }); + getToasts().addWarning( + i18n.translate('xpack.maps.mapActions.deleteCustomIconWarning', { + defaultMessage: `Unable to delete icon. The icon is in use by the {count, plural, one {layer} other {layers}}: {layerNames}`, + values: { + count: layerList.length, + layerNames: layerList.join(', '), + }, + }) + ); + } else { + const newIcons = getState().map.settings.customIcons.filter( + ({ symbolId }) => symbolId !== value + ); + dispatch(updateMapSetting('customIcons', newIcons)); + } + }; +} + export function mapReady() { return ( dispatch: ThunkDispatch, diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 99afa21a3f003..458a8a159a25d 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -26,6 +26,7 @@ import { import { copyPersistentState } from '../../reducers/copy_persistent_state'; import { Attribution, + CustomIcon, LayerDescriptor, MapExtent, StyleDescriptor, @@ -92,7 +93,8 @@ export interface ILayer { isVisible(): boolean; cloneDescriptor(): Promise; renderStyleEditor( - onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void, + onCustomIconsChange: (customIcons: CustomIcon[]) => void ): ReactElement | null; getInFlightRequestTokens(): symbol[]; getPrevRequestToken(dataId: string): symbol | undefined; @@ -431,13 +433,14 @@ export class AbstractLayer implements ILayer { } renderStyleEditor( - onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void, + onCustomIconsChange: (customIcons: CustomIcon[]) => void ): ReactElement | null { const style = this.getStyleForEditing(); if (!style) { return null; } - return style.renderEditor(onStyleDescriptorChange); + return style.renderEditor(onStyleDescriptorChange, onCustomIconsChange); } getIndexPatternIds(): string[] { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.test.tsx index ee97f4c243491..f2ef7ca9588be 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.test.tsx @@ -10,6 +10,7 @@ import { BlendedVectorLayer } from './blended_vector_layer'; import { ESSearchSource } from '../../../sources/es_search_source'; import { AbstractESSourceDescriptor, + CustomIcon, ESGeoGridSourceDescriptor, } from '../../../../../common/descriptor_types'; @@ -23,6 +24,8 @@ jest.mock('../../../../kibana_services', () => { const mapColors: string[] = []; +const customIcons: CustomIcon[] = []; + const notClusteredDataRequest = { data: { isSyncClustered: false }, dataId: 'ACTIVE_COUNT_DATA_ID', @@ -51,6 +54,7 @@ describe('getSource', () => { }, mapColors ), + customIcons, }); const source = blendedVectorLayer.getSource(); @@ -72,6 +76,7 @@ describe('getSource', () => { }, mapColors ), + customIcons, }); const source = blendedVectorLayer.getSource(); @@ -112,6 +117,7 @@ describe('getSource', () => { }, mapColors ), + customIcons, }); const source = blendedVectorLayer.getSource(); @@ -132,6 +138,7 @@ describe('cloneDescriptor', () => { }, mapColors ), + customIcons, }); const clonedLayerDescriptor = await blendedVectorLayer.cloneDescriptor(); @@ -151,6 +158,7 @@ describe('cloneDescriptor', () => { }, mapColors ), + customIcons, }); const clonedLayerDescriptor = await blendedVectorLayer.cloneDescriptor(); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts index 91421a31219cb..e46c670b677ba 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts @@ -31,6 +31,7 @@ import { ISource } from '../../../sources/source'; import { DataRequestContext } from '../../../../actions'; import { DataRequestAbortError } from '../../../util/data_request'; import { + CustomIcon, VectorStyleDescriptor, SizeDynamicOptions, DynamicStylePropertyOptions, @@ -171,6 +172,7 @@ export interface BlendedVectorLayerArguments { chartsPaletteServiceGetColor?: (value: string) => string | null; source: IVectorSource; layerDescriptor: VectorLayerDescriptor; + customIcons: CustomIcon[]; } export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLayer { @@ -207,6 +209,7 @@ export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLay clusterStyleDescriptor, this._clusterSource, this, + options.customIcons, options.chartsPaletteServiceGetColor ); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx index 4e4e76d3634f4..27d377851152e 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx @@ -58,7 +58,7 @@ function createLayer( sourceDescriptor, }; const layerDescriptor = MvtVectorLayer.createDescriptor(defaultLayerOptions); - return new MvtVectorLayer({ layerDescriptor, source: mvtSource }); + return new MvtVectorLayer({ layerDescriptor, source: mvtSource, customIcons: [] }); } describe('visiblity', () => { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx index 5947013dc39f1..325e302c0941a 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -54,8 +54,8 @@ export class MvtVectorLayer extends AbstractVectorLayer { readonly _source: IMvtVectorSource; - constructor({ layerDescriptor, source }: VectorLayerArguments) { - super({ layerDescriptor, source }); + constructor({ layerDescriptor, source, customIcons }: VectorLayerArguments) { + super({ layerDescriptor, source, customIcons }); this._source = source as IMvtVectorSource; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx index bd2c8a036bf59..5b91e5e49c514 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx @@ -86,6 +86,7 @@ describe('cloneDescriptor', () => { const layer = new AbstractVectorLayer({ layerDescriptor, source: new MockSource() as unknown as IVectorSource, + customIcons: [], }); const clonedDescriptor = await layer.cloneDescriptor(); const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; @@ -123,6 +124,7 @@ describe('cloneDescriptor', () => { const layer = new AbstractVectorLayer({ layerDescriptor, source: new MockSource() as unknown as IVectorSource, + customIcons: [], }); const clonedDescriptor = await layer.cloneDescriptor(); const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index b8b90fdf75ff4..17408a2a22df0 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -38,6 +38,7 @@ import { } from '../../util/mb_filter_expressions'; import { AggDescriptor, + CustomIcon, DynamicStylePropertyOptions, DataFilters, ESTermSourceDescriptor, @@ -70,6 +71,7 @@ export interface VectorLayerArguments { source: IVectorSource; joins?: InnerJoin[]; layerDescriptor: VectorLayerDescriptor; + customIcons: CustomIcon[]; chartsPaletteServiceGetColor?: (value: string) => string | null; } @@ -133,6 +135,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { layerDescriptor, source, joins = [], + customIcons, chartsPaletteServiceGetColor, }: VectorLayerArguments) { super({ @@ -144,6 +147,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { layerDescriptor.style, source, this, + customIcons, chartsPaletteServiceGetColor ); } diff --git a/x-pack/plugins/maps/public/classes/styles/_index.scss b/x-pack/plugins/maps/public/classes/styles/_index.scss index bd1467bed9d4e..1cc5f4423d02f 100644 --- a/x-pack/plugins/maps/public/classes/styles/_index.scss +++ b/x-pack/plugins/maps/public/classes/styles/_index.scss @@ -2,5 +2,7 @@ @import 'vector/components/style_prop_editor'; @import 'vector/components/color/color_stops'; @import 'vector/components/symbol/icon_select'; +@import 'vector/components/symbol/icon_preview'; +@import 'vector/components/symbol/custom_icon_modal'; @import 'vector/components/legend/category'; @import 'vector/components/legend/vector_legend'; diff --git a/x-pack/plugins/maps/public/classes/styles/style.ts b/x-pack/plugins/maps/public/classes/styles/style.ts index c8326f365f42b..bafb0e9c36d75 100644 --- a/x-pack/plugins/maps/public/classes/styles/style.ts +++ b/x-pack/plugins/maps/public/classes/styles/style.ts @@ -6,11 +6,12 @@ */ import { ReactElement } from 'react'; -import { StyleDescriptor } from '../../../common/descriptor_types'; +import { CustomIcon, StyleDescriptor } from '../../../common/descriptor_types'; export interface IStyle { getType(): string; renderEditor( - onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void, + onCustomIconsChange: (customIcons: CustomIcon[]) => void ): ReactElement | null; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap index 656a4eb6ca599..e41b3b09134d4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap @@ -41,6 +41,18 @@ exports[`Renders SymbolIcon 1`] = ` key="airfield-15#ff0000rgb(106,173,213)" stroke="rgb(106,173,213)" style={Object {}} + svg="\\\\n\\\\n \\\\n" symbolId="airfield-15" /> `; + +exports[`Renders SymbolIcon with custom icon 1`] = ` +" + symbolId="__kbn__custom_icon_sdf__foobar" +/> +`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx index 5cc4cfa67df01..fb851ae629f62 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx @@ -16,6 +16,7 @@ const EMPTY_VALUE = ''; export interface Break { color: string; label: ReactElement | string | number; + svg?: string; symbolId?: string; } @@ -66,16 +67,17 @@ export class BreakedLegend extends Component { return null; } - const categories = this.props.breaks.map((brk, index) => { + const categories = this.props.breaks.map(({ symbolId, svg, label, color }, index) => { return ( ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx index cec8b48f505e8..cc544fd4030e7 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx @@ -17,9 +17,18 @@ interface Props { isLinesOnly: boolean; isPointsOnly: boolean; symbolId?: string; + svg?: string; } -export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, symbolId }: Props) { +export function Category({ + styleName, + label, + color, + isLinesOnly, + isPointsOnly, + symbolId, + svg, +}: Props) { function renderIcon() { if (styleName === VECTOR_STYLES.LABEL_COLOR) { return ( @@ -36,6 +45,7 @@ export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, s isLinesOnly={isLinesOnly} strokeColor={color} symbolId={symbolId} + svg={svg} /> ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx index cec1e8ed40aa2..4cc4d4169d7e0 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx @@ -7,13 +7,14 @@ import React, { Component, CSSProperties } from 'react'; // @ts-expect-error -import { getMakiSymbolSvg, styleSvg, buildSrcUrl } from '../../symbol_utils'; +import { CUSTOM_ICON_PREFIX_SDF, getSymbolSvg, styleSvg, buildSrcUrl } from '../../symbol_utils'; interface Props { symbolId: string; fill?: string; stroke?: string; - style: CSSProperties; + style?: CSSProperties; + svg: string; } interface State { @@ -39,8 +40,7 @@ export class SymbolIcon extends Component { async _loadSymbol() { let imgDataUrl; try { - const svg = getMakiSymbolSvg(this.props.symbolId); - const styledSvg = await styleSvg(svg, this.props.fill, this.props.stroke); + const styledSvg = await styleSvg(this.props.svg, this.props.fill, this.props.stroke); imgDataUrl = buildSrcUrl(styledSvg); } catch (error) { // ignore failures - component will just not display an icon diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx index 7907780d0ab4a..09993f8d0e5f3 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx @@ -52,6 +52,22 @@ test('Renders SymbolIcon', () => { isLinesOnly={false} strokeColor="rgb(106,173,213)" symbolId="airfield-15" + svg='\n\n \n' + /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('Renders SymbolIcon with custom icon', () => { + const component = shallow( + ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx index 333ba932dc6f3..745d1aae1b8dd 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx @@ -19,6 +19,7 @@ interface Props { isLinesOnly: boolean; strokeColor?: string; symbolId?: string; + svg?: string; } export function VectorIcon({ @@ -28,6 +29,7 @@ export function VectorIcon({ isLinesOnly, strokeColor, symbolId, + svg, }: Props) { if (isLinesOnly) { const style = { @@ -53,13 +55,18 @@ export function VectorIcon({ return ; } - return ( - - ); + if (svg) { + return ( + + ); + } + + return null; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx index 4de64e328eb13..2d282a4b530cb 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx @@ -13,9 +13,10 @@ interface Props { isPointsOnly: boolean; styles: Array>; symbolId?: string; + svg?: string; } -export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId }: Props) { +export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId, svg }: Props) { const legendRows = []; for (let i = 0; i < styles.length; i++) { @@ -23,6 +24,7 @@ export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId isLinesOnly, isPointsOnly, symbolId, + svg, }); legendRows.push( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx index 95be7f56a5597..c239e316d472f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx @@ -17,6 +17,7 @@ import { import { i18n } from '@kbn/i18n'; import { getVectorStyleLabel, getDisabledByMessage } from './get_vector_style_label'; import { STYLE_TYPE, VECTOR_STYLES } from '../../../../../common/constants'; +import { CustomIcon } from '../../../../../common/descriptor_types'; import { IStyleProperty } from '../properties/style_property'; import { StyleField } from '../style_fields_helper'; @@ -27,9 +28,11 @@ export interface Props { defaultDynamicStyleOptions: DynamicOptions; disabled?: boolean; disabledBy?: VECTOR_STYLES; + customIcons?: CustomIcon[]; fields: StyleField[]; onDynamicStyleChange: (propertyName: VECTOR_STYLES, options: DynamicOptions) => void; onStaticStyleChange: (propertyName: VECTOR_STYLES, options: StaticOptions) => void; + onCustomIconsChange?: (customIcons: CustomIcon[]) => void; styleProperty: IStyleProperty; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/custom_icon_modal.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/custom_icon_modal.test.tsx.snap new file mode 100644 index 0000000000000..06e6f24f9ea89 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/custom_icon_modal.test.tsx.snap @@ -0,0 +1,311 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render a custom icon modal with an existing icon 1`] = ` + + + +

+ Edit custom icon +

+
+
+ + + + + + + + + + + + + + + + + Reset + + + + + + Alpha threshold + + + + + } + labelType="label" + > + + + + + Radius + + + + + } + labelType="label" + > + + + + + + + " + /> + + + + + + + + Cancel + + + + + Delete + + + + + Save + + + + +
+`; + +exports[`should render an empty custom icon modal 1`] = ` + + + +

+ Custom Icon +

+
+
+ + + + + + + + + + + + + + + Cancel + + + + + Save + + + + +
+`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap index b0b85268aa1c8..324e4d9dbd453 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap @@ -3,6 +3,7 @@ exports[`Should not render icon map select when isCustomOnly 1`] = ` + + +", }, ] } onChange={[Function]} + onCustomIconsChange={[Function]} /> `; @@ -62,6 +64,7 @@ exports[`Should render custom stops input when useCustomIconMap 1`] = ` size="s" /> `; @@ -122,3 +126,40 @@ exports[`Should render default props 1`] = ` /> `; + +exports[`Should render icon map select with custom icons 1`] = ` + + + mock filledShapes option + , + "value": "filledShapes", + }, + Object { + "inputDisplay":
+ mock hollowShapes option +
, + "value": "hollowShapes", + }, + ] + } + valueOfSelected="filledShapes" + /> + +
+`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap index c0505426d1f63..d9a62dff00423 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap @@ -1,76 +1,204 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Should render icon select 1`] = ` - - + + icon={ + Object { + "side": "right", + "type": "arrowDown", + } } + onKeyDown={[Function]} readOnly={true} - value="symbol1" - /> - - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="s" -> - - + , - "value": "symbol1", - }, + /> + } + readOnly={true} + value="symbol1" + /> + + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="s" + > + + + + +" + symbolId="symbol1" + />, + }, + Object { + "key": "symbol2", + "label": "symbol2", + "prepend": + + +" + symbolId="symbol2" + />, + }, + ] + } + searchable={true} + singleSelection={false} + > + + + + + +`; + +exports[`Should render icon select with custom icons 1`] = ` + + + , - "value": "symbol2", - }, - ] - } - searchable={true} - singleSelection={false} + svg="" + symbolId="__kbn__custom_icon_sdf__foobar" + /> + } + readOnly={true} + value="My Custom Icon" + /> + + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="s" + > + - - - - + " + symbolId="__kbn__custom_icon_sdf__foobar" + />, + }, + Object { + "key": "__kbn__custom_icon_sdf__bizzbuzz", + "label": "My Other Custom Icon", + "prepend": " + symbolId="__kbn__custom_icon_sdf__bizzbuzz" + />, + }, + Object { + "isGroupLabel": true, + "label": "Kibana icons", + }, + Object { + "key": "symbol1", + "label": "symbol1", + "prepend": + + +" + symbolId="symbol1" + />, + }, + Object { + "key": "symbol2", + "label": "symbol2", + "prepend": + + +" + symbolId="symbol2" + />, + }, + ] + } + searchable={true} + singleSelection={false} + > + + + + + `; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_custom_icon_modal.scss b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_custom_icon_modal.scss new file mode 100644 index 0000000000000..e3480d0017435 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_custom_icon_modal.scss @@ -0,0 +1,8 @@ +.mapsCustomIconForm { + min-width: 400px; +} + +.mapsCustomIconForm__preview { + max-width: 210px; + min-height: 210px; +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_preview.scss b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_preview.scss new file mode 100644 index 0000000000000..2204483710eb0 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_preview.scss @@ -0,0 +1,3 @@ +.mapsCustomIconPreview__mapContainer { + height: 150px; +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_select.scss b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_select.scss index 5e69d97131095..bc244131d9314 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_select.scss +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_select.scss @@ -1,3 +1,3 @@ .mapIconSelectSymbol__inputButton { - margin-left: $euiSizeS; + margin: 0 $euiSizeXS; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/custom_icon_modal.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/custom_icon_modal.test.tsx new file mode 100644 index 0000000000000..8e21679405c07 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/custom_icon_modal.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { CustomIconModal } from './custom_icon_modal'; + +const defaultProps = { + cutoff: 0.25, + onCancel: () => {}, + onSave: () => {}, + radius: 0.25, + title: 'Custom Icon', +}; + +test('should render an empty custom icon modal', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); + +test('should render a custom icon modal with an existing icon', () => { + const component = shallow( + {}} + radius={0.15} + svg='' + symbolId="__kbn__custom_icon_sdf__foobar" + title="Edit custom icon" + /> + ); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/custom_icon_modal.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/custom_icon_modal.tsx new file mode 100644 index 0000000000000..9898ac0cd3e93 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/custom_icon_modal.tsx @@ -0,0 +1,393 @@ +/* + * 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, { Component } from 'react'; +import { + EuiAccordion, + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFilePicker, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiPanel, + EuiSpacer, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IconPreview } from './icon_preview'; +// @ts-expect-error +import { getCustomIconId } from '../../symbol_utils'; +// @ts-expect-error +import { ValidatedRange } from '../../../../../components/validated_range'; +import { CustomIcon } from '../../../../../../common/descriptor_types'; + +const strings = { + getAdvancedOptionsLabel: () => + i18n.translate('xpack.maps.customIconModal.advancedOptionsLabel', { + defaultMessage: 'Advanced options', + }), + getCancelButtonLabel: () => + i18n.translate('xpack.maps.customIconModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + getCutoffRangeLabel: () => ( + + <> + {i18n.translate('xpack.maps.customIconModal.cutoffRangeLabel', { + defaultMessage: 'Alpha threshold', + })}{' '} + + + + ), + getDeleteButtonLabel: () => + i18n.translate('xpack.maps.customIconModal.deleteButtonLabel', { + defaultMessage: 'Delete', + }), + getImageFilePickerPlaceholder: () => + i18n.translate('xpack.maps.customIconModal.imageFilePickerPlaceholder', { + defaultMessage: 'Select or drag and drop an SVG icon', + }), + getImageInputDescription: () => + i18n.translate('xpack.maps.customIconModal.imageInputDescription', { + defaultMessage: + 'SVGs without sharp corners and intricate details work best. Modifying the settings under Advanced options may improve rendering.', + }), + getInvalidFileLabel: () => + i18n.translate('xpack.maps.customIconModal.invalidFileError', { + defaultMessage: 'Icon must be in SVG format. Other image types are not supported.', + }), + getNameInputLabel: () => + i18n.translate('xpack.maps.customIconModal.nameInputLabel', { + defaultMessage: 'Name', + }), + getRadiusRangeLabel: () => ( + + <> + {i18n.translate('xpack.maps.customIconModal.radiusRangeLabel', { + defaultMessage: 'Radius', + })}{' '} + + + + ), + getResetButtonLabel: () => + i18n.translate('xpack.maps.customIconModal.resetButtonLabel', { + defaultMessage: 'Reset', + }), + getSaveButtonLabel: () => + i18n.translate('xpack.maps.customIconModal.saveButtonLabel', { + defaultMessage: 'Save', + }), +}; + +function getFileNameWithoutExt(fileName: string) { + const splits = fileName.split('.'); + if (splits.length > 1) { + splits.pop(); + } + return splits.join('.'); +} +interface Props { + /** + * initial value for the id of image added to map + */ + symbolId?: string; + /** + * initial value of the label of the custom element + */ + label?: string; + /** + * initial value of the preview image of the custom element as a base64 dataurl + */ + svg?: string; + /** + * intial value of alpha threshold for signed-distance field + */ + cutoff: number; + /** + * intial value of radius for signed-distance field + */ + radius: number; + /** + * title of the modal + */ + title: string; + /** + * A click handler for the save button + */ + onSave: (icon: CustomIcon) => void; + /** + * A click handler for the cancel button + */ + onCancel: () => void; + /** + * A click handler for the delete button + */ + onDelete?: (symbolId: string) => void; +} + +interface State { + /** + * label of the custom element to be saved + */ + label: string; + /** + * image of the custom element to be saved + */ + svg: string; + + cutoff: number; + radius: number; + isFileInvalid: boolean; +} + +export class CustomIconModal extends Component { + private _isMounted: boolean = false; + + public state = { + label: this.props.label || '', + svg: this.props.svg || '', + cutoff: this.props.cutoff, + radius: this.props.radius, + isFileInvalid: this.props.svg ? false : true, + }; + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidMount() { + this._isMounted = true; + } + + private _handleLabelChange = (value: string) => { + this.setState({ label: value }); + }; + + private _handleCutoffChange = (value: number) => { + this.setState({ cutoff: value }); + }; + + private _handleRadiusChange = (value: number) => { + this.setState({ radius: value }); + }; + + private _resetAdvancedOptions = () => { + this.setState({ radius: this.props.radius, cutoff: this.props.cutoff }); + }; + + private _onFileSelect = async (files: FileList | null) => { + this.setState({ + label: '', + svg: '', + isFileInvalid: false, + }); + + if (files && files.length) { + const file = files[0]; + const { type } = file; + if (type === 'image/svg+xml') { + const label = this.props.label ?? getFileNameWithoutExt(file.name); + try { + const svg = await file.text(); + + if (!this._isMounted) { + return; + } + this.setState({ isFileInvalid: false, label, svg }); + } catch (err) { + if (!this._isMounted) { + return; + } + this.setState({ isFileInvalid: true }); + } + } else { + this.setState({ isFileInvalid: true }); + } + } + }; + + private _renderAdvancedOptions() { + const { cutoff, radius } = this.state; + return ( + + + + + + {strings.getResetButtonLabel()} + + + + + + + + + + + + ); + } + + private _renderIconForm() { + const { label, svg } = this.state; + return svg !== '' ? ( + <> + + this._handleLabelChange(e.target.value)} + required + data-test-subj="mapsCustomIconForm-label" + /> + + + {this._renderAdvancedOptions()} + + ) : null; + } + + private _renderIconPreview() { + const { svg, isFileInvalid, cutoff, radius } = this.state; + return svg !== '' ? ( + + + + ) : null; + } + + public render() { + const { symbolId, onSave, onCancel, onDelete, title } = this.props; + const { label, svg, cutoff, radius, isFileInvalid } = this.state; + const isComplete = label.length !== 0 && svg.length !== 0 && !isFileInvalid; + const fileError = svg && isFileInvalid ? strings.getInvalidFileLabel() : ''; + return ( + + + +

{title}

+
+
+ + + + + + + + {this._renderIconForm()} + + {this._renderIconPreview()} + + + + + + {strings.getCancelButtonLabel()} + + {onDelete && symbolId ? ( + + { + onDelete(symbolId); + }} + data-test-subj="mapsCustomIconForm-submit" + > + {strings.getDeleteButtonLabel()} + + + ) : null} + + { + onSave({ symbolId: symbolId ?? getCustomIconId(), label, svg, cutoff, radius }); + }} + data-test-subj="mapsCustomIconForm-submit" + isDisabled={!isComplete} + > + {strings.getSaveButtonLabel()} + + + + +
+ ); + } +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js index c7d6928884183..3bc8208e2325e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js @@ -14,6 +14,8 @@ import { IconMapSelect } from './icon_map_select'; export function DynamicIconForm({ fields, onDynamicStyleChange, + onCustomIconsChange, + customIcons, staticDynamicSelect, styleProperty, }) { @@ -44,7 +46,9 @@ export function DynamicIconForm({ ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx index be59b2eae9026..e569b0cabb753 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx @@ -52,6 +52,15 @@ const defaultProps = { styleProperty: new MockDynamicStyleProperty() as unknown as IDynamicStyleProperty, isCustomOnly: false, + customIconStops: [ + { + stop: null, + icon: 'circle', + svg: '\n\n \n', + }, + ], + customIcons: [], + onCustomIconsChange: () => {}, }; test('Should render default props', () => { @@ -66,8 +75,50 @@ test('Should render custom stops input when useCustomIconMap', () => { {...defaultProps} useCustomIconMap={true} customIconStops={[ - { stop: null, icon: 'circle' }, - { stop: 'value1', icon: 'marker' }, + { + stop: null, + icon: 'circle', + }, + { + stop: 'value1', + icon: 'marker', + }, + ]} + /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('Should render icon map select with custom icons', () => { + const component = shallow( + ', + cutoff: 0.25, + radius: 0.25, + }, + { + symbolId: '__kbn__custom_icon_sdf__bizzbuzz', + label: 'My Other Custom Icon', + svg: '', + cutoff: 0.3, + radius: 0.15, + }, + ]} + customIconStops={[ + { + stop: null, + icon: '__kbn__custom_icon_sdf__bizzbuzz', + }, + { + stop: 'value1', + icon: 'marker', + }, ]} /> ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx index f5a0390c602e9..37b6a9185ad71 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx @@ -13,14 +13,19 @@ import { i18n } from '@kbn/i18n'; import { IconStops } from './icon_stops'; // @ts-expect-error import { getIconPaletteOptions, PREFERRED_ICONS } from '../../symbol_utils'; -import { IconDynamicOptions, IconStop } from '../../../../../../common/descriptor_types'; +import { + CustomIcon, + IconDynamicOptions, + IconStop, +} from '../../../../../../common/descriptor_types'; +import { ICON_SOURCE } from '../../../../../../common/constants'; import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; const CUSTOM_MAP_ID = 'CUSTOM_MAP_ID'; -const DEFAULT_ICON_STOPS = [ - { stop: null, icon: PREFERRED_ICONS[0] }, // first stop is the "other" category - { stop: '', icon: PREFERRED_ICONS[1] }, +const DEFAULT_ICON_STOPS: IconStop[] = [ + { stop: null, icon: PREFERRED_ICONS[0], iconSource: ICON_SOURCE.MAKI }, // first stop is the "other" category + { stop: '', icon: PREFERRED_ICONS[1], iconSource: ICON_SOURCE.MAKI }, ]; interface StyleOptionChanges { @@ -32,6 +37,8 @@ interface StyleOptionChanges { interface Props { customIconStops?: IconStop[]; iconPaletteId: string | null; + customIcons: CustomIcon[]; + onCustomIconsChange: (customIcons: CustomIcon[]) => void; onChange: ({ customIconStops, iconPaletteId, useCustomIconMap }: StyleOptionChanges) => void; styleProperty: IDynamicStyleProperty; useCustomIconMap?: boolean; @@ -86,6 +93,8 @@ export class IconMapSelect extends Component { getValueSuggestions={this.props.styleProperty.getValueSuggestions} iconStops={this.state.customIconStops} onChange={this._onCustomMapChange} + onCustomIconsChange={this.props.onCustomIconsChange} + customIcons={this.props.customIcons} /> ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_preview.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_preview.tsx new file mode 100644 index 0000000000000..f1a5653da612b --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_preview.tsx @@ -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 React, { Component } from 'react'; +import { + EuiColorPicker, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; +import { mapboxgl, Map as MapboxMap } from '@kbn/mapbox-gl'; +import { i18n } from '@kbn/i18n'; +import { ResizeChecker } from '.././../../../../../../../../src/plugins/kibana_utils/public'; +import { + CUSTOM_ICON_PIXEL_RATIO, + createSdfIcon, + // @ts-expect-error +} from '../../symbol_utils'; + +export interface Props { + svg: string; + cutoff: number; + radius: number; + isSvgInvalid: boolean; +} + +interface State { + map: MapboxMap | null; + iconColor: string; +} + +export class IconPreview extends Component { + static iconId = `iconPreview`; + private _checker?: ResizeChecker; + private _isMounted = false; + private _containerRef: HTMLDivElement | null = null; + + state: State = { + map: null, + iconColor: '#E7664C', + }; + + componentDidMount() { + this._isMounted = true; + this._initializeMap(); + } + + componentDidUpdate(prevProps: Props) { + if ( + this.props.svg !== prevProps.svg || + this.props.cutoff !== prevProps.cutoff || + this.props.radius !== prevProps.radius + ) { + this._syncImageToMap(); + } + } + + componentWillUnmount() { + this._isMounted = false; + if (this._checker) { + this._checker.destroy(); + } + if (this.state.map) { + this.state.map.remove(); + this.state.map = null; + } + } + + _setIconColor = (iconColor: string) => { + this.setState({ iconColor }, () => { + this._syncPaintPropertiesToMap(); + }); + }; + + _setContainerRef = (element: HTMLDivElement) => { + this._containerRef = element; + }; + + async _syncImageToMap() { + if (this._isMounted && this.state.map) { + const map = this.state.map; + const { svg, cutoff, radius, isSvgInvalid } = this.props; + if (!svg || isSvgInvalid) { + map.setLayoutProperty('icon-layer', 'visibility', 'none'); + return; + } + const imageData = await createSdfIcon({ svg, cutoff, radius }); + if (map.hasImage(IconPreview.iconId)) { + // @ts-expect-error + map.updateImage(IconPreview.iconId, imageData); + } else { + map.addImage(IconPreview.iconId, imageData, { + sdf: true, + pixelRatio: CUSTOM_ICON_PIXEL_RATIO, + }); + } + map.setLayoutProperty('icon-layer', 'icon-image', IconPreview.iconId); + map.setLayoutProperty('icon-layer', 'icon-size', 6); + map.setLayoutProperty('icon-layer', 'visibility', 'visible'); + this._syncPaintPropertiesToMap(); + } + } + + _syncPaintPropertiesToMap() { + const { map, iconColor } = this.state; + if (!map) return; + map.setPaintProperty('icon-layer', 'icon-halo-color', '#000000'); + map.setPaintProperty('icon-layer', 'icon-halo-width', 1); + map.setPaintProperty('icon-layer', 'icon-color', iconColor); + map.setLayoutProperty('icon-layer', 'icon-size', 12); + } + + _initResizerChecker() { + this._checker = new ResizeChecker(this._containerRef!); + this._checker.on('resize', () => { + if (this.state.map) { + this.state.map.resize(); + } + }); + } + + _createMapInstance(): MapboxMap { + const map = new mapboxgl.Map({ + container: this._containerRef!, + interactive: false, + center: [0, 0], + zoom: 2, + style: { + version: 8, + name: 'Empty', + sources: {}, + layers: [ + { + id: 'background', + type: 'background', + paint: { + 'background-color': 'rgba(0,0,0,0)', + }, + }, + ], + }, + }); + + map.on('load', () => { + map.addLayer({ + id: 'icon-layer', + type: 'symbol', + source: { + type: 'geojson', + data: { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [0, 0], + }, + properties: {}, + }, + }, + }); + this._syncImageToMap(); + }); + + return map; + } + + _initializeMap() { + const map: MapboxMap = this._createMapInstance(); + + this.setState({ map }, () => { + this._initResizerChecker(); + }); + } + + render() { + const iconColor = this.state.iconColor; + return ( +
+ + + +

+ + <> + {i18n.translate('xpack.maps.customIconModal.elementPreviewTitle', { + defaultMessage: 'Render preview', + })}{' '} + + + +

+
+ + +
+ + + + + + + +
+ ); + } +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js index cc03eb3d5ef1e..432a36478127f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js @@ -5,19 +5,28 @@ * 2.0. */ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { + EuiButton, EuiFormControlLayout, EuiFieldText, EuiPopover, EuiPopoverTitle, + EuiPopoverFooter, EuiFocusTrap, keys, EuiSelectable, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + DEFAULT_CUSTOM_ICON_CUTOFF, + DEFAULT_CUSTOM_ICON_RADIUS, +} from '../../../../../../common/constants'; import { SymbolIcon } from '../legend/symbol_icon'; import { SYMBOL_OPTIONS } from '../../symbol_utils'; import { getIsDarkMode } from '../../../../../kibana_services'; +import { CustomIconModal } from './custom_icon_modal'; function isKeyboardEvent(event) { return typeof event === 'object' && 'keyCode' in event; @@ -26,16 +35,48 @@ function isKeyboardEvent(event) { export class IconSelect extends Component { state = { isPopoverOpen: false, + isModalVisible: false, + }; + + _handleSave = ({ symbolId, svg, cutoff, radius, label }) => { + const icons = [ + ...this.props.customIcons.filter((i) => { + return i.symbolId !== symbolId; + }), + { + symbolId, + svg, + label, + cutoff, + radius, + }, + ]; + this.props.onCustomIconsChange(icons); + this._hideModal(); }; _closePopover = () => { this.setState({ isPopoverOpen: false }); }; + _hideModal = () => { + this.setState({ isModalVisible: false }); + }; + _openPopover = () => { this.setState({ isPopoverOpen: true }); }; + _showModal = () => { + this.setState({ isModalVisible: true }); + }; + + _toggleModal = () => { + this.setState((prevState) => ({ + isModalVisible: !prevState.isModalVisible, + })); + }; + _togglePopover = () => { this.setState((prevState) => ({ isPopoverOpen: !prevState.isPopoverOpen, @@ -59,12 +100,14 @@ export class IconSelect extends Component { }); if (selectedOption) { - this.props.onChange(selectedOption.value); + const { key } = selectedOption; + this.props.onChange({ selectedIconId: key }); } this._closePopover(); }; _renderPopoverButton() { + const { value, svg, label } = this.props.icon; return ( } @@ -95,26 +139,69 @@ export class IconSelect extends Component { } _renderIconSelectable() { - const options = SYMBOL_OPTIONS.map(({ value, label }) => { + const makiOptions = [ + { + label: i18n.translate('xpack.maps.styles.vector.iconSelect.kibanaIconsGroupLabel', { + defaultMessage: 'Kibana icons', + }), + isGroupLabel: true, + }, + ...SYMBOL_OPTIONS.map(({ value, label, svg }) => { + return { + key: value, + label, + prepend: ( + + ), + }; + }), + ]; + + const customOptions = this.props.customIcons.map(({ symbolId, label, svg }) => { return { - value, + key: symbolId, label, prepend: ( ), }; }); + if (customOptions.length) + customOptions.splice(0, 0, { + label: i18n.translate('xpack.maps.styles.vector.iconSelect.customIconsGroupLabel', { + defaultMessage: 'Custom icons', + }), + isGroupLabel: true, + }); + + const options = [...customOptions, ...makiOptions]; + return ( - + {(list, search) => (
{search} {list} + + {' '} + + + +
)}
@@ -123,17 +210,28 @@ export class IconSelect extends Component { render() { return ( - - {this._renderIconSelectable()} - + + + {this._renderIconSelectable()} + + {this.state.isModalVisible ? ( + + ) : null} + ); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js index ac5b3b7dd9847..5bb5f7f1c7808 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js @@ -16,8 +16,16 @@ jest.mock('../../../../../kibana_services', () => { jest.mock('../../symbol_utils', () => { return { SYMBOL_OPTIONS: [ - { value: 'symbol1', label: 'symbol1' }, - { value: 'symbol2', label: 'symbol2' }, + { + value: 'symbol1', + label: 'symbol1', + svg: '\n\n \n', + }, + { + value: 'symbol2', + label: 'symbol2', + svg: '\n\n \n', + }, ], }; }); @@ -28,7 +36,39 @@ import { shallow } from 'enzyme'; import { IconSelect } from './icon_select'; test('Should render icon select', () => { - const component = shallow( {}} />); + const component = shallow( + {}} /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('Should render icon select with custom icons', () => { + const component = shallow( + ', + cutoff: 0.25, + radius: 0.25, + }, + { + symbolId: '__kbn__custom_icon_sdf__bizzbuzz', + label: 'My Other Custom Icon', + svg: '', + cutoff: 0.3, + radius: 0.15, + }, + ]} + icon={{ + value: '__kbn__custom_icon_sdf__foobar', + svg: '', + label: 'My Custom Icon', + }} + /> + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js index 79491d6ededf3..2700a599c3aee 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js @@ -6,13 +6,13 @@ */ import React from 'react'; -import { DEFAULT_ICON } from '../../../../../../common/constants'; +import { DEFAULT_ICON, ICON_SOURCE } from '../../../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getOtherCategoryLabel } from '../../style_util'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldText } from '@elastic/eui'; import { IconSelect } from './icon_select'; import { StopInput } from '../stop_input'; -import { PREFERRED_ICONS, SYMBOL_OPTIONS } from '../../symbol_utils'; +import { getMakiSymbol, PREFERRED_ICONS, SYMBOL_OPTIONS } from '../../symbol_utils'; function isDuplicateStop(targetStop, iconStops) { const stops = iconStops.filter(({ stop }) => { @@ -43,113 +43,136 @@ export function getFirstUnusedSymbol(iconStops) { return firstUnusedSymbol ? firstUnusedSymbol.value : DEFAULT_ICON; } -export function IconStops({ field, getValueSuggestions, iconStops, onChange }) { - return iconStops.map(({ stop, icon }, index) => { - const onIconSelect = (selectedIconId) => { - const newIconStops = [...iconStops]; - newIconStops[index] = { - ...iconStops[index], - icon: selectedIconId, +export function IconStops({ + field, + getValueSuggestions, + iconStops, + onChange, + onCustomIconsChange, + customIcons, +}) { + return iconStops + .map(({ stop, icon, iconSource }, index) => { + const iconInfo = + iconSource === ICON_SOURCE.CUSTOM + ? customIcons.find(({ symbolId }) => symbolId === icon) + : getMakiSymbol(icon); + if (iconInfo === undefined) return; + const { svg, label } = iconInfo; + const onIconSelect = ({ selectedIconId }) => { + const newIconStops = [...iconStops]; + newIconStops[index] = { + ...iconStops[index], + icon: selectedIconId, + }; + onChange({ customStops: newIconStops }); }; - onChange({ customStops: newIconStops }); - }; - const onStopChange = (newStopValue) => { - const newIconStops = [...iconStops]; - newIconStops[index] = { - ...iconStops[index], - stop: newStopValue, + const onStopChange = (newStopValue) => { + const newIconStops = [...iconStops]; + newIconStops[index] = { + ...iconStops[index], + stop: newStopValue, + }; + onChange({ + customStops: newIconStops, + isInvalid: isDuplicateStop(newStopValue, iconStops), + }); }; - onChange({ - customStops: newIconStops, - isInvalid: isDuplicateStop(newStopValue, iconStops), - }); - }; - const onAdd = () => { - onChange({ - customStops: [ - ...iconStops.slice(0, index + 1), - { - stop: '', - icon: getFirstUnusedSymbol(iconStops), - }, - ...iconStops.slice(index + 1), - ], - }); - }; - const onRemove = () => { - onChange({ - customStops: [...iconStops.slice(0, index), ...iconStops.slice(index + 1)], - }); - }; + const onAdd = () => { + onChange({ + customStops: [ + ...iconStops.slice(0, index + 1), + { + stop: '', + icon: getFirstUnusedSymbol(iconStops), + }, + ...iconStops.slice(index + 1), + ], + }); + }; + const onRemove = () => { + onChange({ + customStops: [...iconStops.slice(0, index), ...iconStops.slice(index + 1)], + }); + }; + + let deleteButton; + if (iconStops.length > 2 && index !== 0) { + deleteButton = ( + + ); + } - let deleteButton; - if (iconStops.length > 2 && index !== 0) { - deleteButton = ( - + const iconStopButtons = ( +
+ {deleteButton} + +
); - } - const iconStopButtons = ( -
- {deleteButton} - -
- ); + const errors = []; + // TODO check for duplicate values and add error messages here - const errors = []; - // TODO check for duplicate values and add error messages here + const stopInput = + index === 0 ? ( + + ) : ( + + ); - const stopInput = - index === 0 ? ( - - ) : ( - + return ( + + + + {stopInput} + + + + + + ); - - return ( - - - - {stopInput} - - - - - - - ); - }); + }) + .filter((stop) => { + return stop !== undefined; + }); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js index 262fb5c7e0991..6ec372496e8be 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js @@ -9,9 +9,17 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IconSelect } from './icon_select'; -export function StaticIconForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) { - const onChange = (selectedIconId) => { - onStaticStyleChange(styleProperty.getStyleName(), { value: selectedIconId }); +export function StaticIconForm({ + onStaticStyleChange, + onCustomIconsChange, + customIcons, + staticDynamicSelect, + styleProperty, +}) { + const onChange = ({ selectedIconId }) => { + onStaticStyleChange(styleProperty.getStyleName(), { + value: selectedIconId, + }); }; return ( @@ -20,7 +28,12 @@ export function StaticIconForm({ onStaticStyleChange, staticDynamicSelect, style {staticDynamicSelect}
- + ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx index b89f4ee0b2aa0..8edb67703a4d1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx @@ -11,6 +11,7 @@ import { StyleProperties, VectorStyleEditor } from './vector_style_editor'; import { getDefaultStaticProperties } from '../vector_style_defaults'; import { IVectorLayer } from '../../../layers/vector_layer'; import { IVectorSource } from '../../../sources/vector_source'; +import { CustomIcon } from '../../../../../common/descriptor_types'; import { FIELD_ORIGIN, LAYER_STYLE_TYPE, @@ -61,7 +62,8 @@ const vectorStyleDescriptor = { const vectorStyle = new VectorStyle( vectorStyleDescriptor, {} as unknown as IVectorSource, - {} as unknown as IVectorLayer + {} as unknown as IVectorLayer, + [] as CustomIcon[] ); const styleProperties: StyleProperties = {}; vectorStyle.getAllStyleProperties().forEach((styleProperty) => { @@ -73,11 +75,13 @@ const defaultProps = { isPointsOnly: true, isLinesOnly: false, onIsTimeAwareChange: (isTimeAware: boolean) => {}, + onCustomIconsChange: (customIcons: CustomIcon[]) => {}, handlePropertyChange: (propertyName: VECTOR_STYLES, stylePropertyDescriptor: unknown) => {}, hasBorder: true, styleProperties, isTimeAware: true, showIsTimeAware: true, + customIcons: [], }; test('should render', async () => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx index a7488ab13da6c..4431cead9b0d1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx @@ -33,6 +33,7 @@ import { createStyleFieldsHelper, StyleField, StyleFieldsHelper } from '../style import { ColorDynamicOptions, ColorStaticOptions, + CustomIcon, DynamicStylePropertyOptions, IconDynamicOptions, IconStaticOptions, @@ -62,11 +63,13 @@ interface Props { isPointsOnly: boolean; isLinesOnly: boolean; onIsTimeAwareChange: (isTimeAware: boolean) => void; + onCustomIconsChange: (customIcons: CustomIcon[]) => void; handlePropertyChange: (propertyName: VECTOR_STYLES, stylePropertyDescriptor: unknown) => void; hasBorder: boolean; styleProperties: StyleProperties; isTimeAware: boolean; showIsTimeAware: boolean; + customIcons: CustomIcon[]; } interface State { @@ -392,8 +395,10 @@ export class VectorStyleEditor extends Component { = { aerialway: { label: 'Aerialway', svg: '\n\n \n', diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap index a3f23536326aa..a49dd9a494c7f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap @@ -47,6 +47,10 @@ exports[`renderLegendDetailRow Should render categorical legend with breaks 1`] isPointsOnly={true} label="US_format" styleName="icon" + svg=" + + +" symbolId="circle" /> @@ -59,6 +63,10 @@ exports[`renderLegendDetailRow Should render categorical legend with breaks 1`] isPointsOnly={true} label="CN_format" styleName="icon" + svg=" + + +" symbolId="marker" /> @@ -77,9 +85,93 @@ exports[`renderLegendDetailRow Should render categorical legend with breaks 1`] } styleName="icon" + svg=" + + +" symbolId="square" />
`; + +exports[`renderLegendDetailRow Should render categorical legend with custom icons in breaks 1`] = ` +
+ + + + + + + foobar_label + + + + + + + + + + + +" + symbolId="marker" + /> + + + + Other + + } + styleName="icon" + svg=" + + +" + symbolId="kbn__custom_icon_sdf__foobar" + /> + + +
+`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx index c9e7cfb6d7e39..0802c78c26933 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx @@ -324,7 +324,7 @@ export class DynamicColorProperty extends DynamicStyleProperty | null = null; let getValuePrefix: ((i: number, isNext: boolean) => string) | null = null; if (this._options.useCustomColorRamp) { @@ -361,6 +361,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { if (stop !== null && color != null) { breaks.push({ color, + svg, symbolId, label: this.formatField(stop), }); @@ -427,17 +430,18 @@ export class DynamicColorProperty extends DynamicStyleProperty{getOtherCategoryLabel()}, symbolId, + svg, }); } return breaks; } - renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId }: LegendProps) { + renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId, svg }: LegendProps) { let breaks: Break[] = []; if (this.isOrdinal()) { - breaks = this._getOrdinalBreaks(symbolId); + breaks = this._getOrdinalBreaks(symbolId, svg); } else if (this.isCategorical()) { - breaks = this._getCategoricalBreaks(symbolId); + breaks = this._getCategoricalBreaks(symbolId, svg); } return ( ({ })); import React from 'react'; -import { RawValue, VECTOR_STYLES } from '../../../../../common/constants'; +import { ICON_SOURCE, RawValue, VECTOR_STYLES } from '../../../../../common/constants'; // @ts-ignore import { DynamicIconProperty } from './dynamic_icon_property'; import { mockField, MockLayer } from './test_helpers/test_util'; @@ -57,7 +57,30 @@ describe('renderLegendDetailRow', () => { const iconStyle = makeProperty({ iconPaletteId: 'filledShapes', }); + const legendRow = iconStyle.renderLegendDetailRow({ isPointsOnly: true, isLinesOnly: false }); + const component = shallow(legendRow); + await new Promise((resolve) => process.nextTick(resolve)); + component.update(); + expect(component).toMatchSnapshot(); + }); + + test('Should render categorical legend with custom icons in breaks', async () => { + const iconStyle = makeProperty({ + useCustomIconMap: true, + customIconStops: [ + { + stop: null, + icon: 'kbn__custom_icon_sdf__foobar', + iconSource: ICON_SOURCE.CUSTOM, + }, + { + stop: 'MX', + icon: 'marker', + iconSource: ICON_SOURCE.MAKI, + }, + ], + }); const legendRow = iconStyle.renderLegendDetailRow({ isPointsOnly: true, isLinesOnly: false }); const component = shallow(legendRow); await new Promise((resolve) => process.nextTick(resolve)); @@ -88,8 +111,16 @@ describe('get mapbox icon-image expression (via internal _getMbIconImageExpressi const iconStyle = makeProperty({ useCustomIconMap: true, customIconStops: [ - { stop: null, icon: 'circle' }, - { stop: 'MX', icon: 'marker' }, + { + stop: null, + icon: 'circle', + iconSource: ICON_SOURCE.MAKI, + }, + { + stop: 'MX', + icon: 'marker', + iconSource: ICON_SOURCE.MAKI, + }, ], }); expect(iconStyle._getMbIconImageExpression()).toEqual([ diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx index c95d5eec069a8..db295f200a148 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { EuiTextColor } from '@elastic/eui'; import type { Map as MbMap } from '@kbn/mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; +import { IVectorStyle } from '../vector_style'; import { getIconPalette, getMakiSymbolAnchor, @@ -48,10 +49,11 @@ export class DynamicIconProperty extends DynamicStyleProperty { if (stop) { + const svg = layerStyle.getIconSvg(style); breaks.push({ color: 'grey', label: this.formatField(stop), symbolId: style, + svg, }); } }); if (fallbackSymbolId) { + const svg = layerStyle.getIconSvg(fallbackSymbolId); breaks.push({ color: 'grey', label: {getOtherCategoryLabel()}, symbolId: fallbackSymbolId, + svg, }); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx index 5ea99e64e8626..89f138ff7744b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx @@ -11,10 +11,11 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { OrdinalLegend } from '../components/legend/ordinal_legend'; import { makeMbClampedNumberExpression } from '../style_util'; import { + FieldFormatter, HALF_MAKI_ICON_SIZE, - // @ts-expect-error -} from '../symbol_utils'; -import { FieldFormatter, MB_LOOKUP_FUNCTION, VECTOR_STYLES } from '../../../../../common/constants'; + MB_LOOKUP_FUNCTION, + VECTOR_STYLES, +} from '../../../../../common/constants'; import { SizeDynamicOptions } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../layers/vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts index 1ebe35d65ac95..0a2464d8bed8b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts @@ -8,7 +8,7 @@ import type { Map as MbMap } from '@kbn/mapbox-gl'; import { StaticStyleProperty } from './static_style_property'; // @ts-expect-error -import { getMakiSymbolAnchor, getMakiIconId } from '../symbol_utils'; +import { getMakiSymbolAnchor } from '../symbol_utils'; import { IconStaticOptions } from '../../../../../common/descriptor_types'; export class StaticIconProperty extends StaticStyleProperty { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts index 771e0f8f33a0c..74a4ffebea96d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts @@ -7,11 +7,7 @@ import type { Map as MbMap } from '@kbn/mapbox-gl'; import { StaticStyleProperty } from './static_style_property'; -import { VECTOR_STYLES } from '../../../../../common/constants'; -import { - HALF_MAKI_ICON_SIZE, - // @ts-expect-error -} from '../symbol_utils'; +import { HALF_MAKI_ICON_SIZE, VECTOR_STYLES } from '../../../../../common/constants'; import { SizeStaticOptions } from '../../../../../common/descriptor_types'; export class StaticSizeProperty extends StaticStyleProperty { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts index 41877406f7489..ee3da4e3636b3 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts @@ -16,6 +16,7 @@ export type LegendProps = { isPointsOnly: boolean; isLinesOnly: boolean; symbolId?: string; + svg?: string; }; export interface IStyleProperty { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/test_helpers/test_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/test_helpers/test_util.ts index 13455b3e4f840..3d1cad1561a0e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/test_helpers/test_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/test_helpers/test_util.ts @@ -66,6 +66,10 @@ export class MockStyle implements IStyle { return null; } + getIconSvg(symbolId: string) { + return `\n\n \n`; + } + getType() { return LAYER_STYLE_TYPE.VECTOR; } @@ -109,6 +113,10 @@ export class MockLayer { return this._style; } + getCurrentStyle() { + return this._style; + } + getDataRequest() { return null; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts index 46b2e047d0d63..fc57f1b92f5af 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts @@ -73,53 +73,81 @@ describe('isOnlySingleFeatureType', () => { }); describe('assignCategoriesToPalette', () => { - test('Categories and palette values have same length', () => { + test('Categories and icons have same length', () => { const categories = [ { key: 'alpah', count: 1 }, { key: 'bravo', count: 1 }, { key: 'charlie', count: 1 }, { key: 'delta', count: 1 }, ]; - const paletteValues = ['red', 'orange', 'yellow', 'green']; + const paletteValues = ['circle', 'marker', 'triangle', 'square']; expect(assignCategoriesToPalette({ categories, paletteValues })).toEqual({ stops: [ - { stop: 'alpah', style: 'red' }, - { stop: 'bravo', style: 'orange' }, - { stop: 'charlie', style: 'yellow' }, + { + stop: 'alpah', + style: 'circle', + iconSource: 'MAKI', + }, + { + stop: 'bravo', + style: 'marker', + iconSource: 'MAKI', + }, + { + stop: 'charlie', + style: 'triangle', + iconSource: 'MAKI', + }, ], - fallbackSymbolId: 'green', + fallbackSymbolId: 'square', }); }); - test('Should More categories than palette values', () => { + test('Should More categories than icon values', () => { const categories = [ { key: 'alpah', count: 1 }, { key: 'bravo', count: 1 }, { key: 'charlie', count: 1 }, { key: 'delta', count: 1 }, ]; - const paletteValues = ['red', 'orange', 'yellow']; + const paletteValues = ['circle', 'square', 'triangle']; expect(assignCategoriesToPalette({ categories, paletteValues })).toEqual({ stops: [ - { stop: 'alpah', style: 'red' }, - { stop: 'bravo', style: 'orange' }, + { + stop: 'alpah', + style: 'circle', + iconSource: 'MAKI', + }, + { + stop: 'bravo', + style: 'square', + iconSource: 'MAKI', + }, ], - fallbackSymbolId: 'yellow', + fallbackSymbolId: 'triangle', }); }); - test('Less categories than palette values', () => { + test('Less categories than icon values', () => { const categories = [ { key: 'alpah', count: 1 }, { key: 'bravo', count: 1 }, ]; - const paletteValues = ['red', 'orange', 'yellow', 'green', 'blue']; + const paletteValues = ['circle', 'triangle', 'marker', 'square', 'rectangle']; expect(assignCategoriesToPalette({ categories, paletteValues })).toEqual({ stops: [ - { stop: 'alpah', style: 'red' }, - { stop: 'bravo', style: 'orange' }, + { + stop: 'alpah', + style: 'circle', + iconSource: 'MAKI', + }, + { + stop: 'bravo', + style: 'triangle', + iconSource: 'MAKI', + }, ], - fallbackSymbolId: 'yellow', + fallbackSymbolId: 'marker', }); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts index 467fd6b3621a2..11f564f436dd5 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts @@ -6,7 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import { MB_LOOKUP_FUNCTION, VECTOR_SHAPE_TYPE, VECTOR_STYLES } from '../../../../common/constants'; +import { + ICON_SOURCE, + MB_LOOKUP_FUNCTION, + VECTOR_SHAPE_TYPE, + VECTOR_STYLES, +} from '../../../../common/constants'; import { Category } from '../../../../common/descriptor_types'; import { StaticTextProperty } from './properties/static_text_property'; import { DynamicTextProperty } from './properties/dynamic_text_property'; @@ -74,6 +79,7 @@ export function assignCategoriesToPalette({ stops.push({ stop: categories[i].key, style: paletteValues[i], + iconSource: ICON_SOURCE.MAKI, }); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js index 07ac77dc0cb78..af165863ffc9c 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js @@ -7,39 +7,48 @@ import React from 'react'; import xml2js from 'xml2js'; +import uuid from 'uuid/v4'; import { Canvg } from 'canvg'; import calcSDF from 'bitmap-sdf'; +import { + CUSTOM_ICON_SIZE, + CUSTOM_ICON_PREFIX_SDF, + MAKI_ICON_SIZE, +} from '../../../../common/constants'; import { parseXmlString } from '../../../../common/parse_xml_string'; import { SymbolIcon } from './components/legend/symbol_icon'; import { getIsDarkMode } from '../../../kibana_services'; import { MAKI_ICONS } from './maki_icons'; -const MAKI_ICON_SIZE = 16; -export const HALF_MAKI_ICON_SIZE = MAKI_ICON_SIZE / 2; +export const CUSTOM_ICON_PIXEL_RATIO = Math.floor( + window.devicePixelRatio * (CUSTOM_ICON_SIZE / MAKI_ICON_SIZE) * 0.75 +); -export const SYMBOL_OPTIONS = Object.keys(MAKI_ICONS).map((symbolId) => { +export const SYMBOL_OPTIONS = Object.entries(MAKI_ICONS).map(([value, { svg, label }]) => { return { - value: symbolId, - label: symbolId, + value, + label, + svg, }; }); /** - * Converts a SVG icon to a monochrome image using a signed distance function. + * Converts a SVG icon to a PNG image using a signed distance function (SDF). * * @param {string} svgString - SVG icon as string - * @param {number} [cutoff=0.25] - balance between SDF inside 1 and outside 0 of glyph + * @param {number} [renderSize=64] - size of the output PNG (higher provides better resolution but requires more processing) + * @param {number} [cutoff=0.25] - balance between SDF inside 1 and outside 0 of icon * @param {number} [radius=0.25] - size of SDF around the cutoff as percent of output icon size - * @return {ImageData} Monochrome image that can be added to a MapLibre map + * @return {ImageData} image that can be added to a MapLibre map with option `{ sdf: true }` */ -export async function createSdfIcon(svgString, cutoff = 0.25, radius = 0.25) { +export async function createSdfIcon({ svg, renderSize = 64, cutoff = 0.25, radius = 0.25 }) { const buffer = 3; - const size = MAKI_ICON_SIZE + buffer * 4; + const size = renderSize + buffer * 4; const svgCanvas = document.createElement('canvas'); svgCanvas.width = size; svgCanvas.height = size; const svgCtx = svgCanvas.getContext('2d'); - const v = Canvg.fromString(svgCtx, svgString, { + const v = Canvg.fromString(svgCtx, svg, { ignoreDimensions: true, offsetX: buffer / 2, offsetY: buffer / 2, @@ -70,12 +79,8 @@ export async function createSdfIcon(svgString, cutoff = 0.25, radius = 0.25) { return imageData; } -export function getMakiSymbolSvg(symbolId) { - const svg = MAKI_ICONS?.[symbolId]?.svg; - if (!svg) { - throw new Error(`Unable to find symbol: ${symbolId}`); - } - return svg; +export function getMakiSymbol(symbolId) { + return MAKI_ICONS?.[symbolId]; } export function getMakiSymbolAnchor(symbolId) { @@ -89,6 +94,10 @@ export function getMakiSymbolAnchor(symbolId) { } } +export function getCustomIconId() { + return `${CUSTOM_ICON_PREFIX_SDF}${uuid()}`; +} + export function buildSrcUrl(svgString) { const domUrl = window.URL || window.webkitURL || window; const svg = new Blob([svgString], { type: 'image/svg+xml' }); @@ -130,9 +139,9 @@ const ICON_PALETTES = [ // PREFERRED_ICONS is used to provide less random default icon values for forms that need default icon values export const PREFERRED_ICONS = []; ICON_PALETTES.forEach((iconPalette) => { - iconPalette.icons.forEach((iconId) => { - if (!PREFERRED_ICONS.includes(iconId)) { - PREFERRED_ICONS.push(iconId); + iconPalette.icons.forEach((icon) => { + if (!PREFERRED_ICONS.includes(icon)) { + PREFERRED_ICONS.push(icon); } }); }); @@ -154,6 +163,7 @@ export function getIconPaletteOptions() { className="mapIcon" symbolId={iconId} fill={isDarkMode ? 'rgb(223, 229, 239)' : 'rgb(52, 55, 65)'} + svg={getMakiSymbol(iconId).svg} /> ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.test.js b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.test.js index d5fc3d30a447c..8c85702b19579 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.test.js @@ -5,12 +5,13 @@ * 2.0. */ -import { getMakiSymbolSvg, styleSvg } from './symbol_utils'; +import { getMakiSymbol, styleSvg } from './symbol_utils'; -describe('getMakiSymbolSvg', () => { - it('Should load symbol svg', () => { - const svgString = getMakiSymbolSvg('aerialway'); - expect(svgString.length).toBe(624); +describe('getMakiSymbol', () => { + it('Should load symbol', () => { + const symbol = getMakiSymbol('aerialway'); + expect(symbol.svg.length).toBe(624); + expect(symbol.label).toBe('Aerialway'); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js index 33125019ecc0b..c1aa8e395d8c0 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js @@ -35,6 +35,8 @@ class MockSource { describe('getDescriptorWithUpdatedStyleProps', () => { const previousFieldName = 'doIStillExist'; const mapColors = []; + const layer = {}; + const customIcons = []; const properties = { fillColor: { type: STYLE_TYPE.STATIC, @@ -69,7 +71,7 @@ describe('getDescriptorWithUpdatedStyleProps', () => { describe('When there is no mismatch in configuration', () => { it('Should return no changes when next ordinal fields contain existing style property fields', async () => { - const vectorStyle = new VectorStyle({ properties }, new MockSource()); + const vectorStyle = new VectorStyle({ properties }, new MockSource(), layer, customIcons); const nextFields = [new MockField({ fieldName: previousFieldName, dataType: 'number' })]; const { hasChanges } = await vectorStyle.getDescriptorWithUpdatedStyleProps( @@ -83,7 +85,7 @@ describe('getDescriptorWithUpdatedStyleProps', () => { describe('When styles should revert to static styling', () => { it('Should convert dynamic styles to static styles when there are no next fields', async () => { - const vectorStyle = new VectorStyle({ properties }, new MockSource()); + const vectorStyle = new VectorStyle({ properties }, new MockSource(), layer, customIcons); const nextFields = []; const { hasChanges, nextStyleDescriptor } = @@ -104,7 +106,7 @@ describe('getDescriptorWithUpdatedStyleProps', () => { }); it('Should convert dynamic ICON_SIZE static style when there are no next ordinal fields', async () => { - const vectorStyle = new VectorStyle({ properties }, new MockSource()); + const vectorStyle = new VectorStyle({ properties }, new MockSource(), layer, customIcons); const nextFields = [ { @@ -143,7 +145,7 @@ describe('getDescriptorWithUpdatedStyleProps', () => { describe('When styles should not be cleared', () => { it('Should update field in styles when the fields and style combination remains compatible', async () => { - const vectorStyle = new VectorStyle({ properties }, new MockSource()); + const vectorStyle = new VectorStyle({ properties }, new MockSource(), layer, customIcons); const nextFields = [new MockField({ fieldName: 'someOtherField', dataType: 'number' })]; const { hasChanges, nextStyleDescriptor } = @@ -174,6 +176,8 @@ describe('getDescriptorWithUpdatedStyleProps', () => { }); describe('pluckStyleMetaFromSourceDataRequest', () => { + const layer = {}; + const customIcons = []; describe('has features', () => { it('Should identify when feature collection only contains points', async () => { const sourceDataRequest = new DataRequest({ @@ -195,7 +199,7 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { ], }, }); - const vectorStyle = new VectorStyle({}, new MockSource()); + const vectorStyle = new VectorStyle({}, new MockSource(), layer, customIcons); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.geometryTypes.isPointsOnly).toBe(true); @@ -231,7 +235,7 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { ], }, }); - const vectorStyle = new VectorStyle({}, new MockSource()); + const vectorStyle = new VectorStyle({}, new MockSource(), layer, customIcons); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.geometryTypes.isPointsOnly).toBe(false); @@ -280,7 +284,9 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { }, }, }, - new MockSource() + new MockSource(), + layer, + customIcons ); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); @@ -304,7 +310,9 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { }, }, }, - new MockSource() + new MockSource(), + layer, + customIcons ); const styleMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index 0e87651e234bc..52209563e9807 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -20,6 +20,7 @@ import { DEFAULT_ICON, FIELD_ORIGIN, GEO_JSON_TYPE, + ICON_SOURCE, KBN_IS_CENTROID_FEATURE, LAYER_STYLE_TYPE, SOURCE_FORMATTERS_DATA_REQUEST_ID, @@ -28,6 +29,8 @@ import { VECTOR_STYLES, } from '../../../../common/constants'; import { StyleMeta } from './style_meta'; +// @ts-expect-error +import { getMakiSymbol, PREFERRED_ICONS } from './symbol_utils'; import { VectorIcon } from './components/legend/vector_icon'; import { VectorStyleLegend } from './components/legend/vector_style_legend'; import { isOnlySingleFeatureType, getHasLabel } from './style_util'; @@ -50,6 +53,7 @@ import { ColorDynamicOptions, ColorStaticOptions, ColorStylePropertyDescriptor, + CustomIcon, DynamicStyleProperties, DynamicStylePropertyOptions, IconDynamicOptions, @@ -99,6 +103,8 @@ export interface IVectorStyle extends IStyle { isTimeAware(): boolean; getPrimaryColor(): string; getIcon(showIncompleteIndicator: boolean): ReactElement; + getIconSvg(symbolId: string): string | undefined; + isUsingCustomIcon(symbolId: string): boolean; hasLegendDetails: () => Promise; renderLegendDetails: () => ReactElement; clearFeatureState: (featureCollection: FeatureCollection, mbMap: MbMap, sourceId: string) => void; @@ -151,6 +157,7 @@ export interface IVectorStyle extends IStyle { export class VectorStyle implements IVectorStyle { private readonly _descriptor: VectorStyleDescriptor; private readonly _layer: IVectorLayer; + private readonly _customIcons: CustomIcon[]; private readonly _source: IVectorSource; private readonly _styleMeta: StyleMeta; @@ -186,10 +193,12 @@ export class VectorStyle implements IVectorStyle { descriptor: VectorStyleDescriptor | null, source: IVectorSource, layer: IVectorLayer, + customIcons: CustomIcon[], chartsPaletteServiceGetColor?: (value: string) => string | null ) { this._source = source; this._layer = layer; + this._customIcons = customIcons; this._descriptor = descriptor ? { ...descriptor, @@ -458,7 +467,10 @@ export class VectorStyle implements IVectorStyle { : (this._lineWidthStyleProperty as StaticSizeProperty).getOptions().size !== 0; } - renderEditor(onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void) { + renderEditor( + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void, + onCustomIconsChange: (customIcons: CustomIcon[]) => void + ) { const rawProperties = this.getRawProperties(); const handlePropertyChange = (propertyName: VECTOR_STYLES, stylePropertyDescriptor: any) => { rawProperties[propertyName] = stylePropertyDescriptor; // override single property, but preserve the rest @@ -488,8 +500,10 @@ export class VectorStyle implements IVectorStyle { isPointsOnly={this.getIsPointsOnly()} isLinesOnly={this._getIsLinesOnly()} onIsTimeAwareChange={onIsTimeAwareChange} + onCustomIconsChange={onCustomIconsChange} isTimeAware={this.isTimeAware()} showIsTimeAware={propertiesWithFieldMeta.length > 0} + customIcons={this._customIcons} hasBorder={this._hasBorder()} /> ); @@ -697,12 +711,28 @@ export class VectorStyle implements IVectorStyle { return formatters ? formatters[fieldName] : null; }; + getIconSvg(symbolId: string) { + const meta = this._getIconMeta(symbolId); + return meta ? meta.svg : undefined; + } + _getSymbolId() { return this.arePointsSymbolizedAsCircles() || this._iconStyleProperty.isDynamic() ? undefined : (this._iconStyleProperty as StaticIconProperty).getOptions().value; } + _getIconMeta( + symbolId: string + ): { svg: string; label: string; iconSource: ICON_SOURCE } | undefined { + const icon = this._customIcons.find(({ symbolId: value }) => value === symbolId); + if (icon) { + return { ...icon, iconSource: ICON_SOURCE.CUSTOM }; + } + const symbol = getMakiSymbol(symbolId); + return symbol ? { ...symbol, iconSource: ICON_SOURCE.MAKI } : undefined; + } + getPrimaryColor() { const primaryColorKey = this._getIsLinesOnly() ? VECTOR_STYLES.LINE_COLOR @@ -741,18 +771,31 @@ export class VectorStyle implements IVectorStyle { } : {}; + const symbolId = this._getSymbolId(); + const svg = symbolId ? this.getIconSvg(symbolId) : undefined; + return ( ); } + isUsingCustomIcon(symbolId: string) { + if (this._iconStyleProperty.isDynamic()) { + const { customIconStops } = this._iconStyleProperty.getOptions() as IconDynamicOptions; + return customIconStops ? customIconStops.some(({ icon }) => icon === symbolId) : false; + } + const { value } = this._iconStyleProperty.getOptions() as IconStaticOptions; + return value === symbolId; + } + _getLegendDetailStyleProperties = () => { const hasLabel = getHasLabel(this._labelStyleProperty); return this.getDynamicPropertiesArray().filter((styleProperty) => { @@ -783,12 +826,16 @@ export class VectorStyle implements IVectorStyle { } renderLegendDetails() { + const symbolId = this._getSymbolId(); + const svg = symbolId ? this.getIconSvg(symbolId) : undefined; + return ( ); } @@ -1040,9 +1087,28 @@ export class VectorStyle implements IVectorStyle { if (!descriptor || !descriptor.options) { return new StaticIconProperty({ value: DEFAULT_ICON }, VECTOR_STYLES.ICON); } else if (descriptor.type === StaticStyleProperty.type) { - return new StaticIconProperty(descriptor.options as IconStaticOptions, VECTOR_STYLES.ICON); + const { value } = { ...descriptor.options } as IconStaticOptions; + const meta = this._getIconMeta(value); + let svg; + let label; + let iconSource; + if (meta) { + ({ svg, label, iconSource } = meta); + } + return new StaticIconProperty( + { value, svg, label, iconSource } as IconStaticOptions, + VECTOR_STYLES.ICON + ); } else if (descriptor.type === DynamicStyleProperty.type) { - const options = descriptor.options as IconDynamicOptions; + const options = { ...descriptor.options } as IconDynamicOptions; + if (options.customIconStops) { + options.customIconStops.forEach((iconStop) => { + const meta = this._getIconMeta(iconStop.icon); + if (meta) { + iconStop.iconSource = meta.iconSource; + } + }); + } const field = this._makeField(options.field); return new DynamicIconProperty( options, diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/index.ts b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/index.ts index d52689cda141a..f2125f1a30993 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/index.ts +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/index.ts @@ -10,9 +10,9 @@ import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { StyleSettings } from './style_settings'; import { getSelectedLayer } from '../../../selectors/map_selectors'; -import { updateLayerStyleForSelectedLayer } from '../../../actions'; +import { updateCustomIcons, updateLayerStyleForSelectedLayer } from '../../../actions'; import { MapStoreState } from '../../../reducers/store'; -import { StyleDescriptor } from '../../../../common/descriptor_types'; +import { CustomIcon, StyleDescriptor } from '../../../../common/descriptor_types'; function mapStateToProps(state: MapStoreState) { return { @@ -25,6 +25,9 @@ function mapDispatchToProps(dispatch: ThunkDispatch { dispatch(updateLayerStyleForSelectedLayer(styleDescriptor)); }, + updateCustomIcons: (customIcons: CustomIcon[]) => { + dispatch(updateCustomIcons(customIcons)); + }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx index d4f461e7cb3ec..8d399f19a765c 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx @@ -10,16 +10,17 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { StyleDescriptor } from '../../../../common/descriptor_types'; +import { CustomIcon, StyleDescriptor } from '../../../../common/descriptor_types'; import { ILayer } from '../../../classes/layers/layer'; export interface Props { layer: ILayer; updateStyleDescriptor: (styleDescriptor: StyleDescriptor) => void; + updateCustomIcons: (customIcons: CustomIcon[]) => void; } -export function StyleSettings({ layer, updateStyleDescriptor }: Props) { - const settingsEditor = layer.renderStyleEditor(updateStyleDescriptor); +export function StyleSettings({ layer, updateStyleDescriptor, updateCustomIcons }: Props) { + const settingsEditor = layer.renderStyleEditor(updateStyleDescriptor, updateCustomIcons); if (!settingsEditor) { return null; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/custom_icons_panel.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/custom_icons_panel.test.tsx.snap new file mode 100644 index 0000000000000..033adb3262115 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/custom_icons_panel.test.tsx.snap @@ -0,0 +1,125 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + + + +
+ +
+
+ + +

+ + + +

+
+ + + + + +
+
+`; + +exports[`should render with custom icons 1`] = ` + + + +
+ +
+
+ + " + symbolId="My Custom Icon" + />, + "key": "__kbn__custom_icon_sdf__foobar", + "label": "My Custom Icon", + }, + Object { + "extraAction": Object { + "alwaysShow": true, + "iconType": "gear", + "onClick": [Function], + }, + "icon": " + symbolId="My Other Custom Icon" + />, + "key": "__kbn__custom_icon_sdf__bizzbuzz", + "label": "My Other Custom Icon", + }, + ] + } + /> + + + + + +
+
+`; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/custom_icons_panel.test.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/custom_icons_panel.test.tsx new file mode 100644 index 0000000000000..2665a1e1d1858 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/custom_icons_panel.test.tsx @@ -0,0 +1,52 @@ +/* + * 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. + */ + +jest.mock('../../kibana_services', () => { + return { + getIsDarkMode() { + return false; + }, + }; +}); + +import React from 'react'; +import { shallow } from 'enzyme'; +import { CustomIconsPanel } from './custom_icons_panel'; + +const defaultProps = { + customIcons: [], + updateCustomIcons: () => {}, + deleteCustomIcon: () => {}, +}; + +test('should render', async () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); + +test('should render with custom icons', async () => { + const customIcons = [ + { + symbolId: '__kbn__custom_icon_sdf__foobar', + label: 'My Custom Icon', + svg: '', + cutoff: 0.25, + radius: 0.25, + }, + { + symbolId: '__kbn__custom_icon_sdf__bizzbuzz', + label: 'My Other Custom Icon', + svg: '', + cutoff: 0.3, + radius: 0.15, + }, + ]; + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/custom_icons_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/custom_icons_panel.tsx new file mode 100644 index 0000000000000..acc205a084b5d --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/custom_icons_panel.tsx @@ -0,0 +1,202 @@ +/* + * 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, { Component, Fragment } from 'react'; +import { + EuiButtonEmpty, + EuiListGroup, + EuiPanel, + EuiSpacer, + EuiText, + EuiTextAlign, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { DEFAULT_CUSTOM_ICON_CUTOFF, DEFAULT_CUSTOM_ICON_RADIUS } from '../../../common/constants'; +import { getIsDarkMode } from '../../kibana_services'; +// @ts-expect-error +import { getCustomIconId } from '../../classes/styles/vector/symbol_utils'; +import { SymbolIcon } from '../../classes/styles/vector/components/legend/symbol_icon'; +import { CustomIconModal } from '../../classes/styles/vector/components/symbol/custom_icon_modal'; +import { CustomIcon } from '../../../common/descriptor_types'; + +interface Props { + customIcons: CustomIcon[]; + updateCustomIcons: (customIcons: CustomIcon[]) => void; + deleteCustomIcon: (symbolId: string) => void; +} + +interface State { + isModalVisible: boolean; + selectedIcon?: CustomIcon; +} + +export class CustomIconsPanel extends Component { + public state = { + isModalVisible: false, + selectedIcon: undefined, + }; + + private _handleIconEdit = (icon: CustomIcon) => { + this.setState({ selectedIcon: icon, isModalVisible: true }); + }; + + private _handleNewIcon = () => { + this.setState({ isModalVisible: true }); + }; + + private _renderModal = () => { + if (!this.state.isModalVisible) { + return null; + } + if (this.state.selectedIcon) { + const { symbolId, label, svg, cutoff, radius } = this.state.selectedIcon; + return ( + + ); + } + return ( + + ); + }; + + private _hideModal = () => { + this.setState({ isModalVisible: false, selectedIcon: undefined }); + }; + + private _handleSave = (icon: CustomIcon) => { + const { symbolId, label, svg, cutoff, radius } = icon; + + const icons = [ + ...this.props.customIcons.filter((i) => { + return i.symbolId !== symbolId; + }), + { + symbolId, + svg, + label, + cutoff, + radius, + }, + ]; + this.props.updateCustomIcons(icons); + this._hideModal(); + }; + + private _handleDelete = (symbolId: string) => { + this.props.deleteCustomIcon(symbolId); + this._hideModal(); + }; + + private _renderCustomIconsList = () => { + const addIconButton = ( + + + this._handleNewIcon()} + data-test-subj="mapsCustomIconPanel-add" + > + + + + + ); + if (!this.props.customIcons.length) { + return ( + + +

+ + + +

+
+ {addIconButton} +
+ ); + } + + const customIconsList = this.props.customIcons.map((icon) => { + const { symbolId, label, svg } = icon; + return { + label, + key: symbolId, + icon: ( + + ), + extraAction: { + iconType: 'gear', + alwaysShow: true, + onClick: () => { + this._handleIconEdit(icon); + }, + }, + }; + }); + + return ( + + + {addIconButton} + + ); + }; + + public render() { + return ( + + + +
+ +
+
+ + {this._renderCustomIconsList()} +
+ {this._renderModal()} +
+ ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts index c858c74c819d5..e10e59e83dea6 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts @@ -11,8 +11,16 @@ import { ThunkDispatch } from 'redux-thunk'; import { FLYOUT_STATE } from '../../reducers/ui'; import { MapStoreState } from '../../reducers/store'; import { MapSettingsPanel } from './map_settings_panel'; -import { rollbackMapSettings, updateMapSetting, updateFlyout } from '../../actions'; +import { CustomIcon } from '../../../common/descriptor_types'; import { + deleteCustomIcon, + rollbackMapSettings, + updateCustomIcons, + updateMapSetting, + updateFlyout, +} from '../../actions'; +import { + getCustomIcons, getMapCenter, getMapSettings, getMapZoom, @@ -22,6 +30,7 @@ import { function mapStateToProps(state: MapStoreState) { return { center: getMapCenter(state), + customIcons: getCustomIcons(state), hasMapSettingsChanges: hasMapSettingsChanges(state), settings: getMapSettings(state), zoom: getMapZoom(state), @@ -40,6 +49,12 @@ function mapDispatchToProps(dispatch: ThunkDispatch { dispatch(updateMapSetting(settingKey, settingValue)); }, + updateCustomIcons: (customIcons: CustomIcon[]) => { + dispatch(updateCustomIcons(customIcons)); + }, + deleteCustomIcon: (symbolId: string) => { + dispatch(deleteCustomIcon(symbolId)); + }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx index baee9c4ff48a0..1efa07e280039 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx @@ -22,7 +22,8 @@ import { MapSettings } from '../../reducers/map'; import { NavigationPanel } from './navigation_panel'; import { SpatialFiltersPanel } from './spatial_filters_panel'; import { DisplayPanel } from './display_panel'; -import { MapCenter } from '../../../common/descriptor_types'; +import { CustomIconsPanel } from './custom_icons_panel'; +import { CustomIcon, MapCenter } from '../../../common/descriptor_types'; export interface Props { cancelChanges: () => void; @@ -30,7 +31,10 @@ export interface Props { hasMapSettingsChanges: boolean; keepChanges: () => void; settings: MapSettings; + customIcons: CustomIcon[]; updateMapSetting: (settingKey: string, settingValue: string | number | boolean | object) => void; + updateCustomIcons: (customIcons: CustomIcon[]) => void; + deleteCustomIcon: (symbolId: string) => void; zoom: number; } @@ -40,7 +44,10 @@ export function MapSettingsPanel({ hasMapSettingsChanges, keepChanges, settings, + customIcons, updateMapSetting, + updateCustomIcons, + deleteCustomIcon, zoom, }: Props) { // TODO move common text like Cancel and Close to common i18n translation @@ -77,6 +84,12 @@ export function MapSettingsPanel({ /> + + diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts index d46d4f53de47f..df03f755d6d2b 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts @@ -21,6 +21,7 @@ import { updateMetaFromTiles, } from '../../actions'; import { + getCustomIcons, getGoto, getLayerList, getMapReady, @@ -40,6 +41,7 @@ function mapStateToProps(state: MapStoreState) { return { isMapReady: getMapReady(state), settings: getMapSettings(state), + customIcons: getCustomIcons(state), layerList: getLayerList(state), spatialFiltersLayer: getSpatialFiltersLayer(state), goto: getGoto(state), diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index b4c13294e292d..f778fd06cce9b 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -23,12 +23,19 @@ import { ILayer } from '../../classes/layers/layer'; import { IVectorSource } from '../../classes/sources/vector_source'; import { MapSettings } from '../../reducers/map'; import { + CustomIcon, Goto, MapCenterAndZoom, TileMetaFeature, Timeslice, } from '../../../common/descriptor_types'; -import { DECIMAL_DEGREES_PRECISION, RawValue, ZOOM_PRECISION } from '../../../common/constants'; +import { + CUSTOM_ICON_SIZE, + DECIMAL_DEGREES_PRECISION, + MAKI_ICON_SIZE, + RawValue, + ZOOM_PRECISION, +} from '../../../common/constants'; import { getGlyphUrl } from '../../util'; import { syncLayerOrder } from './sort_layers'; @@ -39,12 +46,13 @@ import { TileStatusTracker } from './tile_status_tracker'; import { DrawFeatureControl } from './draw_control/draw_feature_control'; import type { MapExtentState } from '../../reducers/map/types'; // @ts-expect-error -import { createSdfIcon } from '../../classes/styles/vector/symbol_utils'; +import { CUSTOM_ICON_PIXEL_RATIO, createSdfIcon } from '../../classes/styles/vector/symbol_utils'; import { MAKI_ICONS } from '../../classes/styles/vector/maki_icons'; export interface Props { isMapReady: boolean; settings: MapSettings; + customIcons: CustomIcon[]; layerList: ILayer[]; spatialFiltersLayer: ILayer; goto?: Goto | null; @@ -78,6 +86,7 @@ export class MbMap extends Component { private _checker?: ResizeChecker; private _isMounted: boolean = false; private _containerRef: HTMLDivElement | null = null; + private _prevCustomIcons?: CustomIcon[]; private _prevDisableInteractive?: boolean; private _prevLayerList?: ILayer[]; private _prevTimeslice?: Timeslice; @@ -288,7 +297,7 @@ export class MbMap extends Component { const pixelRatio = Math.floor(window.devicePixelRatio); for (const [symbolId, { svg }] of Object.entries(MAKI_ICONS)) { if (!mbMap.hasImage(symbolId)) { - const imageData = await createSdfIcon(svg, 0.25, 0.25); + const imageData = await createSdfIcon({ renderSize: MAKI_ICON_SIZE, svg }); mbMap.addImage(symbolId, imageData, { pixelRatio, sdf: true, @@ -389,6 +398,27 @@ export class MbMap extends Component { } } + if ( + this._prevCustomIcons === undefined || + !_.isEqual(this._prevCustomIcons, this.props.customIcons) + ) { + this._prevCustomIcons = this.props.customIcons; + const mbMap = this.state.mbMap; + for (const { symbolId, svg, cutoff, radius } of this.props.customIcons) { + createSdfIcon({ svg, renderSize: CUSTOM_ICON_SIZE, cutoff, radius }).then( + (imageData: ImageData) => { + // @ts-expect-error MapboxMap type is missing updateImage method + if (mbMap.hasImage(symbolId)) mbMap.updateImage(symbolId, imageData); + else + mbMap.addImage(symbolId, imageData, { + sdf: true, + pixelRatio: CUSTOM_ICON_PIXEL_RATIO, + }); + } + ); + } + } + let zoomRangeChanged = false; if (this.props.settings.minZoom !== this.state.mbMap.getMinZoom()) { this.state.mbMap.setMinZoom(this.props.settings.minZoom); diff --git a/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts index f5af113b3b316..242d0684f565a 100644 --- a/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts +++ b/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts @@ -13,6 +13,7 @@ export function getDefaultMapSettings(): MapSettings { return { autoFitToDataBounds: false, backgroundColor: euiThemeVars.euiColorEmptyShade, + customIcons: [], disableInteractive: false, disableTooltipControl: false, hideToolbarOverlay: false, diff --git a/x-pack/plugins/maps/public/reducers/map/types.ts b/x-pack/plugins/maps/public/reducers/map/types.ts index d7fa98c24b46f..9f70c7e67271a 100644 --- a/x-pack/plugins/maps/public/reducers/map/types.ts +++ b/x-pack/plugins/maps/public/reducers/map/types.ts @@ -10,6 +10,7 @@ import type { Query } from 'src/plugins/data/common'; import { Filter } from '@kbn/es-query'; import { + CustomIcon, DrawState, EditState, Goto, @@ -51,6 +52,7 @@ export type MapContext = Partial & { export type MapSettings = { autoFitToDataBounds: boolean; backgroundColor: string; + customIcons: CustomIcon[]; disableInteractive: boolean; disableTooltipControl: boolean; hideToolbarOverlay: boolean; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts index baca2d79b833d..3c08a0e6f19ce 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts @@ -35,7 +35,7 @@ import { getQueryableUniqueIndexPatternIds, } from './map_selectors'; -import { LayerDescriptor, VectorLayerDescriptor } from '../../common/descriptor_types'; +import { CustomIcon, LayerDescriptor, VectorLayerDescriptor } from '../../common/descriptor_types'; import { ILayer } from '../classes/layers/layer'; import { Filter } from '@kbn/es-query'; import { ESSearchSource } from '../classes/sources/es_search_source'; @@ -255,8 +255,13 @@ describe('getQueryableUniqueIndexPatternIds', () => { ]; const waitingForMapReadyLayerList: VectorLayerDescriptor[] = [] as unknown as VectorLayerDescriptor[]; + const customIcons: CustomIcon[] = []; expect( - getQueryableUniqueIndexPatternIds.resultFunc(layerList, waitingForMapReadyLayerList) + getQueryableUniqueIndexPatternIds.resultFunc( + layerList, + waitingForMapReadyLayerList, + customIcons + ) ).toEqual(['foo', 'bar']); }); @@ -274,8 +279,13 @@ describe('getQueryableUniqueIndexPatternIds', () => { createWaitLayerDescriptorMock({ indexPatternId: 'fbr' }), createWaitLayerDescriptorMock({ indexPatternId: 'foo' }), ] as unknown as VectorLayerDescriptor[]; + const customIcons: CustomIcon[] = []; expect( - getQueryableUniqueIndexPatternIds.resultFunc(layerList, waitingForMapReadyLayerList) + getQueryableUniqueIndexPatternIds.resultFunc( + layerList, + waitingForMapReadyLayerList, + customIcons + ) ).toEqual(['foo', 'fbr']); }); }); diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 9253b27a50f66..f86f3dd927c69 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -43,6 +43,7 @@ import { MapStoreState } from '../reducers/store'; import { AbstractSourceDescriptor, DataRequestDescriptor, + CustomIcon, DrawState, EditState, Goto, @@ -65,6 +66,7 @@ import { getIsReadOnly } from './ui_selectors'; export function createLayerInstance( layerDescriptor: LayerDescriptor, + customIcons: CustomIcon[], inspectorAdapters?: Adapters, chartsPaletteServiceGetColor?: (value: string) => string | null ): ILayer { @@ -86,6 +88,7 @@ export function createLayerInstance( layerDescriptor: vectorLayerDescriptor, source: source as IVectorSource, joins, + customIcons, chartsPaletteServiceGetColor, }); case LAYER_TYPE.EMS_VECTOR_TILE: @@ -99,12 +102,14 @@ export function createLayerInstance( return new BlendedVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, source: source as IVectorSource, + customIcons, chartsPaletteServiceGetColor, }); case LAYER_TYPE.MVT_VECTOR: return new MvtVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, source: source as IVectorSource, + customIcons, }); default: throw new Error(`Unrecognized layerType ${layerDescriptor.type}`); @@ -184,6 +189,14 @@ export const getTimeFilters = ({ map }: MapStoreState): TimeRange => export const getTimeslice = ({ map }: MapStoreState) => map.mapState.timeslice; +export const getCustomIcons = ({ map }: MapStoreState): CustomIcon[] => { + return ( + map.settings.customIcons.map((icon) => { + return { ...icon, svg: Buffer.from(icon.svg, 'base64').toString('utf-8') }; + }) ?? [] + ); +}; + export const getQuery = ({ map }: MapStoreState): Query | undefined => map.mapState.query; export const getFilters = ({ map }: MapStoreState): Filter[] => map.mapState.filters; @@ -261,7 +274,8 @@ export const getDataFilters = createSelector( export const getSpatialFiltersLayer = createSelector( getFilters, getMapSettings, - (filters, settings) => { + getCustomIcons, + (filters, settings, customIcons) => { const featureCollection: FeatureCollection = { type: 'FeatureCollection', features: extractFeaturesFromFilters(filters), @@ -298,6 +312,7 @@ export const getSpatialFiltersLayer = createSelector( }), }), source: new GeoJsonFileSource(geoJsonSourceDescriptor), + customIcons, }); } ); @@ -306,9 +321,15 @@ export const getLayerList = createSelector( getLayerListRaw, getInspectorAdapters, getChartsPaletteServiceGetColor, - (layerDescriptorList, inspectorAdapters, chartsPaletteServiceGetColor) => { + getCustomIcons, + (layerDescriptorList, inspectorAdapters, chartsPaletteServiceGetColor, customIcons) => { return layerDescriptorList.map((layerDescriptor) => - createLayerInstance(layerDescriptor, inspectorAdapters, chartsPaletteServiceGetColor) + createLayerInstance( + layerDescriptor, + customIcons, + inspectorAdapters, + chartsPaletteServiceGetColor + ) ); } ); @@ -375,12 +396,13 @@ export const getSelectedLayerJoinDescriptors = createSelector(getSelectedLayer, export const getQueryableUniqueIndexPatternIds = createSelector( getLayerList, getWaitingForMapReadyLayerListRaw, - (layerList, waitingForMapReadyLayerList) => { + getCustomIcons, + (layerList, waitingForMapReadyLayerList, customIcons) => { const indexPatternIds: string[] = []; if (waitingForMapReadyLayerList.length) { waitingForMapReadyLayerList.forEach((layerDescriptor) => { - const layer = createLayerInstance(layerDescriptor); + const layer = createLayerInstance(layerDescriptor, customIcons); if (layer.isVisible()) { indexPatternIds.push(...layer.getQueryableIndexPatternIds()); } @@ -399,12 +421,13 @@ export const getQueryableUniqueIndexPatternIds = createSelector( export const getGeoFieldNames = createSelector( getLayerList, getWaitingForMapReadyLayerListRaw, - (layerList, waitingForMapReadyLayerList) => { + getCustomIcons, + (layerList, waitingForMapReadyLayerList, customIcons) => { const geoFieldNames: string[] = []; if (waitingForMapReadyLayerList.length) { waitingForMapReadyLayerList.forEach((layerDescriptor) => { - const layer = createLayerInstance(layerDescriptor); + const layer = createLayerInstance(layerDescriptor, customIcons); geoFieldNames.push(...layer.getGeoFieldNames()); }); } else { diff --git a/yarn.lock b/yarn.lock index 7168c8af39761..eabf97f983bc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6562,6 +6562,11 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/raf@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2" + integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw== + "@types/rbush@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-3.0.0.tgz#b6887d99b159e87ae23cd14eceff34f139842aa6" @@ -9737,6 +9742,20 @@ canvg@^3.0.9: stackblur-canvas "^2.0.0" svg-pathdata "^6.0.3" +canvg@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.9.tgz#9ba095f158b94b97ca2c9c1c40785b11dc08df6d" + integrity sha512-rDXcnRPuz4QHoCilMeoTxql+fvGqNAxp+qV/KHD8rOiJSAfVjFclbdUNHD2Uqfthr+VMg17bD2bVuk6F07oLGw== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/raf" "^3.4.0" + core-js "^3.8.3" + raf "^3.4.1" + regenerator-runtime "^0.13.7" + rgbcolor "^1.0.1" + stackblur-canvas "^2.0.0" + svg-pathdata "^6.0.3" + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -10830,6 +10849,11 @@ core-js@^3.0.4, core-js@^3.21.1, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== +core-js@^3.8.3: + version "3.19.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.1.tgz#f6f173cae23e73a7d88fa23b6e9da329276c6641" + integrity sha512-Tnc7E9iKd/b/ff7GFbhwPVzJzPztGrChB8X8GLqoYGdEOG8IpLnK1xPyo3ZoO3HsK6TodJS58VGPOxA+hLHQMg== + core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" From d9d5f15fe472a2a7def623bd0f877c5118c5c273 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Wed, 30 Mar 2022 00:59:09 -0400 Subject: [PATCH 057/108] [Security Solution][Endpoint][Admin][Policy] Remove page header and update ui for endpoint and policy empty state (#128844) --- .../components/management_empty_state.tsx | 17 +++++++++++------ .../pages/endpoint_hosts/view/index.tsx | 1 + .../pages/policy/view/policy_list.tsx | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx index b4a03fb18b2fe..b659cdd118837 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import styled from 'styled-components'; import onboardingLogo from '../images/security_administration_onboarding.svg'; import { useKibana } from '../../common/lib/kibana'; @@ -40,6 +41,10 @@ interface ManagementStep { children: JSX.Element; } +const StyledDiv = styled.div` + padding-left: 20%; +`; + const PolicyEmptyState = React.memo<{ loading: boolean; onActionClick: (event: MouseEvent) => void; @@ -48,7 +53,7 @@ const PolicyEmptyState = React.memo<{ }>(({ loading, onActionClick, actionDisabled, policyEntryPoint = false }) => { const docLinks = useKibana().services.docLinks; return ( -
+ {loading ? ( @@ -57,14 +62,14 @@ const PolicyEmptyState = React.memo<{ ) : ( - + -

+

-

+
@@ -118,12 +123,12 @@ const PolicyEmptyState = React.memo<{
- + )} -
+ ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index c2d01d0181d3b..c43c20a4bcae1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -671,6 +671,7 @@ export const EndpointList = () => { return ( { return ( Date: Tue, 29 Mar 2022 22:05:05 -0700 Subject: [PATCH 058/108] [Security Solution][Actions] - Update newest bulk routes to include action migration logic (#128518) Partially addresses #127918 Thanks to our newly added telemetry noticed that in 8.0+ versions there continued to be enabled rules with legacy actions. This was not expected as we expected that on re-enable of rules post 8.0 update, the rules' actions would be migrated so we would see 0 enabled rules with legacy actions. The new bulk routes were added in 8.1 so this does not answer why we see some 8.0 legacy actions, but could account for some of the 8.1+ ones. This PR adds the migration code to the new rule bulk routes and adds e2e tests. --- .../routes/rules/perform_bulk_action_route.ts | 93 +++++- .../security_and_spaces/tests/patch_rules.ts | 2 +- .../tests/patch_rules_bulk.ts | 2 +- .../tests/perform_bulk_action.ts | 266 ++++++++++++++++++ .../detection_engine_api_integration/utils.ts | 19 +- 5 files changed, 366 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index d8978cd8b11aa..4c4eee366ab84 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -9,10 +9,12 @@ import { truncate } from 'lodash'; import moment from 'moment'; import { BadRequestError, transformError } from '@kbn/securitysolution-es-utils'; import { KibanaResponseFactory, Logger } from 'src/core/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { RuleAlertType } from '../../rules/types'; import type { RulesClient } from '../../../../../../alerting/server'; +import { SanitizedAlert } from '../../../../../../alerting/common'; import { DETECTION_ENGINE_RULES_BULK_ACTION, @@ -42,6 +44,8 @@ import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { buildSiemResponse } from '../utils'; import { AbortError } from '../../../../../../../../src/plugins/kibana_utils/common'; import { internalRuleToAPIResponse } from '../../schemas/rule_converters'; +import { legacyMigrate } from '../../rules/utils'; +import { RuleParams } from '../../schemas/rule_schemas'; const MAX_RULES_TO_PROCESS_TOTAL = 10000; const MAX_ERROR_MESSAGE_LENGTH = 1000; @@ -191,6 +195,39 @@ const fetchRulesByQueryOrIds = async ({ }; }; +/** + * Helper method to migrate any legacy actions a rule may have. If no actions or no legacy actions + * no migration is performed. + * @params rulesClient + * @params savedObjectsClient + * @params rule - rule to be migrated + * @returns The migrated rule + */ +export const migrateRuleActions = async ({ + rulesClient, + savedObjectsClient, + rule, +}: { + rulesClient: RulesClient; + savedObjectsClient: SavedObjectsClientContract; + rule: RuleAlertType; +}): Promise> => { + const migratedRule = await legacyMigrate({ + rulesClient, + savedObjectsClient, + rule, + }); + + // This should only be hit if `rule` passed into `legacyMigrate` + // is `null` or `rule.id` is null which right now, as typed, should not occur + // but catching if does, in which case something upstream would be breaking down + if (migratedRule == null) { + throw new Error(`An error occurred processing rule with id:${rule.id}`); + } + + return migratedRule; +}; + export const performBulkActionRoute = ( router: SecuritySolutionPluginRouter, ml: SetupPlugins['ml'], @@ -264,13 +301,19 @@ export const performBulkActionRoute = ( concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL, items: rules, executor: async (rule) => { - if (!rule.enabled) { - throwAuthzError(await mlAuthz.validateRuleType(rule.params.type)); - await rulesClient.enable({ id: rule.id }); + const migratedRule = await migrateRuleActions({ + rulesClient, + savedObjectsClient, + rule, + }); + + if (!migratedRule.enabled) { + throwAuthzError(await mlAuthz.validateRuleType(migratedRule.params.type)); + await rulesClient.enable({ id: migratedRule.id }); } return { - ...rule, + ...migratedRule, enabled: true, }; }, @@ -282,13 +325,19 @@ export const performBulkActionRoute = ( concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL, items: rules, executor: async (rule) => { - if (rule.enabled) { - throwAuthzError(await mlAuthz.validateRuleType(rule.params.type)); - await rulesClient.disable({ id: rule.id }); + const migratedRule = await migrateRuleActions({ + rulesClient, + savedObjectsClient, + rule, + }); + + if (migratedRule.enabled) { + throwAuthzError(await mlAuthz.validateRuleType(migratedRule.params.type)); + await rulesClient.disable({ id: migratedRule.id }); } return { - ...rule, + ...migratedRule, enabled: false, }; }, @@ -300,8 +349,14 @@ export const performBulkActionRoute = ( concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL, items: rules, executor: async (rule) => { + const migratedRule = await migrateRuleActions({ + rulesClient, + savedObjectsClient, + rule, + }); + await deleteRules({ - ruleId: rule.id, + ruleId: migratedRule.id, rulesClient, ruleExecutionLog, }); @@ -316,10 +371,16 @@ export const performBulkActionRoute = ( concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL, items: rules, executor: async (rule) => { - throwAuthzError(await mlAuthz.validateRuleType(rule.params.type)); + const migratedRule = await migrateRuleActions({ + rulesClient, + savedObjectsClient, + rule, + }); + + throwAuthzError(await mlAuthz.validateRuleType(migratedRule.params.type)); const createdRule = await rulesClient.create({ - data: duplicateRule(rule, isRuleRegistryEnabled), + data: duplicateRule(migratedRule, isRuleRegistryEnabled), }); return createdRule; @@ -357,9 +418,15 @@ export const performBulkActionRoute = ( throwAuthzError(await mlAuthz.validateRuleType(rule.params.type)); + const migratedRule = await migrateRuleActions({ + rulesClient, + savedObjectsClient, + rule, + }); + const editedRule = body[BulkAction.edit].reduce( (acc, action) => applyBulkActionEditToRule(acc, action), - rule + migratedRule ); const { tags, params: { timelineTitle, timelineId } = {} } = editedRule; @@ -367,7 +434,7 @@ export const performBulkActionRoute = ( await patchRules({ rulesClient, - rule, + rule: migratedRule, tags, index, timelineTitle, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts index 7867fc2fd16e4..ccfd8371c3f2d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts @@ -224,7 +224,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, ]; - outputRule.throttle = '1m'; + outputRule.throttle = '1h'; const bodyToCompare = removeServerGeneratedProperties(patchResponse.body); expect(bodyToCompare).to.eql(outputRule); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts index d3128c6670402..51cf1a334a2c7 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts @@ -171,7 +171,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, ]; - outputRule.throttle = '1m'; + outputRule.throttle = '1h'; const bodyToCompare = removeServerGeneratedProperties(response); expect(bodyToCompare).to.eql(outputRule); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts index 1bbfafd8f0b14..5b36d9a880397 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts @@ -25,11 +25,15 @@ import { getSimpleRule, getSimpleRuleOutput, removeServerGeneratedProperties, + createLegacyRuleAction, + getLegacyActionSO, } from '../../utils'; +import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const es = getService('es'); const log = getService('log'); const postBulkAction = () => @@ -95,6 +99,45 @@ export default ({ getService }: FtrProviderContext): void => { await fetchRule(ruleId).expect(404); }); + it('should delete rules and any associated legacy actions', async () => { + const ruleId = 'ruleId'; + const [connector, rule1] = await Promise.all([ + supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: '.slack', + secrets: { + webhookUrl: 'http://localhost:1234', + }, + }), + createRule(supertest, log, getSimpleRule(ruleId, false)), + ]); + await createLegacyRuleAction(supertest, rule1.id, connector.body.id); + + // check for legacy sidecar action + const sidecarActionsResults = await getLegacyActionSO(es); + expect(sidecarActionsResults.hits.hits.length).to.eql(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(rule1.id); + + const { body } = await postBulkAction() + .send({ query: '', action: BulkAction.delete }) + .expect(200); + + expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + + // Check that the deleted rule is returned with the response + expect(body.attributes.results.deleted[0].name).to.eql(rule1.name); + + // legacy sidecar action should be gone + const sidecarActionsPostResults = await getLegacyActionSO(es); + expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + + // Check that the updates have been persisted + await fetchRule(ruleId).expect(404); + }); + it('should enable rules', async () => { const ruleId = 'ruleId'; await createRule(supertest, log, getSimpleRule(ruleId)); @@ -113,6 +156,57 @@ export default ({ getService }: FtrProviderContext): void => { expect(ruleBody.enabled).to.eql(true); }); + it('should enable rules and migrate actions', async () => { + const ruleId = 'ruleId'; + const [connector, rule1] = await Promise.all([ + supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: '.slack', + secrets: { + webhookUrl: 'http://localhost:1234', + }, + }), + createRule(supertest, log, getSimpleRule(ruleId, false)), + ]); + await createLegacyRuleAction(supertest, rule1.id, connector.body.id); + + // check for legacy sidecar action + const sidecarActionsResults = await getLegacyActionSO(es); + expect(sidecarActionsResults.hits.hits.length).to.eql(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(rule1.id); + + const { body } = await postBulkAction() + .send({ query: '', action: BulkAction.enable }) + .expect(200); + + expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + + // Check that the updated rule is returned with the response + expect(body.attributes.results.updated[0].enabled).to.eql(true); + + // Check that the updates have been persisted + const { body: ruleBody } = await fetchRule(ruleId).expect(200); + + // legacy sidecar action should be gone + const sidecarActionsPostResults = await getLegacyActionSO(es); + expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + + expect(ruleBody.enabled).to.eql(true); + expect(ruleBody.actions).to.eql([ + { + action_type_id: '.slack', + group: 'default', + id: connector.body.id, + params: { + message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ]); + }); + it('should disable rules', async () => { const ruleId = 'ruleId'; await createRule(supertest, log, getSimpleRule(ruleId, true)); @@ -131,6 +225,57 @@ export default ({ getService }: FtrProviderContext): void => { expect(ruleBody.enabled).to.eql(false); }); + it('should disable rules and migrate actions', async () => { + const ruleId = 'ruleId'; + const [connector, rule1] = await Promise.all([ + supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: '.slack', + secrets: { + webhookUrl: 'http://localhost:1234', + }, + }), + createRule(supertest, log, getSimpleRule(ruleId, true)), + ]); + await createLegacyRuleAction(supertest, rule1.id, connector.body.id); + + // check for legacy sidecar action + const sidecarActionsResults = await getLegacyActionSO(es); + expect(sidecarActionsResults.hits.hits.length).to.eql(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(rule1.id); + + const { body } = await postBulkAction() + .send({ query: '', action: BulkAction.disable }) + .expect(200); + + expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + + // Check that the updated rule is returned with the response + expect(body.attributes.results.updated[0].enabled).to.eql(false); + + // Check that the updates have been persisted + const { body: ruleBody } = await fetchRule(ruleId).expect(200); + + // legacy sidecar action should be gone + const sidecarActionsPostResults = await getLegacyActionSO(es); + expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + + expect(ruleBody.enabled).to.eql(false); + expect(ruleBody.actions).to.eql([ + { + action_type_id: '.slack', + group: 'default', + id: connector.body.id, + params: { + message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ]); + }); + it('should duplicate rules', async () => { const ruleId = 'ruleId'; const ruleToDuplicate = getSimpleRule(ruleId); @@ -154,6 +299,66 @@ export default ({ getService }: FtrProviderContext): void => { expect(rulesResponse.total).to.eql(2); }); + it('should duplicate rule with a legacy action and migrate new rules action', async () => { + const ruleId = 'ruleId'; + const [connector, ruleToDuplicate] = await Promise.all([ + supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: '.slack', + secrets: { + webhookUrl: 'http://localhost:1234', + }, + }), + createRule(supertest, log, getSimpleRule(ruleId, true)), + ]); + await createLegacyRuleAction(supertest, ruleToDuplicate.id, connector.body.id); + + // check for legacy sidecar action + const sidecarActionsResults = await getLegacyActionSO(es); + expect(sidecarActionsResults.hits.hits.length).to.eql(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql( + ruleToDuplicate.id + ); + + const { body } = await postBulkAction() + .send({ query: '', action: BulkAction.duplicate }) + .expect(200); + + expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + + // Check that the duplicated rule is returned with the response + expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + + // legacy sidecar action should be gone + const sidecarActionsPostResults = await getLegacyActionSO(es); + expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + + // Check that the updates have been persisted + const { body: rulesResponse } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(rulesResponse.total).to.eql(2); + + rulesResponse.data.forEach((rule: RulesSchema) => { + expect(rule.actions).to.eql([ + { + action_type_id: '.slack', + group: 'default', + id: connector.body.id, + params: { + message: + 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ]); + }); + }); + describe('edit action', () => { it('should set, add and delete tags in rules', async () => { const ruleId = 'ruleId'; @@ -224,6 +429,67 @@ export default ({ getService }: FtrProviderContext): void => { expect(deletedTagsRule.tags).to.eql(['tag2']); }); + it('should migrate legacy actions on edit', async () => { + const ruleId = 'ruleId'; + const [connector, ruleToDuplicate] = await Promise.all([ + supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: '.slack', + secrets: { + webhookUrl: 'http://localhost:1234', + }, + }), + createRule(supertest, log, getSimpleRule(ruleId, true)), + ]); + await createLegacyRuleAction(supertest, ruleToDuplicate.id, connector.body.id); + + // check for legacy sidecar action + const sidecarActionsResults = await getLegacyActionSO(es); + expect(sidecarActionsResults.hits.hits.length).to.eql(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql( + ruleToDuplicate.id + ); + + const { body: setTagsBody } = await postBulkAction() + .send({ + query: '', + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_tags, + value: ['reset-tag'], + }, + ], + }) + .expect(200); + + expect(setTagsBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + + // Check that the updates have been persisted + const { body: setTagsRule } = await fetchRule(ruleId).expect(200); + + // Sidecar should be removed + const sidecarActionsPostResults = await getLegacyActionSO(es); + expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + + expect(setTagsRule.tags).to.eql(['reset-tag']); + + expect(setTagsRule.actions).to.eql([ + { + action_type_id: '.slack', + group: 'default', + id: connector.body.id, + params: { + message: + 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ]); + }); + it('should set, add and delete index patterns in rules', async () => { const ruleId = 'ruleId'; const indices = ['index1-*', 'index2-*']; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 2fc5cdadecb2a..ebf3a7008cd57 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -26,6 +26,8 @@ import type { } from '@kbn/securitysolution-io-ts-list-types'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { ToolingLog } from '@kbn/dev-utils'; +import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { SavedObjectReference } from 'kibana/server'; import { PrePackagedRulesAndTimelinesStatusSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response'; import { CreateRulesSchema, @@ -59,6 +61,7 @@ import { UPDATE_OR_CREATE_LEGACY_ACTIONS, } from '../../plugins/security_solution/common/constants'; import { DetectionMetrics } from '../../plugins/security_solution/server/usage/detections/types'; +import { LegacyRuleActions } from '../../plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types'; import { DetectionAlert } from '../../plugins/security_solution/common/detection_engine/schemas/alerts'; /** @@ -638,7 +641,7 @@ export const createLegacyRuleAction = async ( .query({ alert_id: alertId }) .send({ name: 'Legacy notification with one action', - interval: '1m', + interval: '1h', actions: [ { id: connectorId, @@ -2100,3 +2103,17 @@ export const getSimpleThreatMatch = ( ], threat_filters: [], }); + +interface LegacyActionSO extends LegacyRuleActions { + references: SavedObjectReference[]; +} + +/** + * Fetch all legacy action sidecar SOs from the .kibana index + * @param es The ElasticSearch service + */ +export const getLegacyActionSO = async (es: Client): Promise> => + es.search({ + index: '.kibana', + q: 'type:siem-detection-engine-rule-actions', + }); From 4f070b3435b0b4e6f739ac6949a93ac9a1338944 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 30 Mar 2022 01:04:26 -0600 Subject: [PATCH 059/108] [ML][Maps] Anomaly Detection: Add link to maps in charts section of Anomaly Explorer (#128697) * add link to maps in charts section of explorer * linting fix and wrap path generator in useCallback * export necessary constants from maps plugin * add size constant * typical layer to grey. Update maps export. Fix jest test * add query for partition fields * add tests and escape special characters --- x-pack/plugins/maps/common/index.ts | 1 + x-pack/plugins/maps/public/index.ts | 1 + .../explorer_charts_container.js | 138 ++++++++++++++++++ .../explorer_charts_container.test.js | 101 ++++++++++++- .../maps/anomaly_layer_wizard_factory.tsx | 2 +- .../plugins/ml/public/maps/anomaly_source.tsx | 28 +++- 6 files changed, 264 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/maps/common/index.ts b/x-pack/plugins/maps/common/index.ts index fc86af0e2b1f5..56abccb510ad5 100644 --- a/x-pack/plugins/maps/common/index.ts +++ b/x-pack/plugins/maps/common/index.ts @@ -7,6 +7,7 @@ export { AGG_TYPE, + APP_ID, COLOR_MAP_TYPE, DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 071300b7784fb..6b8767abe9c11 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -18,6 +18,7 @@ export const plugin: PluginInitializer = ( }; export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +export { MAPS_APP_LOCATOR } from './locators'; export type { PreIndexedShape } from '../common/elasticsearch_util'; export { GEOJSON_FEATURE_ID_PROPERTY_NAME } from './classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 2029211c98970..79a1121a98a62 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -7,6 +7,7 @@ import './_index.scss'; import React, { useEffect, useState, useCallback, useRef } from 'react'; +import { escapeKuery } from '@kbn/es-query'; import { EuiButtonEmpty, @@ -15,6 +16,7 @@ import { EuiFlexItem, EuiIconTip, EuiToolTip, + htmlIdGenerator, } from '@elastic/eui'; import { @@ -27,15 +29,22 @@ import { ExplorerChartSingleMetric } from './explorer_chart_single_metric'; import { ExplorerChartLabel } from './components/explorer_chart_label'; import { CHART_TYPE } from '../explorer_constants'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { MlTooltipComponent } from '../../components/chart_tooltip'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useMlKibana } from '../../contexts/kibana'; import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; +import { AnomalySource } from '../../../maps/anomaly_source'; +import { CUSTOM_COLOR_RAMP } from '../../../maps/anomaly_layer_wizard_factory'; +import { LAYER_TYPE, APP_ID as MAPS_APP_ID } from '../../../../../maps/common'; +import { MAPS_APP_LOCATOR } from '../../../../../maps/public'; import { ExplorerChartsErrorCallOuts } from './explorer_charts_error_callouts'; import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; import { EmbeddedMapComponentWrapper } from './explorer_chart_embedded_map'; import { useActiveCursor } from '../../../../../../../src/plugins/charts/public'; +import { ML_ANOMALY_LAYERS } from '../../../maps/util'; import { Chart, Settings } from '@elastic/charts'; import useObservable from 'react-use/lib/useObservable'; @@ -53,6 +62,20 @@ const textViewButton = i18n.translate( const mapsPluginMessage = i18n.translate('xpack.ml.explorer.charts.mapsPluginMissingMessage', { defaultMessage: 'maps or embeddable start plugin not found', }); +const openInMapsPluginMessage = i18n.translate('xpack.ml.explorer.charts.openInMapsPluginMessage', { + defaultMessage: 'Open in Maps', +}); + +export function getEntitiesQuery(series) { + const queryString = series.entityFields + ?.map(({ fieldName, fieldValue }) => `${escapeKuery(fieldName)}:${escapeKuery(fieldValue)}`) + .join(' or '); + const query = { + language: SEARCH_QUERY_LANGUAGE.KUERY, + query: queryString, + }; + return { query, queryString }; +} // create a somewhat unique ID // from charts metadata for React's key attribute @@ -79,6 +102,81 @@ function ExplorerChartContainer({ chartsService, }) { const [explorerSeriesLink, setExplorerSeriesLink] = useState(''); + const [mapsLink, setMapsLink] = useState(''); + + const { + services: { + data, + share, + application: { navigateToApp }, + }, + } = useMlKibana(); + + const getMapsLink = useCallback(async () => { + const { queryString, query } = getEntitiesQuery(series); + const initialLayers = []; + const typicalStyle = { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#98A2B2', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }; + + const style = { + type: 'VECTOR', + properties: { + fillColor: CUSTOM_COLOR_RAMP, + lineColor: CUSTOM_COLOR_RAMP, + }, + isTimeAware: false, + }; + + for (const layer in ML_ANOMALY_LAYERS) { + if (ML_ANOMALY_LAYERS.hasOwnProperty(layer)) { + initialLayers.push({ + id: htmlIdGenerator()(), + type: LAYER_TYPE.GEOJSON_VECTOR, + sourceDescriptor: AnomalySource.createDescriptor({ + jobId: series.jobId, + typicalActual: ML_ANOMALY_LAYERS[layer], + }), + style: ML_ANOMALY_LAYERS[layer] === ML_ANOMALY_LAYERS.TYPICAL ? typicalStyle : style, + }); + } + } + + const locator = share.url.locators.get(MAPS_APP_LOCATOR); + const location = await locator.getLocation({ + initialLayers: initialLayers, + timeRange: data.query.timefilter.timefilter.getTime(), + ...(queryString !== undefined ? { query } : {}), + }); + + return location; + }, [series?.jobId]); useEffect(() => { let isCancelled = false; @@ -98,6 +196,29 @@ function ExplorerChartContainer({ }; }, [mlLocator, series]); + useEffect( + function getMapsPluginLink() { + if (!series) return; + let isCancelled = false; + const generateLink = async () => { + if (!isCancelled) { + try { + const mapsLink = await getMapsLink(); + setMapsLink(mapsLink?.path); + } catch (error) { + console.error(error); + setMapsLink(''); + } + } + }; + generateLink().catch(console.error); + return () => { + isCancelled = true; + }; + }, + [series] + ); + const chartRef = useRef(null); const chartTheme = chartsService.theme.useChartsTheme(); @@ -191,6 +312,23 @@ function ExplorerChartContainer({ )} + {chartType === CHART_TYPE.GEO_MAP && mapsLink ? ( + + { + await navigateToApp(MAPS_APP_ID, { path: mapsLink }); + }} + > + + + + ) : null} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js index 903a0d75e6f60..0d324a83e27ad 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js @@ -11,7 +11,7 @@ import { mount, shallow } from 'enzyme'; import { I18nProvider } from '@kbn/i18n-react'; import { getDefaultChartsData } from './explorer_charts_container_service'; -import { ExplorerChartsContainer } from './explorer_charts_container'; +import { ExplorerChartsContainer, getEntitiesQuery } from './explorer_charts_container'; import { chartData } from './__mocks__/mock_chart_data'; import seriesConfig from './__mocks__/mock_series_config_filebeat.json'; @@ -37,6 +37,40 @@ jest.mock('../../../../../../../src/plugins/kibana_react/public', () => ({ }, })); +jest.mock('../../contexts/kibana', () => ({ + useMlKibana: () => { + return { + services: { + share: { + url: { + locators: { + get: jest.fn(() => { + return { + getLocation: jest.fn(() => ({ path: '/#maps' })), + }; + }), + }, + }, + }, + data: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn(() => { + return { from: '', to: '' }; + }), + }, + }, + }, + }, + application: { + navigateToApp: jest.fn(), + }, + }, + }; + }, +})); + const getUtilityProps = () => { const mlUrlGenerator = { createUrl: jest.fn(), @@ -124,4 +158,69 @@ describe('ExplorerChartsContainer', () => { // Check if the additional y-axis information for rare charts is part of the chart expect(wrapper.html().search(rareChartUniqueString)).toBeGreaterThan(0); }); + + describe('getEntitiesQuery', () => { + test('no entity fields', () => { + const series = {}; + const expected = { + query: { language: 'kuery', query: undefined }, + queryString: undefined, + }; + const actual = getEntitiesQuery(series); + expect(actual).toMatchObject(expected); + }); + + test('with entity field', () => { + const series = { + entityFields: [{ fieldName: 'testFieldName', fieldValue: 'testFieldValue' }], + }; + const expected = { + query: { language: 'kuery', query: 'testFieldName:testFieldValue' }, + queryString: 'testFieldName:testFieldValue', + }; + const actual = getEntitiesQuery(series); + expect(actual).toMatchObject(expected); + }); + + test('with multiple entity fields', () => { + const series = { + entityFields: [ + { fieldName: 'testFieldName1', fieldValue: 'testFieldValue1' }, + { fieldName: 'testFieldName2', fieldValue: 'testFieldValue2' }, + ], + }; + const expected = { + query: { + language: 'kuery', + query: 'testFieldName1:testFieldValue1 or testFieldName2:testFieldValue2', + }, + queryString: 'testFieldName1:testFieldValue1 or testFieldName2:testFieldValue2', + }; + const actual = getEntitiesQuery(series); + expect(actual).toMatchObject(expected); + }); + + test('with entity field with special characters', () => { + const series = { + entityFields: [ + { + fieldName: 'agent.keyword', + fieldValue: + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24', + }, + ], + }; + const expected = { + query: { + language: 'kuery', + query: + 'agent.keyword:Mozilla/5.0 \\(X11; Linux i686\\) AppleWebKit/534.24 \\(KHTML, like Gecko\\) Chrome/11.0.696.50 Safari/534.24', + }, + queryString: + 'agent.keyword:Mozilla/5.0 \\(X11; Linux i686\\) AppleWebKit/534.24 \\(KHTML, like Gecko\\) Chrome/11.0.696.50 Safari/534.24', + }; + const actual = getEntitiesQuery(series); + expect(actual).toMatchObject(expected); + }); + }); }); diff --git a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx index 260d058b78e78..e4309247a272a 100644 --- a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx +++ b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx @@ -26,7 +26,7 @@ import type { MlPluginStart, MlStartDependencies } from '../plugin'; import type { MlApiServices } from '../application/services/ml_api_service'; export const ML_ANOMALY = 'ML_ANOMALIES'; -const CUSTOM_COLOR_RAMP = { +export const CUSTOM_COLOR_RAMP = { type: STYLE_TYPE.DYNAMIC, options: { customColorRamp: SEVERITY_COLOR_RAMP, diff --git a/x-pack/plugins/ml/public/maps/anomaly_source.tsx b/x-pack/plugins/ml/public/maps/anomaly_source.tsx index 07f6df52f44e5..1ae65b49adc69 100644 --- a/x-pack/plugins/ml/public/maps/anomaly_source.tsx +++ b/x-pack/plugins/ml/public/maps/anomaly_source.tsx @@ -33,6 +33,8 @@ import { getResultsForJobId, ML_ANOMALY_LAYERS, MlAnomalyLayersType } from './ut import { UpdateAnomalySourceEditor } from './update_anomaly_source_editor'; import type { MlApiServices } from '../application/services/ml_api_service'; +const RESULT_LIMIT = 1000; + export interface AnomalySourceDescriptor extends AbstractSourceDescriptor { jobId: string; typicalActual: MlAnomalyLayersType; @@ -59,7 +61,7 @@ export class AnomalySource implements IVectorSource { constructor(sourceDescriptor: Partial, adapters?: Adapters) { this._descriptor = AnomalySource.createDescriptor(sourceDescriptor); } - // TODO: implement query awareness + async getGeoJsonWithMeta( layerName: string, searchFilters: VectorSourceRequestMeta, @@ -77,7 +79,7 @@ export class AnomalySource implements IVectorSource { data: results, meta: { // Set this to true if data is incomplete (e.g. capping number of results to first 1k) - areResultsTrimmed: false, + areResultsTrimmed: results.features.length === RESULT_LIMIT, }, }; } @@ -147,8 +149,23 @@ export class AnomalySource implements IVectorSource { return null; } - getSourceStatus() { - return { tooltipContent: null, areResultsTrimmed: true }; + getSourceStatus(sourceDataRequest?: DataRequest): SourceStatus { + const meta = sourceDataRequest ? sourceDataRequest.getMeta() : null; + + if (meta?.areResultsTrimmed) { + return { + tooltipContent: i18n.translate('xpack.ml.maps.resultsTrimmedMsg', { + defaultMessage: `Results limited to first {count} documents.`, + values: { count: RESULT_LIMIT }, + }), + areResultsTrimmed: true, + }; + } + + return { + tooltipContent: null, + areResultsTrimmed: false, + }; } getType(): string { @@ -222,12 +239,13 @@ export class AnomalySource implements IVectorSource { } getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceStatus { + const meta = sourceDataRequest ? sourceDataRequest.getMeta() : null; return { tooltipContent: i18n.translate('xpack.ml.maps.sourceTooltip', { defaultMessage: 'Shows anomalies', }), // set to true if data is incomplete (we limit to first 1000 results) - areResultsTrimmed: true, + areResultsTrimmed: meta?.areResultsTrimmed ?? false, }; } From d0c06b0112c97eb83f67a9375a4d92b57ef03b25 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Wed, 30 Mar 2022 12:19:46 +0500 Subject: [PATCH 060/108] [Discover] Update document explorer callout wording (#128556) - Adding a new callout when Document explorer is active, linking to documentation Co-authored-by: Matthias Wilhelm --- .../document_explorer_callout.tsx | 29 +++- .../document_explorer_update_callout.test.tsx | 60 +++++++++ .../document_explorer_update_callout.tsx | 124 ++++++++++++++++++ .../components/layout/discover_documents.tsx | 52 ++++---- src/plugins/discover/server/ui_settings.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 239 insertions(+), 30 deletions(-) create mode 100644 src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx create mode 100644 src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx index 73fc7cdf9a105..9b35b52dbc87f 100644 --- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import './document_explorer_callout.scss'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -17,7 +17,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, + useEuiTheme, } from '@elastic/eui'; +import { css } from '@emotion/react'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; import { DOC_TABLE_LEGACY } from '../../../../../common'; import { Storage } from '../../../../../../kibana_utils/public'; @@ -32,7 +34,11 @@ const updateStoredCalloutState = (newState: boolean, storage: Storage) => { storage.set(CALLOUT_STATE_KEY, newState); }; +/** + * The callout that's displayed when Document explorer is disabled + */ export const DocumentExplorerCallout = () => { + const { euiTheme } = useEuiTheme(); const { storage, capabilities, docLinks, addBasePath } = useDiscoverServices(); const [calloutClosed, setCalloutClosed] = useState(getStoredCalloutState(storage)); @@ -41,6 +47,13 @@ export const DocumentExplorerCallout = () => { setCalloutClosed(true); }, [storage]); + const semiBoldStyle = useMemo( + () => css` + font-weight: ${euiTheme.font.weight.semiBold}; + `, + [euiTheme.font.weight.semiBold] + ); + if (calloutClosed || !capabilities.advancedSettings.save) { return null; } @@ -54,7 +67,17 @@ export const DocumentExplorerCallout = () => {

+ + + ), + }} />

{ /> - + '', + docLinks: { links: { discover: { documentExplorer: '' } } }, + capabilities: { advancedSettings: { save: true } }, + storage: new LocalStorageMock({ [CALLOUT_STATE_KEY]: false }), +} as unknown as DiscoverServices; + +const mount = (services: DiscoverServices) => { + return mountWithIntl( + + + + ); +}; + +describe('Document Explorer Update callout', () => { + it('should render callout', () => { + const result = mount(defaultServices); + + expect(result.find('.dscDocumentExplorerCallout').exists()).toBeTruthy(); + }); + + it('should not render callout for user without permissions', () => { + const services = { + ...defaultServices, + capabilities: { advancedSettings: { save: false } }, + } as unknown as DiscoverServices; + const result = mount(services); + + expect(result.find('.dscDocumentExplorerCallout').exists()).toBeFalsy(); + }); + + it('should not render callout of it was closed', () => { + const services = { + ...defaultServices, + storage: new LocalStorageMock({ [CALLOUT_STATE_KEY]: true }), + } as unknown as DiscoverServices; + const result = mount(services); + + expect(result.find('.dscDocumentExplorerCallout').exists()).toBeFalsy(); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx new file mode 100644 index 0000000000000..7e45193cba3ee --- /dev/null +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx @@ -0,0 +1,124 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react'; +import './document_explorer_callout.scss'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiButton, + EuiButtonIcon, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { Storage } from '../../../../../../kibana_utils/public'; + +export const CALLOUT_STATE_KEY = 'discover:docExplorerUpdateCalloutClosed'; + +const getStoredCalloutState = (storage: Storage): boolean => { + const calloutClosed = storage.get(CALLOUT_STATE_KEY); + return Boolean(calloutClosed); +}; +const updateStoredCalloutState = (newState: boolean, storage: Storage) => { + storage.set(CALLOUT_STATE_KEY, newState); +}; + +/** + * The callout that's displayed when Document explorer is enabled + */ +export const DocumentExplorerUpdateCallout = () => { + const { euiTheme } = useEuiTheme(); + const { storage, capabilities, docLinks } = useDiscoverServices(); + const [calloutClosed, setCalloutClosed] = useState(getStoredCalloutState(storage)); + + const semiBoldStyle = useMemo( + () => css` + font-weight: ${euiTheme.font.weight.semiBold}; + `, + [euiTheme.font.weight.semiBold] + ); + + const onCloseCallout = useCallback(() => { + updateStoredCalloutState(true, storage); + setCalloutClosed(true); + }, [storage]); + + if (calloutClosed || !capabilities.advancedSettings.save) { + return null; + } + + return ( + } + iconType="search" + > +

+ + + + ), + documentExplorer: ( + + + + ), + }} + /> +

+ + + +
+ ); +}; + +function CalloutTitle({ onCloseCallout }: { onCloseCallout: () => void }) { + return ( + + + + + + + + + ); +} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index b3b1b31186dbd..1c7971fe9a292 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -34,6 +34,7 @@ import { DocTableInfinite } from '../../../../components/doc_table/doc_table_inf import { SortPairArr } from '../../../../components/doc_table/lib/get_sort'; import { ElasticSearchHit } from '../../../../types'; import { DocumentExplorerCallout } from '../document_explorer_callout'; +import { DocumentExplorerUpdateCallout } from '../document_explorer_callout/document_explorer_update_callout'; const DocTableInfiniteMemoized = React.memo(DocTableInfinite); const DataGridMemoized = React.memo(DiscoverGrid); @@ -156,30 +157,33 @@ function DiscoverDocumentsComponent({ )} {!isLegacy && (
- + <> + + +
)}
diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index b3e955c592ad2..94c85e42673ec 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -171,7 +171,7 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record` + i18n.translate('discover.advancedSettings.documentExplorerLinkText', { defaultMessage: 'Document Explorer', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c22beedbb4e27..04ef060b80ffd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2720,7 +2720,6 @@ "discover.doc.loadingDescription": "読み込み中…", "discover.doc.somethingWentWrongDescription": "{indexName}が見つかりません。", "discover.doc.somethingWentWrongDescriptionAddon": "インデックスが存在することを確認してください。", - "discover.docExplorerCallout.bodyMessage": "ドキュメントエクスプローラーでは、データの並べ替え、選択、比較のほか、列のサイズ変更やドキュメントの全画面表示をすばやく実行できます。", "discover.docExplorerCallout.closeButtonAriaLabel": "閉じる", "discover.docExplorerCallout.headerMessage": "より効率的な探索方法", "discover.docExplorerCallout.tryDocumentExplorer": "ドキュメントエクスプローラーを試す", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3b85ad51fdb3d..da1bda187ee80 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2725,7 +2725,6 @@ "discover.doc.loadingDescription": "正在加载……", "discover.doc.somethingWentWrongDescription": "{indexName} 缺失。", "discover.doc.somethingWentWrongDescriptionAddon": "请确保索引存在。", - "discover.docExplorerCallout.bodyMessage": "使用 Document Explorer 快速排序、选择和比较数据,调整列大小并以全屏方式查看文档。", "discover.docExplorerCallout.closeButtonAriaLabel": "关闭", "discover.docExplorerCallout.headerMessage": "更好的浏览方式", "discover.docExplorerCallout.tryDocumentExplorer": "试用 Document Explorer", From 4f54124b77198d3ffc40ca3e22217a0ce0996791 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 30 Mar 2022 09:40:17 +0200 Subject: [PATCH 061/108] [Discover] Implement search source migration (#128609) --- src/plugins/discover/server/plugin.ts | 15 +++- .../discover/server/saved_objects/index.ts | 2 +- .../discover/server/saved_objects/search.ts | 81 ++++++++++--------- .../saved_objects/search_migrations.test.ts | 33 +++++++- .../server/saved_objects/search_migrations.ts | 54 ++++++++++++- 5 files changed, 138 insertions(+), 47 deletions(-) diff --git a/src/plugins/discover/server/plugin.ts b/src/plugins/discover/server/plugin.ts index 879b75986365b..00888d31f8d9b 100644 --- a/src/plugins/discover/server/plugin.ts +++ b/src/plugins/discover/server/plugin.ts @@ -9,13 +9,22 @@ import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; import { getUiSettings } from './ui_settings'; import { capabilitiesProvider } from './capabilities_provider'; -import { searchSavedObjectType } from './saved_objects'; +import { getSavedSearchObjectType } from './saved_objects'; +import type { PluginSetup as DataPluginSetup } from '../../data/server'; export class DiscoverServerPlugin implements Plugin { - public setup(core: CoreSetup) { + public setup( + core: CoreSetup, + plugins: { + data: DataPluginSetup; + } + ) { + const getSearchSourceMigrations = plugins.data.search.searchSource.getAllMigrations.bind( + plugins.data.search.searchSource + ); core.capabilities.registerProvider(capabilitiesProvider); core.uiSettings.register(getUiSettings(core.docLinks)); - core.savedObjects.registerType(searchSavedObjectType); + core.savedObjects.registerType(getSavedSearchObjectType(getSearchSourceMigrations)); return {}; } diff --git a/src/plugins/discover/server/saved_objects/index.ts b/src/plugins/discover/server/saved_objects/index.ts index 27bb6eead7f61..fdb078850cf77 100644 --- a/src/plugins/discover/server/saved_objects/index.ts +++ b/src/plugins/discover/server/saved_objects/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { searchSavedObjectType } from './search'; +export { getSavedSearchObjectType } from './search'; diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index d5b0a3e09bc61..796ab164cf0af 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -7,46 +7,51 @@ */ import { SavedObjectsType } from 'kibana/server'; -import { searchMigrations } from './search_migrations'; +import { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common'; +import { getAllMigrations } from './search_migrations'; -export const searchSavedObjectType: SavedObjectsType = { - name: 'search', - hidden: false, - namespaceType: 'multiple-isolated', - convertToMultiNamespaceTypeVersion: '8.0.0', - management: { - icon: 'discoverApp', - defaultSearchField: 'title', - importableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getInAppUrl(obj) { - return { - path: `/app/discover#/view/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'discover.show', - }; +export function getSavedSearchObjectType( + getSearchSourceMigrations: () => MigrateFunctionsObject +): SavedObjectsType { + return { + name: 'search', + hidden: false, + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', + management: { + icon: 'discoverApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/discover#/view/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'discover.show', + }; + }, }, - }, - mappings: { - properties: { - columns: { type: 'keyword', index: false, doc_values: false }, - description: { type: 'text' }, - viewMode: { type: 'keyword', index: false, doc_values: false }, - hideChart: { type: 'boolean', index: false, doc_values: false }, - hideAggregatedPreview: { type: 'boolean', index: false, doc_values: false }, - hits: { type: 'integer', index: false, doc_values: false }, - kibanaSavedObjectMeta: { - properties: { - searchSourceJSON: { type: 'text', index: false }, + mappings: { + properties: { + columns: { type: 'keyword', index: false, doc_values: false }, + description: { type: 'text' }, + viewMode: { type: 'keyword', index: false, doc_values: false }, + hideChart: { type: 'boolean', index: false, doc_values: false }, + hideAggregatedPreview: { type: 'boolean', index: false, doc_values: false }, + hits: { type: 'integer', index: false, doc_values: false }, + kibanaSavedObjectMeta: { + properties: { + searchSourceJSON: { type: 'text', index: false }, + }, }, + sort: { type: 'keyword', index: false, doc_values: false }, + title: { type: 'text' }, + grid: { type: 'object', enabled: false }, + version: { type: 'integer' }, + rowHeight: { type: 'text' }, }, - sort: { type: 'keyword', index: false, doc_values: false }, - title: { type: 'text' }, - grid: { type: 'object', enabled: false }, - version: { type: 'integer' }, - rowHeight: { type: 'text' }, }, - }, - migrations: searchMigrations, -}; + migrations: () => getAllMigrations(getSearchSourceMigrations()), + }; +} diff --git a/src/plugins/discover/server/saved_objects/search_migrations.test.ts b/src/plugins/discover/server/saved_objects/search_migrations.test.ts index 122371642fabd..9e4c23c91976c 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.test.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { SavedObjectMigrationContext } from 'kibana/server'; -import { searchMigrations } from './search_migrations'; +import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { getAllMigrations, searchMigrations } from './search_migrations'; const savedObjectMigrationContext = null as unknown as SavedObjectMigrationContext; @@ -350,4 +350,33 @@ Object { testMigrateMatchAllQuery(migrationFn); }); }); + it('should apply search source migrations within saved search', () => { + const savedSearch = { + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + some: 'prop', + migrated: false, + }), + }, + }, + } as SavedObjectUnsanitizedDoc; + + const versionToTest = '9.1.1'; + const migrations = getAllMigrations({ + // providing a function for search source migration that's just setting `migrated` to true + [versionToTest]: (state) => ({ ...state, migrated: true }), + }); + + expect(migrations[versionToTest](savedSearch, {} as SavedObjectMigrationContext)).toEqual({ + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + some: 'prop', + migrated: true, + }), + }, + }, + }); + }); }); diff --git a/src/plugins/discover/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts index 5d630f782fb78..cb8ced07387a1 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -8,10 +8,22 @@ // TODO: This needs to be removed and properly typed /* eslint-disable @typescript-eslint/no-explicit-any */ - -import { flow, get } from 'lodash'; -import { SavedObjectMigrationFn } from 'kibana/server'; +import { flow, get, mapValues } from 'lodash'; +import type { + SavedObjectAttributes, + SavedObjectMigrationFn, + SavedObjectMigrationMap, +} from 'kibana/server'; +import { mergeSavedObjectMigrationMaps } from '../../../../core/server'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/server'; +import { MigrateFunctionsObject, MigrateFunction } from '../../../kibana_utils/common'; +import type { SerializedSearchSourceFields } from '../../../data/common'; + +export interface SavedSearchMigrationAttributes extends SavedObjectAttributes { + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; +} /** * This migration script is related to: @@ -120,9 +132,45 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = }; }; +/** + * This creates a migration map that applies search source migrations + */ +const getSearchSourceMigrations = (searchSourceMigrations: MigrateFunctionsObject) => + mapValues( + searchSourceMigrations, + (migrate: MigrateFunction): MigrateFunction => + (state) => { + const _state = state as unknown as { attributes: SavedSearchMigrationAttributes }; + + const parsedSearchSourceJSON = _state.attributes.kibanaSavedObjectMeta.searchSourceJSON; + + if (!parsedSearchSourceJSON) return _state; + + return { + ..._state, + attributes: { + ..._state.attributes, + kibanaSavedObjectMeta: { + ..._state.attributes.kibanaSavedObjectMeta, + searchSourceJSON: JSON.stringify(migrate(JSON.parse(parsedSearchSourceJSON))), + }, + }, + }; + } + ); + export const searchMigrations = { '6.7.2': flow(migrateMatchAllQuery), '7.0.0': flow(setNewReferences), '7.4.0': flow(migrateSearchSortToNestedArray), '7.9.3': flow(migrateMatchAllQuery), }; + +export const getAllMigrations = ( + searchSourceMigrations: MigrateFunctionsObject +): SavedObjectMigrationMap => { + return mergeSavedObjectMigrationMaps( + searchMigrations, + getSearchSourceMigrations(searchSourceMigrations) as unknown as SavedObjectMigrationMap + ); +}; From 349b6cf5d2d664703f46a217d0f298524ae3042a Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Wed, 30 Mar 2022 14:19:52 +0500 Subject: [PATCH 062/108] [Discover] Fix toggle table column for classic table (#128603) * Fx toggle table column * Add functional test Co-authored-by: Matthias Wilhelm --- .../document_explorer_callout.tsx | 1 + .../doc_table/doc_table_wrapper.tsx | 5 ++-- test/functional/apps/discover/_doc_table.ts | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx index 9b35b52dbc87f..efae6bcc51db5 100644 --- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx @@ -125,6 +125,7 @@ function CalloutTitle({ onCloseCallout }: { onCloseCallout: () => void }) { aria-label={i18n.translate('discover.docExplorerCallout.closeButtonAriaLabel', { defaultMessage: 'Close', })} + data-test-subj="dscExplorerCalloutClose" onClick={onCloseCallout} type="button" iconType="cross" diff --git a/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx index b208331601330..334c825d78de0 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx @@ -164,12 +164,13 @@ export const DocTableWrapper = forwardRef( indexPattern={indexPattern} row={current} useNewFieldsApi={useNewFieldsApi} - onAddColumn={onAddColumn} fieldsToShow={fieldsToShow} + onAddColumn={onAddColumn} + onRemoveColumn={onRemoveColumn} /> )); }, - [columns, onFilter, indexPattern, useNewFieldsApi, onAddColumn, fieldsToShow] + [columns, onFilter, indexPattern, useNewFieldsApi, fieldsToShow, onAddColumn, onRemoveColumn] ); return ( diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 794204b923b72..321c41b92e9be 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -189,6 +189,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(defaultMessageElResubmit).to.be.ok(); }); }); + it('should show allow toggling columns from the expanded document', async function () { + await PageObjects.discover.clickNewSearchButton(); + await testSubjects.click('dscExplorerCalloutClose'); + await retry.try(async function () { + await docTable.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 }); + + // add columns + const fields = ['_id', '_index', 'agent']; + for (const field of fields) { + await testSubjects.click(`toggleColumnButton-${field}`); + } + + const headerWithFields = await docTable.getHeaderFields(); + expect(headerWithFields.join(' ')).to.contain(fields.join(' ')); + + // remove columns + for (const field of fields) { + await testSubjects.click(`toggleColumnButton-${field}`); + } + + const headerWithoutFields = await docTable.getHeaderFields(); + expect(headerWithoutFields.join(' ')).not.to.contain(fields.join(' ')); + }); + }); }); describe('add and remove columns', function () { From 6304e4a54f4c09e198165c5b581bafa7533124c4 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 30 Mar 2022 11:39:19 +0200 Subject: [PATCH 063/108] improve check for 0 opacity (#128630) --- .../public/application/components/vis_types/timeseries/vis.js | 4 ++-- .../vis_types/timeseries/public/trigger_action/get_extents.ts | 4 ++-- .../vis_types/timeseries/public/trigger_action/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js index 8ea5a8594bf19..0fb4e99b1e973 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js @@ -85,8 +85,8 @@ class TimeseriesVisualization extends Component { const axisMin = get(model, 'axis_min', '').toString(); const axisMax = get(model, 'axis_max', '').toString(); const fit = model.series - ? model.series.filter(({ hidden }) => !hidden).every(({ fill }) => fill === '0') - : model.fill === '0'; + ? model.series.filter(({ hidden }) => !hidden).every(({ fill }) => Number(fill) === 0) + : Number(model.fill) === 0; return { min: axisMin.length ? Number(axisMin) : undefined, diff --git a/src/plugins/vis_types/timeseries/public/trigger_action/get_extents.ts b/src/plugins/vis_types/timeseries/public/trigger_action/get_extents.ts index 857de8390a6a3..a0587671c7686 100644 --- a/src/plugins/vis_types/timeseries/public/trigger_action/get_extents.ts +++ b/src/plugins/vis_types/timeseries/public/trigger_action/get_extents.ts @@ -57,7 +57,7 @@ export const getYExtents = (model: Panel) => { model.series.forEach((s) => { if (s.axis_position === 'left') { - if (s.chart_type !== 'line' || (s.chart_type === 'line' && s.fill !== '0')) { + if (s.chart_type !== 'line' || (s.chart_type === 'line' && Number(s.fill) > 0)) { hasBarOrAreaLeft = true; } if (s.separate_axis) { @@ -68,7 +68,7 @@ export const getYExtents = (model: Panel) => { } } if (s.axis_position === 'right' && s.separate_axis) { - if (s.chart_type !== 'line' || (s.chart_type === 'line' && s.fill !== '0')) { + if (s.chart_type !== 'line' || (s.chart_type === 'line' && Number(s.fill) > 0)) { hasBarOrAreaRight = true; } if (s.separate_axis) { diff --git a/src/plugins/vis_types/timeseries/public/trigger_action/index.ts b/src/plugins/vis_types/timeseries/public/trigger_action/index.ts index 0df0ac55e35eb..15fb1aec2974c 100644 --- a/src/plugins/vis_types/timeseries/public/trigger_action/index.ts +++ b/src/plugins/vis_types/timeseries/public/trigger_action/index.ts @@ -58,7 +58,7 @@ export const triggerTSVBtoLensConfiguration = async ( const timeShift = layer.offset_time; // translate to Lens seriesType const layerChartType = - layer.chart_type === 'line' && layer.fill !== '0' ? 'area' : layer.chart_type; + layer.chart_type === 'line' && Number(layer.fill) > 0 ? 'area' : layer.chart_type; let chartType = layerChartType; if (layer.stacked !== 'none' && layer.stacked !== 'percent') { From 84bd77ca443f082758c6d819dd915c2fe53f9e23 Mon Sep 17 00:00:00 2001 From: "Lucas F. da Costa" Date: Wed, 30 Mar 2022 10:45:37 +0100 Subject: [PATCH 064/108] [Monitor management] Show public beta fair usage (#128857) * show public beta fair usage * type * add callout for async service errors * remove unused variables * Re-add "Revert "[Monitor management] Show public beta fair usage (#128770)" with types fixed Co-authored-by: shahzad31 Co-authored-by: Dominique Clarke --- .../monitor_management/locations.ts | 15 ++- .../monitor_management/monitor_types.ts | 27 ++-- .../action_bar/action_bar.tsx | 40 +----- .../monitor_list/monitor_async_error.test.tsx | 117 ++++++++++++++++++ .../monitor_list/monitor_async_error.tsx | 75 +++++++++++ .../monitor_list/monitor_list_container.tsx | 2 + .../monitor_management/show_sync_errors.tsx | 52 ++++++++ .../monitor_management/monitor_management.tsx | 4 +- .../state/reducers/monitor_management.ts | 1 + .../synthetics_service/synthetics_service.ts | 7 +- .../synthetics_service/get_monitor.ts | 3 +- 11 files changed, 286 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/show_sync_errors.tsx diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts index d11ae7c655405..82d2bc8afa412 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts @@ -64,12 +64,15 @@ export const ServiceLocationErrors = t.array( status: t.number, }), t.partial({ - failed_monitors: t.array( - t.interface({ - id: t.string, - message: t.string, - }) - ), + failed_monitors: t.union([ + t.array( + t.interface({ + id: t.string, + message: t.string, + }) + ), + t.null, + ]), }), ]), }) diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts index 44c643d2160d1..872ccdbb71ec8 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { secretKeys } from '../../constants/monitor_management'; import { ConfigKey } from './config_key'; -import { LocationsCodec } from './locations'; +import { LocationsCodec, ServiceLocationErrors } from './locations'; import { DataStreamCodec, ModeCodec, @@ -306,14 +306,23 @@ export type EncryptedSyntheticsMonitorWithId = t.TypeOf< typeof EncryptedSyntheticsMonitorWithIdCodec >; -export const MonitorManagementListResultCodec = t.type({ - monitors: t.array( - t.interface({ id: t.string, attributes: EncryptedSyntheticsMonitorCodec, updated_at: t.string }) - ), - page: t.number, - perPage: t.number, - total: t.union([t.number, t.null]), -}); +export const MonitorManagementListResultCodec = t.intersection([ + t.type({ + monitors: t.array( + t.interface({ + id: t.string, + attributes: EncryptedSyntheticsMonitorCodec, + updated_at: t.string, + }) + ), + page: t.number, + perPage: t.number, + total: t.union([t.number, t.null]), + }), + t.partial({ + syncErrors: ServiceLocationErrors, + }), +]); export type MonitorManagementListResult = t.TypeOf; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx index 3b30458974ed7..5f6e67e363171 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx @@ -19,7 +19,6 @@ import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; -import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { MONITOR_MANAGEMENT_ROUTE } from '../../../../common/constants'; import { UptimeSettingsContext } from '../../../contexts'; @@ -32,6 +31,7 @@ import { TestRun } from '../test_now_mode/test_now_mode'; import { monitorManagementListSelector } from '../../../state/selectors'; import { kibanaService } from '../../../state/kibana_service'; +import { showSyncErrors } from '../show_sync_errors'; export interface ActionBarProps { monitor: SyntheticsMonitor; @@ -103,43 +103,7 @@ export const ActionBar = ({ }); setIsSuccessful(true); } else if (hasErrors && !loading) { - Object.values(data.attributes.errors!).forEach((location) => { - const { status: responseStatus, reason } = location.error || {}; - kibanaService.toasts.addWarning({ - title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { - defaultMessage: `Unable to sync monitor config`, - }), - text: toMountPoint( - <> -

- {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { - defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, - values: { - location: locations?.find((loc) => loc?.id === location.locationId)?.label, - }, - })} -

- {responseStatus || reason ? ( -

- {responseStatus - ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { - defaultMessage: 'Status: {status}. ', - values: { status: responseStatus }, - }) - : null} - {reason - ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { - defaultMessage: 'Reason: {reason}.', - values: { reason }, - }) - : null} -

- ) : null} - - ), - toastLifeTimeMs: 30000, - }); - }); + showSyncErrors(data.attributes.errors, locations); setIsSuccessful(true); } }, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx new file mode 100644 index 0000000000000..6085c7f8b419d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.test.tsx @@ -0,0 +1,117 @@ +/* + * 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 { screen } from '@testing-library/react'; +import React from 'react'; +import { DEFAULT_THROTTLING } from '../../../../common/runtime_types'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; +import { MonitorAsyncError } from './monitor_async_error'; + +describe('', () => { + const location1 = 'US Central'; + const location2 = 'US North'; + const reason1 = 'Unauthorized'; + const reason2 = 'Forbidden'; + const status1 = 401; + const status2 = 403; + const state = { + monitorManagementList: { + throttling: DEFAULT_THROTTLING, + enablement: null, + list: { + perPage: 5, + page: 1, + total: 6, + monitors: [], + syncErrors: [ + { + locationId: 'us_central', + error: { + reason: reason1, + status: status1, + }, + }, + { + locationId: 'us_north', + error: { + reason: reason2, + status: status2, + }, + }, + ], + }, + locations: [ + { + id: 'us_central', + label: location1, + geo: { + lat: 0, + lon: 0, + }, + url: '', + }, + { + id: 'us_north', + label: location2, + geo: { + lat: 0, + lon: 0, + }, + url: '', + }, + ], + error: { + serviceLocations: null, + monitorList: null, + enablement: null, + }, + loading: { + monitorList: true, + serviceLocations: false, + enablement: false, + }, + syntheticsService: { + loading: false, + signupUrl: null, + }, + } as MonitorManagementListState, + }; + + it('renders when errors are defined', () => { + render(, { state }); + + expect(screen.getByText(new RegExp(reason1))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(`${status1}`))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(reason2))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(`${status2}`))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(location1))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(location2))).toBeInTheDocument(); + }); + + it('renders null when errors are empty', () => { + render(, { + state: { + ...state, + monitorManagementList: { + ...state.monitorManagementList, + list: { + ...state.monitorManagementList.list, + syncErrors: [], + }, + }, + }, + }); + + expect(screen.queryByText(new RegExp(reason1))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(`${status1}`))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(reason2))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(`${status2}`))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(location1))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(location2))).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx new file mode 100644 index 0000000000000..c9e9dba2027a4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_async_error.tsx @@ -0,0 +1,75 @@ +/* + * 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, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { monitorManagementListSelector } from '../../../state/selectors'; + +export const MonitorAsyncError = () => { + const [isDismissed, setIsDismissed] = useState(false); + const { list, locations } = useSelector(monitorManagementListSelector); + const syncErrors = list.syncErrors; + const hasSyncErrors = syncErrors && syncErrors.length > 0; + + return hasSyncErrors && !isDismissed ? ( + <> + + } + color="warning" + iconType="alert" + > +

+ +

+
    + {Object.values(syncErrors).map((e) => { + return ( +
  • {`${ + locations.find((location) => location.id === e.locationId)?.label + } - ${STATUS_LABEL}: ${e.error.status}; ${REASON_LABEL}: ${e.error.reason}.`}
  • + ); + })} +
+ setIsDismissed(true)} color="warning"> + {DISMISS_LABEL} + +
+ + + ) : null; +}; + +const REASON_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.monitorSync.failure.reasonLabel', + { + defaultMessage: 'Reason', + } +); + +const STATUS_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.monitorSync.failure.statusLabel', + { + defaultMessage: 'Status', + } +); + +const DISMISS_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.monitorSync.failure.dismissLabel', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx index a3f041a33a9f8..53afdf49c1592 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list_container.tsx @@ -13,6 +13,7 @@ import { ConfigKey } from '../../../../common/runtime_types'; import { getMonitors } from '../../../state/actions'; import { monitorManagementListSelector } from '../../../state/selectors'; import { MonitorManagementListPageState } from './monitor_list'; +import { MonitorAsyncError } from './monitor_async_error'; import { useInlineErrors } from '../hooks/use_inline_errors'; import { MonitorListTabs } from './list_tabs'; import { AllMonitors } from './all_monitors'; @@ -66,6 +67,7 @@ export const MonitorListContainer: React.FC = () => { return ( <> + { + Object.values(errors).forEach((location) => { + const { status: responseStatus, reason } = location.error || {}; + kibanaService.toasts.addWarning({ + title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { + defaultMessage: `Unable to sync monitor config`, + }), + text: toMountPoint( + <> +

+ {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { + defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, + values: { + location: locations?.find((loc) => loc?.id === location.locationId)?.label, + }, + })} +

+ {responseStatus || reason ? ( +

+ {responseStatus + ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { + defaultMessage: 'Status: {status}. ', + values: { status: responseStatus }, + }) + : null} + {reason + ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { + defaultMessage: 'Reason: {reason}.', + values: { reason }, + }) + : null} +

+ ) : null} + + ), + toastLifeTimeMs: 30000, + }); + }); +}; diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx index 3e0e9b955f31f..71785dbaf78ee 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx @@ -17,6 +17,7 @@ import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadc import { MonitorListContainer } from '../../components/monitor_management/monitor_list/monitor_list_container'; import { EnablementEmptyState } from '../../components/monitor_management/monitor_list/enablement_empty_state'; import { useEnablement } from '../../components/monitor_management/hooks/use_enablement'; +import { useLocations } from '../../components/monitor_management/hooks/use_locations'; import { Loader } from '../../components/monitor_management/loader/loader'; export const MonitorManagementPage: React.FC = () => { @@ -32,6 +33,7 @@ export const MonitorManagementPage: React.FC = () => { loading: enablementLoading, enableSynthetics, } = useEnablement(); + const { loading: locationsLoading } = useLocations(); const { list: monitorList } = useSelector(monitorManagementListSelector); const { isEnabled } = enablement; @@ -62,7 +64,7 @@ export const MonitorManagementPage: React.FC = () => { return ( <> ({ search: schema.maybe(schema.string()), }), }, - handler: async ({ request, savedObjectsClient }): Promise => { + handler: async ({ request, savedObjectsClient, server }): Promise => { const { perPage = 50, page, sortField, sortOrder, search } = request.query; // TODO: add query/filtering params const { @@ -78,6 +78,7 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ ...rest, perPage: perPageT, monitors, + syncErrors: server.syntheticsService.syncErrors, }; }, }); From 3a65e8b98496ed66d5775556e6159b0e5665ce26 Mon Sep 17 00:00:00 2001 From: CohenIdo <90558359+CohenIdo@users.noreply.github.com> Date: Wed, 30 Mar 2022 14:00:37 +0300 Subject: [PATCH 065/108] [Cloud Security] Update transform indices naming (#128781) --- .../common/constants.ts | 12 ++++--- .../create_transforms_indices.ts | 35 +++++++++++++++---- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index ca8148bacb623..be4ba273c5408 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -10,13 +10,15 @@ export const FINDINGS_ROUTE_PATH = '/api/csp/findings'; export const BENCHMARKS_ROUTE_PATH = '/api/csp/benchmarks'; export const UPDATE_RULES_CONFIG_ROUTE_PATH = '/api/csp/update_rules_config'; -export const CSP_KUBEBEAT_INDEX_PATTERN = 'logs-cis_kubernetes_benchmark.findings*'; -export const AGENT_LOGS_INDEX_PATTERN = '.logs-cis_kubernetes_benchmark.metadata*'; -export const LATEST_FINDINGS_INDEX_PATTERN = 'cloud_security_posture-findings_latest'; -export const BENCHMARK_SCORE_INDEX_PATTERN = 'cloud_security_posture-benchmark_scores'; - export const CSP_FINDINGS_INDEX_NAME = 'findings'; export const CIS_KUBERNETES_PACKAGE_NAME = 'cis_kubernetes_benchmark'; +export const LATEST_FINDINGS_INDEX_NAME = 'cloud_security_posture.findings_latest'; +export const BENCHMARK_SCORE_INDEX_NAME = 'cloud_security_posture.scores'; + +export const AGENT_LOGS_INDEX_PATTERN = '.logs-cis_kubernetes_benchmark.metadata*'; +export const CSP_KUBEBEAT_INDEX_PATTERN = 'logs-cis_kubernetes_benchmark.findings*'; +export const LATEST_FINDINGS_INDEX_PATTERN = 'logs-' + LATEST_FINDINGS_INDEX_NAME + '-default'; +export const BENCHMARK_SCORE_INDEX_PATTERN = 'logs-' + BENCHMARK_SCORE_INDEX_NAME + '-default'; export const RULE_PASSED = `passed`; export const RULE_FAILED = `failed`; diff --git a/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts b/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts index 892cb78145c61..f98122bf28bc7 100644 --- a/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts +++ b/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts @@ -11,7 +11,9 @@ import { benchmarkScoreMapping } from './benchmark_score_mapping'; import { latestFindingsMapping } from './latest_findings_mapping'; import { LATEST_FINDINGS_INDEX_PATTERN, + LATEST_FINDINGS_INDEX_NAME, BENCHMARK_SCORE_INDEX_PATTERN, + BENCHMARK_SCORE_INDEX_NAME, } from '../../common/constants'; // TODO: Add integration tests @@ -19,25 +21,44 @@ export const initializeCspTransformsIndices = async ( esClient: ElasticsearchClient, logger: Logger ) => { - createIndexIfNotExists(esClient, LATEST_FINDINGS_INDEX_PATTERN, latestFindingsMapping, logger); - createIndexIfNotExists(esClient, BENCHMARK_SCORE_INDEX_PATTERN, benchmarkScoreMapping, logger); + createIndexIfNotExists( + esClient, + LATEST_FINDINGS_INDEX_NAME, + LATEST_FINDINGS_INDEX_PATTERN, + latestFindingsMapping, + logger + ); + createIndexIfNotExists( + esClient, + BENCHMARK_SCORE_INDEX_NAME, + BENCHMARK_SCORE_INDEX_PATTERN, + benchmarkScoreMapping, + logger + ); }; export const createIndexIfNotExists = async ( esClient: ElasticsearchClient, - index: string, - mapping: MappingTypeMapping, + indexName: string, + indexPattern: string, + mappings: MappingTypeMapping, logger: Logger ) => { try { const isLatestIndexExists = await esClient.indices.exists({ - index, + index: indexPattern, }); if (!isLatestIndexExists) { + await esClient.indices.putIndexTemplate({ + name: indexName, + index_patterns: indexPattern, + template: { mappings }, + priority: 500, + }); await esClient.indices.create({ - index, - mappings: mapping, + index: indexPattern, + mappings, }); } } catch (err) { From 3dd27be411eb1430fd0fa42c887a8af6e0904539 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 30 Mar 2022 14:28:15 +0300 Subject: [PATCH 066/108] [Lens] Fixed terms multifields flakiness (#128862) --- x-pack/test/functional/page_objects/lens_page.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 5a95b195fb0c0..0825d8355466e 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -596,7 +596,13 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const lastIndex = ( await find.allByCssSelector('[data-test-subj^="indexPattern-dimension-field"]') ).length; - await testSubjects.click('indexPattern-terms-add-field'); + await retry.waitFor('check for field combobox existance', async () => { + await testSubjects.click('indexPattern-terms-add-field'); + const comboboxExists = await testSubjects.exists( + `indexPattern-dimension-field-${lastIndex}` + ); + return comboboxExists === true; + }); // count the number of defined terms const target = await testSubjects.find(`indexPattern-dimension-field-${lastIndex}`); // await comboBox.openOptionsList(target); From d869a7fc8106dfa40ba158c57a7f7ebfc6f34b75 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Wed, 30 Mar 2022 13:37:26 +0200 Subject: [PATCH 067/108] [Ingest Pipelines] Remove `axios` dependency in tests (#128467) * Refactor main cits * commit using @elastic.co * Finish refactoring pipeline_editor cits * Carefully access prop * Fix hardcoded props * Fix ts issues * Add back missing attr * Address CR changes --- .../helpers/http_requests.ts | 119 +++++++++--------- .../helpers/pipelines_clone.helpers.ts | 9 +- .../helpers/pipelines_create.helpers.ts | 9 +- .../pipelines_create_from_csv.helpers.ts | 11 +- .../helpers/pipelines_edit.helpers.ts | 6 +- .../helpers/pipelines_list.helpers.ts | 6 +- .../helpers/setup_environment.tsx | 21 ++-- .../ingest_pipelines_clone.test.tsx | 30 +++-- .../ingest_pipelines_create.test.tsx | 38 +++--- .../ingest_pipelines_create_from_csv.test.tsx | 34 +++-- .../ingest_pipelines_edit.test.tsx | 31 +++-- .../ingest_pipelines_list.test.ts | 31 ++--- .../__jest__/http_requests.helpers.ts | 78 +++++++----- .../pipeline_processors_editor.helpers.tsx | 2 +- .../__jest__/processors/processor.helpers.tsx | 5 +- .../__jest__/test_pipeline.helpers.tsx | 27 ++-- .../__jest__/test_pipeline.test.tsx | 54 ++++---- 17 files changed, 259 insertions(+), 252 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts index e5c0e0a5e3673..7029e47b29229 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts @@ -5,63 +5,73 @@ * 2.0. */ -import sinon, { SinonFakeServer } from 'sinon'; - +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; import { API_BASE_PATH } from '../../../common/constants'; -// Register helpers to mock HTTP Requests -const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setLoadPipelinesResponse = (response?: any[], error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? error.body : response; +type HttpMethod = 'GET' | 'PUT' | 'DELETE' | 'POST'; +export interface ResponseError { + statusCode: number; + message: string | Error; + attributes?: Record; +} - server.respondWith('GET', API_BASE_PATH, [ - status, - { 'Content-Type': 'application/json' }, - JSON.stringify(body), - ]); +// Register helpers to mock HTTP Requests +const registerHttpRequestMockHelpers = ( + httpSetup: ReturnType +) => { + const mockResponses = new Map>>( + ['GET', 'PUT', 'DELETE', 'POST'].map( + (method) => [method, new Map()] as [HttpMethod, Map>] + ) + ); + + const mockMethodImplementation = (method: HttpMethod, path: string) => + mockResponses.get(method)?.get(path) ?? Promise.resolve({}); + + httpSetup.get.mockImplementation((path) => + mockMethodImplementation('GET', path as unknown as string) + ); + httpSetup.delete.mockImplementation((path) => + mockMethodImplementation('DELETE', path as unknown as string) + ); + httpSetup.post.mockImplementation((path) => + mockMethodImplementation('POST', path as unknown as string) + ); + httpSetup.put.mockImplementation((path) => + mockMethodImplementation('PUT', path as unknown as string) + ); + + const mockResponse = (method: HttpMethod, path: string, response?: unknown, error?: unknown) => { + const defuse = (promise: Promise) => { + promise.catch(() => {}); + return promise; + }; + + return mockResponses + .get(method)! + .set(path, error ? defuse(Promise.reject({ body: error })) : Promise.resolve(response)); }; - const setLoadPipelineResponse = (response?: {}, error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? error.body : response; + const setLoadPipelinesResponse = (response?: object[], error?: ResponseError) => + mockResponse('GET', API_BASE_PATH, response, error); - server.respondWith('GET', `${API_BASE_PATH}/:name`, [ - status, - { 'Content-Type': 'application/json' }, - JSON.stringify(body), - ]); - }; + const setLoadPipelineResponse = ( + pipelineName: string, + response?: object, + error?: ResponseError + ) => mockResponse('GET', `${API_BASE_PATH}/${pipelineName}`, response, error); - const setDeletePipelineResponse = (response?: object) => { - server.respondWith('DELETE', `${API_BASE_PATH}/:name`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; + const setDeletePipelineResponse = ( + pipelineName: string, + response?: object, + error?: ResponseError + ) => mockResponse('DELETE', `${API_BASE_PATH}/${pipelineName}`, response, error); - const setCreatePipelineResponse = (response?: object, error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + const setCreatePipelineResponse = (response?: object, error?: ResponseError) => + mockResponse('POST', API_BASE_PATH, response, error); - server.respondWith('POST', API_BASE_PATH, [ - status, - { 'Content-Type': 'application/json' }, - body, - ]); - }; - - const setParseCsvResponse = (response?: object, error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - - server.respondWith('POST', `${API_BASE_PATH}/parse_csv`, [ - status, - { 'Content-Type': 'application/json' }, - body, - ]); - }; + const setParseCsvResponse = (response?: object, error?: ResponseError) => + mockResponse('POST', `${API_BASE_PATH}/parse_csv`, response, error); return { setLoadPipelinesResponse, @@ -73,18 +83,11 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; export const init = () => { - const server = sinon.fakeServer.create(); - server.respondImmediately = true; - - // Define default response for unhandled requests. - // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, - // and we can mock them all with a 200 instead of mocking each one individually. - server.respondWith([200, {}, 'DefaultMockedResponse']); - - const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + const httpSetup = httpServiceMock.createSetupContract(); + const httpRequestsMockHelpers = registerHttpRequestMockHelpers(httpSetup); return { - server, + httpSetup, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts index 5b5d6704e9001..6091dd0ef9587 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts @@ -6,6 +6,7 @@ */ import { registerTestBed, AsyncTestBedConfig, TestBed } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; import { PipelinesClone } from '../../../public/application/sections/pipelines_clone'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; @@ -36,9 +37,11 @@ const testBedConfig: AsyncTestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WithAppDependencies(PipelinesClone), testBedConfig); - -export const setup = async (): Promise => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed( + WithAppDependencies(PipelinesClone, httpSetup), + testBedConfig + ); const testBed = await initTestBed(); return { diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts index 3dc97cf121b98..7394552494f3c 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts @@ -6,6 +6,7 @@ */ import { registerTestBed, AsyncTestBedConfig, TestBed } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; import { PipelinesCreate } from '../../../public/application/sections/pipelines_create'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; @@ -23,9 +24,11 @@ const testBedConfig: AsyncTestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WithAppDependencies(PipelinesCreate), testBedConfig); - -export const setup = async (): Promise => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed( + WithAppDependencies(PipelinesCreate, httpSetup), + testBedConfig + ); const testBed = await initTestBed(); return { diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create_from_csv.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create_from_csv.helpers.ts index ea9d623e216b2..3f68b174f3c4f 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create_from_csv.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create_from_csv.helpers.ts @@ -6,8 +6,9 @@ */ import { act } from 'react-dom/test-utils'; - +import { HttpSetup } from 'src/core/public'; import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test-jest-helpers'; + import { PipelinesCreateFromCsv } from '../../../public/application/sections/pipelines_create_from_csv'; import { WithAppDependencies } from './setup_environment'; import { getCreateFromCsvPath, ROUTES } from '../../../public/application/services/navigation'; @@ -20,8 +21,6 @@ const testBedConfig: AsyncTestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WithAppDependencies(PipelinesCreateFromCsv), testBedConfig); - export type PipelineCreateFromCsvTestBed = TestBed & { actions: ReturnType; }; @@ -59,7 +58,11 @@ const createFromCsvActions = (testBed: TestBed) => { }; }; -export const setup = async (): Promise => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed( + WithAppDependencies(PipelinesCreateFromCsv, httpSetup), + testBedConfig + ); const testBed = await initTestBed(); return { diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts index 74d124de885ff..1902e5c1f2aed 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts @@ -6,6 +6,7 @@ */ import { registerTestBed, AsyncTestBedConfig, TestBed } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; import { PipelinesEdit } from '../../../public/application/sections/pipelines_edit'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; @@ -36,9 +37,8 @@ const testBedConfig: AsyncTestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WithAppDependencies(PipelinesEdit), testBedConfig); - -export const setup = async (): Promise => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed(WithAppDependencies(PipelinesEdit, httpSetup), testBedConfig); const testBed = await initTestBed(); return { diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts index 6fa3a7a9473fe..33d3fb31ef81f 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts @@ -6,6 +6,7 @@ */ import { act } from 'react-dom/test-utils'; +import { HttpSetup } from 'src/core/public'; import { registerTestBed, @@ -25,8 +26,6 @@ const testBedConfig: AsyncTestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WithAppDependencies(PipelinesList), testBedConfig); - export type PipelineListTestBed = TestBed & { actions: ReturnType; }; @@ -89,7 +88,8 @@ const createActions = (testBed: TestBed) => { }; }; -export const setup = async (): Promise => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed(WithAppDependencies(PipelinesList, httpSetup), testBedConfig); const testBed = await initTestBed(); return { diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx index 96a0f9e23348a..7b7a467d59a91 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx @@ -6,8 +6,6 @@ */ import React from 'react'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { LocationDescriptorObject } from 'history'; import { HttpSetup } from 'kibana/public'; @@ -34,8 +32,6 @@ import { import { init as initHttpRequests } from './http_requests'; -const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); - const history = scopedHistoryMock.create(); history.createHref.mockImplementation((location: LocationDescriptorObject) => { return `${location.pathname}?${location.search}`; @@ -73,22 +69,19 @@ const appServices = { }; export const setupEnvironment = () => { - uiMetricService.setup(usageCollectionPluginMock.createSetupContract()); - apiService.setup(mockHttpClient as unknown as HttpSetup, uiMetricService); documentationService.setup(docLinksServiceMock.createStartContract()); breadcrumbService.setup(() => {}); - const { server, httpRequestsMockHelpers } = initHttpRequests(); - - return { - server, - httpRequestsMockHelpers, - }; + return initHttpRequests(); }; -export const WithAppDependencies = (Comp: any) => (props: any) => - ( +export const WithAppDependencies = (Comp: any, httpSetup: HttpSetup) => (props: any) => { + uiMetricService.setup(usageCollectionPluginMock.createSetupContract()); + apiService.setup(httpSetup, uiMetricService); + + return ( ); +}; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx index 556cea9eb5f80..8d7ed011b60cd 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers } from './helpers'; +import { API_BASE_PATH } from '../../common/constants'; import { PIPELINE_TO_CLONE, PipelinesCloneTestBed } from './helpers/pipelines_clone.helpers'; const { setup } = pageHelpers.pipelinesClone; @@ -33,17 +34,13 @@ jest.mock('@elastic/eui', () => { describe('', () => { let testBed: PipelinesCloneTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - afterAll(() => { - server.restore(); - }); - - httpRequestsMockHelpers.setLoadPipelineResponse(PIPELINE_TO_CLONE); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); beforeEach(async () => { + httpRequestsMockHelpers.setLoadPipelineResponse(PIPELINE_TO_CLONE.name, PIPELINE_TO_CLONE); + await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); @@ -67,14 +64,15 @@ describe('', () => { await actions.clickSubmitButton(); - const latestRequest = server.requests[server.requests.length - 1]; - - const expected = { - ...PIPELINE_TO_CLONE, - name: `${PIPELINE_TO_CLONE.name}-copy`, - }; - - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + expect(httpSetup.post).toHaveBeenLastCalledWith( + API_BASE_PATH, + expect.objectContaining({ + body: JSON.stringify({ + ...PIPELINE_TO_CLONE, + name: `${PIPELINE_TO_CLONE.name}-copy`, + }), + }) + ); }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index 5be5cecd750f6..ebc7acee3095e 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers } from './helpers'; +import { API_BASE_PATH } from '../../common/constants'; import { PipelinesCreateTestBed } from './helpers/pipelines_create.helpers'; import { nestedProcessorsErrorFixture } from './fixtures'; @@ -35,16 +36,12 @@ jest.mock('@elastic/eui', () => { describe('', () => { let testBed: PipelinesCreateTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - afterAll(() => { - server.restore(); - }); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); describe('on component mount', () => { beforeEach(async () => { await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); @@ -106,7 +103,7 @@ describe('', () => { describe('form submission', () => { beforeEach(async () => { await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); @@ -129,27 +126,28 @@ describe('', () => { await actions.clickSubmitButton(); - const latestRequest = server.requests[server.requests.length - 1]; - - const expected = { - name: 'my_pipeline', - description: 'pipeline description', - processors: [], - }; - - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + expect(httpSetup.post).toHaveBeenLastCalledWith( + API_BASE_PATH, + expect.objectContaining({ + body: JSON.stringify({ + name: 'my_pipeline', + description: 'pipeline description', + processors: [], + }), + }) + ); }); test('should surface API errors from the request', async () => { const { actions, find, exists } = testBed; const error = { - status: 409, + statusCode: 409, error: 'Conflict', message: `There is already a pipeline with name 'my_pipeline'.`, }; - httpRequestsMockHelpers.setCreatePipelineResponse(undefined, { body: error }); + httpRequestsMockHelpers.setCreatePipelineResponse(undefined, error); await actions.clickSubmitButton(); @@ -160,7 +158,9 @@ describe('', () => { test('displays nested pipeline errors as a flat list', async () => { const { actions, find, exists, component } = testBed; httpRequestsMockHelpers.setCreatePipelineResponse(undefined, { - body: nestedProcessorsErrorFixture, + statusCode: 409, + message: 'Error', + ...nestedProcessorsErrorFixture, }); await actions.clickSubmitButton(); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx index d6a5b4e01a9b7..4ac864e1ca36f 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx @@ -43,16 +43,12 @@ jest.mock('../../../../../src/plugins/kibana_react/public', () => { }); describe('', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); let testBed: PipelineCreateFromCsvTestBed; - afterAll(() => { - server.restore(); - }); - beforeEach(async () => { await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); @@ -105,7 +101,7 @@ describe('', () => { beforeEach(async () => { await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); @@ -119,18 +115,18 @@ describe('', () => { test('should parse csv from file upload', async () => { const { actions, find } = testBed; - const totalRequests = server.requests.length; await actions.clickProcessCsv(); - expect(server.requests.length).toBe(totalRequests + 1); - - const lastRequest = server.requests[server.requests.length - 1]; - expect(lastRequest.url).toBe(`${API_BASE_PATH}/parse_csv`); - expect(JSON.parse(JSON.parse(lastRequest.requestBody).body)).toEqual({ - copyAction: 'copy', - file: fileContent, - }); + expect(httpSetup.post).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/parse_csv`, + expect.objectContaining({ + body: JSON.stringify({ + file: fileContent, + copyAction: 'copy', + }), + }) + ); expect(JSON.parse(find('pipelineMappingsJSONEditor').text())).toEqual(parsedCsv); }); @@ -142,12 +138,12 @@ describe('', () => { const errorDetails = 'helpful description'; const error = { - status: 400, + statusCode: 400, error: 'Bad Request', message: `${errorTitle}:${errorDetails}`, }; - httpRequestsMockHelpers.setParseCsvResponse(undefined, { body: error }); + httpRequestsMockHelpers.setParseCsvResponse(undefined, error); actions.selectCsvForUpload(mockFile); await actions.clickProcessCsv(); @@ -160,7 +156,7 @@ describe('', () => { describe('results', () => { beforeEach(async () => { await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx index 8b44727b4a985..04ea1e5928a19 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers } from './helpers'; +import { API_BASE_PATH } from '../../common/constants'; import { PIPELINE_TO_EDIT, PipelinesEditTestBed } from './helpers/pipelines_edit.helpers'; const { setup } = pageHelpers.pipelinesEdit; @@ -33,17 +34,13 @@ jest.mock('@elastic/eui', () => { describe('', () => { let testBed: PipelinesEditTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - afterAll(() => { - server.restore(); - }); - - httpRequestsMockHelpers.setLoadPipelineResponse(PIPELINE_TO_EDIT); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); beforeEach(async () => { + httpRequestsMockHelpers.setLoadPipelineResponse(PIPELINE_TO_EDIT.name, PIPELINE_TO_EDIT); + await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); @@ -78,16 +75,16 @@ describe('', () => { await actions.clickSubmitButton(); - const latestRequest = server.requests[server.requests.length - 1]; - const { name, ...pipelineDefinition } = PIPELINE_TO_EDIT; - - const expected = { - ...pipelineDefinition, - description: UPDATED_DESCRIPTION, - }; - - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/${name}`, + expect.objectContaining({ + body: JSON.stringify({ + ...pipelineDefinition, + description: UPDATED_DESCRIPTION, + }), + }) + ); }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts index 3f6a0f57bac34..521dfd4368206 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts @@ -15,17 +15,13 @@ import { PipelineListTestBed } from './helpers/pipelines_list.helpers'; const { setup } = pageHelpers.pipelinesList; describe('', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); let testBed: PipelineListTestBed; - afterAll(() => { - server.restore(); - }); - describe('With pipelines', () => { beforeEach(async () => { await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); @@ -72,12 +68,10 @@ describe('', () => { test('should reload the pipeline data', async () => { const { actions } = testBed; - const totalRequests = server.requests.length; await actions.clickReloadButton(); - expect(server.requests.length).toBe(totalRequests + 1); - expect(server.requests[server.requests.length - 1].url).toBe(API_BASE_PATH); + expect(httpSetup.get).toHaveBeenLastCalledWith(API_BASE_PATH, expect.anything()); }); test('should show the details of a pipeline', async () => { @@ -94,7 +88,7 @@ describe('', () => { const { actions, component } = testBed; const { name: pipelineName } = pipeline1; - httpRequestsMockHelpers.setDeletePipelineResponse({ + httpRequestsMockHelpers.setDeletePipelineResponse(pipelineName, { itemsDeleted: [pipelineName], errors: [], }); @@ -117,11 +111,10 @@ describe('', () => { component.update(); - const deleteRequest = server.requests[server.requests.length - 2]; - - expect(deleteRequest.method).toBe('DELETE'); - expect(deleteRequest.url).toBe(`${API_BASE_PATH}/${pipelineName}`); - expect(deleteRequest.status).toEqual(200); + expect(httpSetup.delete).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/${pipelineName}`, + expect.anything() + ); }); }); @@ -130,7 +123,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadPipelinesResponse([]); await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); const { exists, component, find } = testBed; component.update(); @@ -144,15 +137,15 @@ describe('', () => { describe('Error handling', () => { beforeEach(async () => { const error = { - status: 500, + statusCode: 500, error: 'Internal server error', message: 'Internal server error', }; - httpRequestsMockHelpers.setLoadPipelinesResponse(undefined, { body: error }); + httpRequestsMockHelpers.setLoadPipelinesResponse(undefined, error); await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/http_requests.helpers.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/http_requests.helpers.ts index 4bea242fb8656..516e104b37b3f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/http_requests.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/http_requests.helpers.ts @@ -5,34 +5,58 @@ * 2.0. */ -import sinon, { SinonFakeServer } from 'sinon'; +import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; +import { API_BASE_PATH } from '../../../../../common/constants'; type HttpResponse = Record | any[]; +type HttpMethod = 'GET' | 'POST'; +export interface ResponseError { + statusCode: number; + message: string | Error; + attributes?: Record; +} // Register helpers to mock HTTP Requests -const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setSimulatePipelineResponse = (response?: HttpResponse, error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - - server.respondWith('POST', '/api/ingest_pipelines/simulate', [ - status, - { 'Content-Type': 'application/json' }, - body, - ]); - }; +const registerHttpRequestMockHelpers = ( + httpSetup: ReturnType +) => { + const mockResponses = new Map>>( + ['GET', 'POST'].map( + (method) => [method, new Map()] as [HttpMethod, Map>] + ) + ); + + const mockMethodImplementation = (method: HttpMethod, path: string) => + mockResponses.get(method)?.get(path) ?? Promise.resolve({}); - const setFetchDocumentsResponse = (response?: HttpResponse, error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + httpSetup.get.mockImplementation((path) => + mockMethodImplementation('GET', path as unknown as string) + ); + httpSetup.post.mockImplementation((path) => + mockMethodImplementation('POST', path as unknown as string) + ); - server.respondWith('GET', '/api/ingest_pipelines/documents/:index/:id', [ - status, - { 'Content-Type': 'application/json' }, - body, - ]); + const mockResponse = (method: HttpMethod, path: string, response?: unknown, error?: unknown) => { + const defuse = (promise: Promise) => { + promise.catch(() => {}); + return promise; + }; + + return mockResponses + .get(method)! + .set(path, error ? defuse(Promise.reject({ body: error })) : Promise.resolve(response)); }; + const setSimulatePipelineResponse = (response?: HttpResponse, error?: ResponseError) => + mockResponse('POST', `${API_BASE_PATH}/simulate`, response, error); + + const setFetchDocumentsResponse = ( + index: string, + documentId: string, + response?: HttpResponse, + error?: ResponseError + ) => mockResponse('GET', `${API_BASE_PATH}/documents/${index}/${documentId}`, response, error); + return { setSimulatePipelineResponse, setFetchDocumentsResponse, @@ -40,19 +64,11 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; export const initHttpRequests = () => { - const server = sinon.fakeServer.create(); - - server.respondImmediately = true; - - // Define default response for unhandled requests. - // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, - // and we can mock them all with a 200 instead of mocking each one individually. - server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); - - const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + const httpSetup = httpServiceMock.createSetupContract(); + const httpRequestsMockHelpers = registerHttpRequestMockHelpers(httpSetup); return { - server, + httpSetup, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx index dd269e34fa694..3874fd84932ee 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -178,7 +178,7 @@ const createActions = (testBed: TestBed) => { }; export const setup = async (props: Props): Promise => { - const testBed = await testBedSetup(props); + const testBed = testBedSetup(props); return { ...testBed, actions: createActions(testBed), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index 274d41651fe91..f7ab4c169be50 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -104,7 +104,7 @@ const createActions = (testBed: TestBed) => { }; export const setup = async (props: Props): Promise => { - const testBed = await testBedSetup(props); + const testBed = testBedSetup(props); return { ...testBed, actions: createActions(testBed), @@ -119,10 +119,9 @@ export const setupEnvironment = () => { // @ts-ignore apiService.setup(mockHttpClient, uiMetricService); - const { server, httpRequestsMockHelpers } = initHttpRequests(); + const { httpRequestsMockHelpers } = initHttpRequests(); return { - server, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx index ff8802a91cc9b..8273e650ff137 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx @@ -7,11 +7,10 @@ import { act } from 'react-dom/test-utils'; import React from 'react'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; /* eslint-disable-next-line @kbn/eslint/no-restricted-paths */ import { usageCollectionPluginMock } from 'src/plugins/usage_collection/public/mocks'; +import { HttpSetup } from 'src/core/public'; import { registerTestBed, TestBed } from '@kbn/test-jest-helpers'; import { stubWebWorker } from '@kbn/test-jest-helpers'; @@ -62,6 +61,7 @@ const testBedSetup = registerTestBed( ); export interface SetupResult extends TestBed { + httpSetup: HttpSetup; actions: ReturnType; } @@ -189,30 +189,23 @@ const createActions = (testBed: TestBed) => { }; }; -export const setup = async (props: Props): Promise => { - const testBed = await testBedSetup(props); - return { - ...testBed, - actions: createActions(testBed), - }; -}; - -const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); - -export const setupEnvironment = () => { +export const setup = async (httpSetup: HttpSetup, props: Props): Promise => { // Initialize mock services uiMetricService.setup(usageCollectionPluginMock.createSetupContract()); // @ts-ignore - apiService.setup(mockHttpClient, uiMetricService); + apiService.setup(httpSetup, uiMetricService); - const { server, httpRequestsMockHelpers } = initHttpRequests(); + const testBed = testBedSetup(props); return { - server, - httpRequestsMockHelpers, + ...testBed, + httpSetup, + actions: createActions(testBed), }; }; +export const setupEnvironment = initHttpRequests; + type TestSubject = | 'addDocumentsButton' | 'testPipelineFlyout' diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx index b6026748d99b7..b15172185cff2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx @@ -6,6 +6,7 @@ */ import { Pipeline } from '../../../../../common/types'; +import { API_BASE_PATH } from '../../../../../common/constants'; import { VerboseTestOutput, Document } from '../types'; import { setup, SetupResult, setupEnvironment } from './test_pipeline.helpers'; @@ -21,7 +22,7 @@ describe('Test pipeline', () => { let onUpdate: jest.Mock; let testBed: SetupResult; - const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); // This is a hack // We need to provide the processor id in the mocked output; @@ -49,13 +50,12 @@ describe('Test pipeline', () => { }); afterAll(() => { - server.restore(); jest.useRealTimers(); }); beforeEach(async () => { onUpdate = jest.fn(); - testBed = await setup({ + testBed = await setup(httpSetup, { value: { ...PROCESSORS, }, @@ -87,8 +87,9 @@ describe('Test pipeline', () => { await actions.clickRunPipelineButton(); // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - const requestBody: ReqBody = JSON.parse(JSON.parse(latestRequest.requestBody).body); + const latestRequest: any = httpSetup.post.mock.calls.pop() || []; + const requestBody: ReqBody = JSON.parse(latestRequest[1]?.body); + const { documents: reqDocuments, verbose: reqVerbose, @@ -114,23 +115,26 @@ describe('Test pipeline', () => { expect(exists('outputTabContent')).toBe(true); // Click reload button and verify request - const totalRequests = server.requests.length; await actions.clickRefreshOutputButton(); // There will be two requests made to the simulate API // the second request will have verbose enabled to update the processor results - expect(server.requests.length).toBe(totalRequests + 2); - expect(server.requests[server.requests.length - 2].url).toBe( - '/api/ingest_pipelines/simulate' + expect(httpSetup.post).toHaveBeenNthCalledWith( + 1, + `${API_BASE_PATH}/simulate`, + expect.anything() ); - expect(server.requests[server.requests.length - 1].url).toBe( - '/api/ingest_pipelines/simulate' + expect(httpSetup.post).toHaveBeenNthCalledWith( + 2, + `${API_BASE_PATH}/simulate`, + expect.anything() ); // Click verbose toggle and verify request await actions.toggleVerboseSwitch(); - expect(server.requests.length).toBe(totalRequests + 3); - expect(server.requests[server.requests.length - 1].url).toBe( - '/api/ingest_pipelines/simulate' + // There will be one request made to the simulate API + expect(httpSetup.post).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/simulate`, + expect.anything() ); }); @@ -163,12 +167,12 @@ describe('Test pipeline', () => { const { actions, find, exists } = testBed; const error = { - status: 500, + statusCode: 500, error: 'Internal server error', message: 'Internal server error', }; - httpRequestsMockHelpers.setSimulatePipelineResponse(undefined, { body: error }); + httpRequestsMockHelpers.setSimulatePipelineResponse(undefined, error); // Open flyout actions.clickAddDocumentsButton(); @@ -201,7 +205,7 @@ describe('Test pipeline', () => { const { _index: index, _id: documentId } = DOCUMENTS[0]; - httpRequestsMockHelpers.setFetchDocumentsResponse(DOCUMENTS[0]); + httpRequestsMockHelpers.setFetchDocumentsResponse(index, documentId, DOCUMENTS[0]); // Open flyout actions.clickAddDocumentsButton(); @@ -220,9 +224,10 @@ describe('Test pipeline', () => { await actions.clickAddDocumentButton(); // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - expect(latestRequest.status).toEqual(200); - expect(latestRequest.url).toEqual(`/api/ingest_pipelines/documents/${index}/${documentId}`); + expect(httpSetup.get).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/documents/${index}/${documentId}`, + expect.anything() + ); // Verify success callout expect(exists('addDocumentSuccess')).toBe(true); }); @@ -236,12 +241,17 @@ describe('Test pipeline', () => { }; const error = { - status: 404, + statusCode: 404, error: 'Not found', message: '[index_not_found_exception] no such index', }; - httpRequestsMockHelpers.setFetchDocumentsResponse(undefined, { body: error }); + httpRequestsMockHelpers.setFetchDocumentsResponse( + nonExistentDoc.index, + nonExistentDoc.id, + undefined, + error + ); // Open flyout actions.clickAddDocumentsButton(); From 080c9e5373c48f8bb18bc256605a3f237d8c2bb4 Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Wed, 30 Mar 2022 14:20:01 +0200 Subject: [PATCH 068/108] disable create button when no policy id (#128863) --- .../plugins/fleet/public/components/new_enrollment_key_modal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx b/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx index 9d71a50ce026c..ffda9bdcb16ad 100644 --- a/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx +++ b/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx @@ -149,6 +149,7 @@ export const NewEnrollmentTokenModal: React.FunctionComponent = ({ confirmButtonText={i18n.translate('xpack.fleet.newEnrollmentKey.submitButton', { defaultMessage: 'Create enrollment token', })} + confirmButtonDisabled={!form.policyIdInput.value} > {body} From 141081ea2a4c16f26245c35079444d4af33d79f4 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Wed, 30 Mar 2022 14:23:46 +0200 Subject: [PATCH 069/108] [Remote Clusters] Remove `axios` dependency in tests (#128590) * Remove axios dependency * commit using @elastic.co * Address CR changes --- .../add/remote_clusters_add.helpers.tsx | 37 ++++------ .../add/remote_clusters_add.test.ts | 20 ++---- .../edit/remote_clusters_edit.helpers.tsx | 25 +++---- .../edit/remote_clusters_edit.test.tsx | 15 ++-- .../helpers/http_requests.ts | 70 +++++++++++++------ .../client_integration/helpers/index.ts | 2 +- ...p_environment.ts => setup_environment.tsx} | 36 +++++----- .../list/remote_clusters_list.helpers.js | 12 +++- .../list/remote_clusters_list.test.js | 17 +++-- 9 files changed, 118 insertions(+), 116 deletions(-) rename x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/{setup_environment.ts => setup_environment.tsx} (68%) diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx index a4debdc6ae964..385815f3133db 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.tsx @@ -5,38 +5,27 @@ * 2.0. */ -import React from 'react'; import { registerTestBed } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; import { RemoteClusterAdd } from '../../../public/application/sections'; import { createRemoteClustersStore } from '../../../public/application/store'; import { AppRouter, registerRouter } from '../../../public/application/services'; -import { createRemoteClustersActions } from '../helpers'; -import { AppContextProvider } from '../../../public/application/app_context'; +import { createRemoteClustersActions, WithAppDependencies } from '../helpers'; -const ComponentWithContext = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => { - return ( - - - - ); -}; - -const testBedConfig = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => { - return { - store: createRemoteClustersStore, - memoryRouter: { - onRouter: (router: AppRouter) => registerRouter(router), - }, - defaultProps: { isCloudEnabled }, - }; +const testBedConfig = { + store: createRemoteClustersStore, + memoryRouter: { + onRouter: (router: AppRouter) => registerRouter(router), + }, }; -const initTestBed = (isCloudEnabled: boolean) => - registerTestBed(ComponentWithContext, testBedConfig({ isCloudEnabled }))(); - -export const setup = async (isCloudEnabled = false) => { - const testBed = await initTestBed(isCloudEnabled); +export const setup = async (httpSetup: HttpSetup, overrides?: Record) => { + const initTestBed = registerTestBed( + WithAppDependencies(RemoteClusterAdd, httpSetup, overrides), + testBedConfig + ); + const testBed = await initTestBed(); return { ...testBed, diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts index 28332f71ca6ac..75a1656b0daed 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { SinonFakeServer } from 'sinon'; import { TestBed } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; @@ -17,20 +16,13 @@ const notInArray = (array: string[]) => (value: string) => array.indexOf(value) let component: TestBed['component']; let actions: RemoteClustersActions; -let server: SinonFakeServer; describe('Create Remote cluster', () => { - beforeAll(() => { - ({ server } = setupEnvironment()); - }); - - afterAll(() => { - server.restore(); - }); + const { httpSetup } = setupEnvironment(); beforeEach(async () => { await act(async () => { - ({ actions, component } = await setup()); + ({ actions, component } = await setup(httpSetup)); }); component.update(); }); @@ -95,7 +87,7 @@ describe('Create Remote cluster', () => { describe('on cloud', () => { beforeEach(async () => { await act(async () => { - ({ actions, component } = await setup(true)); + ({ actions, component } = await setup(httpSetup, { isCloudEnabled: true })); }); component.update(); @@ -153,7 +145,7 @@ describe('Create Remote cluster', () => { describe('proxy address', () => { beforeEach(async () => { await act(async () => { - ({ actions, component } = await setup()); + ({ actions, component } = await setup(httpSetup)); }); component.update(); @@ -190,7 +182,7 @@ describe('Create Remote cluster', () => { describe('on prem', () => { beforeEach(async () => { await act(async () => { - ({ actions, component } = await setup()); + ({ actions, component } = await setup(httpSetup)); }); component.update(); @@ -235,7 +227,7 @@ describe('Create Remote cluster', () => { describe('on cloud', () => { beforeEach(async () => { await act(async () => { - ({ actions, component } = await setup(true)); + ({ actions, component } = await setup(httpSetup, { isCloudEnabled: true })); }); component.update(); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx index 86f75c12424e7..87561ccd79c4d 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.tsx @@ -6,13 +6,12 @@ */ import { registerTestBed, TestBedConfig } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; -import React from 'react'; import { RemoteClusterEdit } from '../../../public/application/sections'; import { createRemoteClustersStore } from '../../../public/application/store'; import { AppRouter, registerRouter } from '../../../public/application/services'; -import { createRemoteClustersActions } from '../helpers'; -import { AppContextProvider } from '../../../public/application/app_context'; +import { createRemoteClustersActions, WithAppDependencies } from '../helpers'; export const REMOTE_CLUSTER_EDIT_NAME = 'new-york'; @@ -22,15 +21,6 @@ export const REMOTE_CLUSTER_EDIT = { skipUnavailable: true, }; -const ComponentWithContext = (props: { isCloudEnabled: boolean }) => { - const { isCloudEnabled, ...rest } = props; - return ( - - - - ); -}; - const testBedConfig: TestBedConfig = { store: createRemoteClustersStore, memoryRouter: { @@ -43,11 +33,12 @@ const testBedConfig: TestBedConfig = { }, }; -const initTestBed = (isCloudEnabled: boolean) => - registerTestBed(ComponentWithContext, testBedConfig)({ isCloudEnabled }); - -export const setup = async (isCloudEnabled = false) => { - const testBed = await initTestBed(isCloudEnabled); +export const setup = async (httpSetup: HttpSetup, overrides?: Record) => { + const initTestBed = registerTestBed( + WithAppDependencies(RemoteClusterEdit, httpSetup, overrides), + testBedConfig + ); + const testBed = await initTestBed(); return { ...testBed, diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx index 47aac3f924b96..89bd3a5d9f0e9 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.tsx @@ -20,18 +20,15 @@ import { Cluster } from '../../../common/lib'; let component: TestBed['component']; let actions: RemoteClustersActions; -const { server, httpRequestsMockHelpers } = setupEnvironment(); describe('Edit Remote cluster', () => { - afterAll(() => { - server.restore(); - }); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]); beforeEach(async () => { await act(async () => { - ({ component, actions } = await setup()); + ({ component, actions } = await setup(httpSetup)); }); component.update(); }); @@ -54,7 +51,7 @@ describe('Edit Remote cluster', () => { let addRemoteClusterTestBed: TestBed; await act(async () => { - addRemoteClusterTestBed = await setupRemoteClustersAdd(); + addRemoteClusterTestBed = await setupRemoteClustersAdd(httpSetup); }); addRemoteClusterTestBed!.component.update(); @@ -90,7 +87,7 @@ describe('Edit Remote cluster', () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]); await act(async () => { - ({ component, actions } = await setup(true)); + ({ component, actions } = await setup(httpSetup, { isCloudEnabled: true })); }); component.update(); @@ -108,7 +105,7 @@ describe('Edit Remote cluster', () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]); await act(async () => { - ({ component, actions } = await setup(true)); + ({ component, actions } = await setup(httpSetup, { isCloudEnabled: true })); }); component.update(); @@ -128,7 +125,7 @@ describe('Edit Remote cluster', () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]); await act(async () => { - ({ component, actions } = await setup(true)); + ({ component, actions } = await setup(httpSetup, { isCloudEnabled: true })); }); component.update(); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts index 3ebe3ab5738d6..92b5e4ccbb1ce 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/http_requests.ts @@ -5,26 +5,56 @@ * 2.0. */ -import sinon, { SinonFakeServer } from 'sinon'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; +import { API_BASE_PATH } from '../../../common/constants'; import { Cluster } from '../../../common/lib'; +type HttpMethod = 'GET' | 'DELETE'; + +export interface ResponseError { + statusCode: number; + message: string | Error; +} + // Register helpers to mock HTTP Requests -const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const mockResponse = (response: Cluster[] | { itemsDeleted: string[]; errors: string[] }) => [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]; - - const setLoadRemoteClustersResponse = (response: Cluster[] = []) => { - server.respondWith('GET', '/api/remote_clusters', mockResponse(response)); +const registerHttpRequestMockHelpers = ( + httpSetup: ReturnType +) => { + const mockResponses = new Map>>( + ['GET', 'DELETE'].map( + (method) => [method, new Map()] as [HttpMethod, Map>] + ) + ); + + const mockMethodImplementation = (method: HttpMethod, path: string) => + mockResponses.get(method)?.get(path) ?? Promise.resolve({}); + + httpSetup.get.mockImplementation((path) => + mockMethodImplementation('GET', path as unknown as string) + ); + httpSetup.delete.mockImplementation((path) => + mockMethodImplementation('DELETE', path as unknown as string) + ); + + const mockResponse = (method: HttpMethod, path: string, response?: unknown, error?: unknown) => { + const defuse = (promise: Promise) => { + promise.catch(() => {}); + return promise; + }; + + return mockResponses + .get(method)! + .set(path, error ? defuse(Promise.reject({ body: error })) : Promise.resolve(response)); }; + const setLoadRemoteClustersResponse = (response: Cluster[], error?: ResponseError) => + mockResponse('GET', API_BASE_PATH, response, error); + const setDeleteRemoteClusterResponse = ( - response: { itemsDeleted: string[]; errors: string[] } = { itemsDeleted: [], errors: [] } - ) => { - server.respondWith('DELETE', /api\/remote_clusters/, mockResponse(response)); - }; + clusterName: string, + response: { itemsDeleted: string[]; errors: string[] } = { itemsDeleted: [], errors: [] }, + error?: ResponseError + ) => mockResponse('DELETE', `${API_BASE_PATH}/${clusterName}`, response, error); return { setLoadRemoteClustersResponse, @@ -33,15 +63,11 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; export const init = () => { - const server = sinon.fakeServer.create(); - server.respondImmediately = true; - - // We make requests to APIs which don't impact the UX, e.g. UI metric telemetry, - // and we can mock them all with a 200 instead of mocking each one individually. - server.respondWith([200, {}, '']); + const httpSetup = httpServiceMock.createSetupContract(); + const httpRequestsMockHelpers = registerHttpRequestMockHelpers(httpSetup); return { - server, - httpRequestsMockHelpers: registerHttpRequestMockHelpers(server), + httpSetup, + httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts index b2a7e2d90dc64..caa40969627ac 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/index.ts @@ -6,6 +6,6 @@ */ export { nextTick, getRandomString, findTestSubject } from '@kbn/test-jest-helpers'; -export { setupEnvironment } from './setup_environment'; +export { setupEnvironment, WithAppDependencies } from './setup_environment'; export type { RemoteClustersActions } from './remote_clusters_actions'; export { createRemoteClustersActions } from './remote_clusters_actions'; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.tsx similarity index 68% rename from x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.ts rename to x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.tsx index 084552c5e6abe..a150e2a92fcc9 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.ts +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.tsx @@ -5,15 +5,15 @@ * 2.0. */ -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; - +import React from 'react'; +import { HttpSetup } from 'src/core/public'; import { notificationServiceMock, fatalErrorsServiceMock, docLinksServiceMock, } from '../../../../../../src/core/public/mocks'; +import { AppContextProvider } from '../../../public/application/app_context'; import { usageCollectionPluginMock } from '../../../../../../src/plugins/usage_collection/public/mocks'; import { init as initBreadcrumb } from '../../../public/application/services/breadcrumb'; @@ -23,12 +23,22 @@ import { init as initUiMetric } from '../../../public/application/services/ui_me import { init as initDocumentation } from '../../../public/application/services/documentation'; import { init as initHttpRequests } from './http_requests'; -export const setupEnvironment = () => { - // axios has a similar interface to HttpSetup, but we - // flatten out the response. - const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); - mockHttpClient.interceptors.response.use(({ data }) => data); +export const WithAppDependencies = + (Comp: any, httpSetup: HttpSetup, overrides: Record = {}) => + (props: Record) => { + const { isCloudEnabled, ...rest } = props; + initHttp(httpSetup); + + return ( + + + + ); + }; +export const setupEnvironment = () => { initBreadcrumb(() => {}); initDocumentation(docLinksServiceMock.createStartContract()); initUiMetric(usageCollectionPluginMock.createSetupContract()); @@ -36,14 +46,6 @@ export const setupEnvironment = () => { notificationServiceMock.createSetupContract().toasts, fatalErrorsServiceMock.createSetupContract() ); - // This expects HttpSetup but we're giving it AxiosInstance. - // @ts-ignore - initHttp(mockHttpClient); - - const { server, httpRequestsMockHelpers } = initHttpRequests(); - return { - server, - httpRequestsMockHelpers, - }; + return initHttpRequests(); }; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.helpers.js index 9aeef5d684f3f..f3f25afee3bd7 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.helpers.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.helpers.js @@ -9,6 +9,7 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, findTestSubject } from '@kbn/test-jest-helpers'; +import { WithAppDependencies } from '../helpers'; import { RemoteClusterList } from '../../../public/application/sections/remote_cluster_list'; import { createRemoteClustersStore } from '../../../public/application/store'; import { registerRouter } from '../../../public/application/services/routing'; @@ -20,10 +21,15 @@ const testBedConfig = { }, }; -const initTestBed = registerTestBed(RemoteClusterList, testBedConfig); +export const setup = async (httpSetup, overrides) => { + const initTestBed = registerTestBed( + // ESlint cannot figure out that the hoc should start with a capital leter. + // eslint-disable-next-line + WithAppDependencies(RemoteClusterList, httpSetup, overrides), + testBedConfig + ); + const testBed = await initTestBed(); -export const setup = (props) => { - const testBed = initTestBed(props); const EUI_TABLE = 'remoteClusterListTable'; // User actions diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js index 26af30ba17c04..63367cfd6d001 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js @@ -31,7 +31,7 @@ jest.mock('@elastic/eui/lib/components/search_bar/search_box', () => { }); describe('', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -39,7 +39,6 @@ describe('', () => { afterAll(() => { jest.useRealTimers(); - server.restore(); }); httpRequestsMockHelpers.setLoadRemoteClustersResponse([]); @@ -47,8 +46,8 @@ describe('', () => { describe('on component mount', () => { let exists; - beforeEach(() => { - ({ exists } = setup()); + beforeEach(async () => { + ({ exists } = await setup(httpSetup)); }); test('should show a "loading remote clusters" indicator', () => { @@ -62,7 +61,7 @@ describe('', () => { beforeEach(async () => { await act(async () => { - ({ exists, component } = setup()); + ({ exists, component } = await setup(httpSetup)); }); component.update(); @@ -98,7 +97,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); await act(async () => { - ({ table, component, form } = setup()); + ({ table, component, form } = await setup(httpSetup)); }); component.update(); @@ -154,7 +153,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); await act(async () => { - ({ table, actions, component, form } = setup()); + ({ table, actions, component, form } = await setup(httpSetup)); }); component.update(); @@ -217,7 +216,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); await act(async () => { - ({ component, find, exists, table, actions } = setup()); + ({ component, find, exists, table, actions } = await setup(httpSetup)); }); component.update(); @@ -339,7 +338,7 @@ describe('', () => { describe('confirmation modal (delete remote cluster)', () => { test('should remove the remote cluster from the table after delete is successful', async () => { // Mock HTTP DELETE request - httpRequestsMockHelpers.setDeleteRemoteClusterResponse({ + httpRequestsMockHelpers.setDeleteRemoteClusterResponse(remoteCluster1.name, { itemsDeleted: [remoteCluster1.name], errors: [], }); From dd8176186985a8c3cc3f8cb0dc80b3c2437271e8 Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Wed, 30 Mar 2022 14:30:54 +0200 Subject: [PATCH 070/108] Improve Short URL HTTP error semantics (#128866) * return 409 status code on duplicate slug * support 404 error in by-slug resolution * remove mime type header for errors * harden error code type --- src/plugins/share/server/url_service/error.ts | 15 +++++++ .../http/short_urls/register_create_route.ts | 40 ++++++++++++------- .../http/short_urls/register_resolve_route.ts | 30 ++++++++++---- src/plugins/share/server/url_service/index.ts | 1 + .../short_urls/short_url_client.test.ts | 3 +- .../short_urls/short_url_client.ts | 3 +- .../storage/saved_object_short_url_storage.ts | 3 +- .../apis/short_url/create_short_url/main.ts | 4 +- .../apis/short_url/get_short_url/main.ts | 6 +++ .../apis/short_url/resolve_short_url/main.ts | 6 +++ 10 files changed, 83 insertions(+), 28 deletions(-) create mode 100644 src/plugins/share/server/url_service/error.ts diff --git a/src/plugins/share/server/url_service/error.ts b/src/plugins/share/server/url_service/error.ts new file mode 100644 index 0000000000000..27c52f6b6a796 --- /dev/null +++ b/src/plugins/share/server/url_service/error.ts @@ -0,0 +1,15 @@ +/* + * 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 type UrlServiceErrorCode = 'SLUG_EXISTS' | 'NOT_FOUND' | ''; + +export class UrlServiceError extends Error { + constructor(message: string, public readonly code: UrlServiceErrorCode = '') { + super(message); + } +} diff --git a/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts b/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts index 1d883bfa38086..ca408fbfa8989 100644 --- a/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts +++ b/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; +import { UrlServiceError } from '../../error'; import { ServerUrlService } from '../../types'; export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => { @@ -41,26 +42,35 @@ export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => { if (!locator) { return res.customError({ statusCode: 409, - headers: { - 'content-type': 'application/json', - }, body: 'Locator not found.', }); } - const shortUrl = await shortUrls.create({ - locator, - params, - slug, - humanReadableSlug, - }); + try { + const shortUrl = await shortUrls.create({ + locator, + params, + slug, + humanReadableSlug, + }); - return res.ok({ - headers: { - 'content-type': 'application/json', - }, - body: shortUrl.data, - }); + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: shortUrl.data, + }); + } catch (error) { + if (error instanceof UrlServiceError) { + if (error.code === 'SLUG_EXISTS') { + return res.customError({ + statusCode: 409, + body: error.message, + }); + } + } + throw error; + } }) ); }; diff --git a/src/plugins/share/server/url_service/http/short_urls/register_resolve_route.ts b/src/plugins/share/server/url_service/http/short_urls/register_resolve_route.ts index 5093b12f5450f..a89048b4bbd18 100644 --- a/src/plugins/share/server/url_service/http/short_urls/register_resolve_route.ts +++ b/src/plugins/share/server/url_service/http/short_urls/register_resolve_route.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; +import { UrlServiceError } from '../../error'; import { ServerUrlService } from '../../types'; export const registerResolveRoute = (router: IRouter, url: ServerUrlService) => { @@ -26,15 +27,28 @@ export const registerResolveRoute = (router: IRouter, url: ServerUrlService) => router.handleLegacyErrors(async (ctx, req, res) => { const slug = req.params.slug; const savedObjects = ctx.core.savedObjects.client; - const shortUrls = url.shortUrls.get({ savedObjects }); - const shortUrl = await shortUrls.resolve(slug); - return res.ok({ - headers: { - 'content-type': 'application/json', - }, - body: shortUrl.data, - }); + try { + const shortUrls = url.shortUrls.get({ savedObjects }); + const shortUrl = await shortUrls.resolve(slug); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: shortUrl.data, + }); + } catch (error) { + if (error instanceof UrlServiceError) { + if (error.code === 'NOT_FOUND') { + return res.customError({ + statusCode: 404, + body: error.message, + }); + } + } + throw error; + } }) ); }; diff --git a/src/plugins/share/server/url_service/index.ts b/src/plugins/share/server/url_service/index.ts index 62d1329371736..e88f28b9d5a4f 100644 --- a/src/plugins/share/server/url_service/index.ts +++ b/src/plugins/share/server/url_service/index.ts @@ -10,3 +10,4 @@ export * from './types'; export * from './short_urls'; export { registerUrlServiceRoutes } from './http/register_url_service_routes'; export { registerUrlServiceSavedObjectType } from './saved_objects/register_url_service_saved_object_type'; +export * from './error'; diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts index 503748a2b1cad..8c6ad49fcb9bb 100644 --- a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts +++ b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts @@ -12,6 +12,7 @@ import { LegacyShortUrlLocatorDefinition } from '../../../common/url_service/loc import { MemoryShortUrlStorage } from './storage/memory_short_url_storage'; import { SerializableRecord } from '@kbn/utility-types'; import { SavedObjectReference } from 'kibana/server'; +import { UrlServiceError } from '../error'; const setup = () => { const currentVersion = '1.2.3'; @@ -125,7 +126,7 @@ describe('ServerShortUrlClient', () => { url: '/app/test#foo/bar/baz', }, }) - ).rejects.toThrowError(new Error(`Slug "lala" already exists.`)); + ).rejects.toThrowError(new UrlServiceError(`Slug "lala" already exists.`, 'SLUG_EXISTS')); }); test('can automatically generate human-readable slug', async () => { diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.ts index 1efece073d955..dc1b6127a7c18 100644 --- a/src/plugins/share/server/url_service/short_urls/short_url_client.ts +++ b/src/plugins/share/server/url_service/short_urls/short_url_client.ts @@ -18,6 +18,7 @@ import type { ShortUrlData, LocatorData, } from '../../../common/url_service'; +import { UrlServiceError } from '../error'; import type { ShortUrlStorage } from './types'; import { validateSlug } from './util'; @@ -74,7 +75,7 @@ export class ServerShortUrlClient implements IShortUrlClient { if (slug) { const isSlugTaken = await storage.exists(slug); if (isSlugTaken) { - throw new Error(`Slug "${slug}" already exists.`); + throw new UrlServiceError(`Slug "${slug}" already exists.`, 'SLUG_EXISTS'); } } diff --git a/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts b/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts index 792dfabde3cab..49d5bfb4741d5 100644 --- a/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts +++ b/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts @@ -9,6 +9,7 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { SavedObject, SavedObjectReference, SavedObjectsClientContract } from 'kibana/server'; import { ShortUrlRecord } from '..'; +import { UrlServiceError } from '../..'; import { LEGACY_SHORT_URL_LOCATOR_ID } from '../../../../common/url_service/locators/legacy_short_url_locator'; import { ShortUrlData } from '../../../../common/url_service/short_urls/types'; import { ShortUrlStorage } from '../types'; @@ -161,7 +162,7 @@ export class SavedObjectShortUrlStorage implements ShortUrlStorage { }); if (result.saved_objects.length !== 1) { - throw new Error('not found'); + throw new UrlServiceError('not found', 'NOT_FOUND'); } const savedObject = result.saved_objects[0] as ShortUrlSavedObject; diff --git a/test/api_integration/apis/short_url/create_short_url/main.ts b/test/api_integration/apis/short_url/create_short_url/main.ts index a01a23906a337..4eb6fa489b725 100644 --- a/test/api_integration/apis/short_url/create_short_url/main.ts +++ b/test/api_integration/apis/short_url/create_short_url/main.ts @@ -131,8 +131,8 @@ export default function ({ getService }: FtrProviderContext) { slug, }); - expect(response1.status === 200).to.be(true); - expect(response2.status >= 400).to.be(true); + expect(response1.status).to.be(200); + expect(response2.status).to.be(409); }); }); }); diff --git a/test/api_integration/apis/short_url/get_short_url/main.ts b/test/api_integration/apis/short_url/get_short_url/main.ts index 692c907874255..65ccd50fe5b5f 100644 --- a/test/api_integration/apis/short_url/get_short_url/main.ts +++ b/test/api_integration/apis/short_url/get_short_url/main.ts @@ -23,6 +23,12 @@ export default function ({ getService }: FtrProviderContext) { expect(response2.body).to.eql(response1.body); }); + it('returns 404 error when short URL does not exist', async () => { + const response = await supertest.get('/api/short_url/NotExistingID'); + + expect(response.status).to.be(404); + }); + it('supports legacy short URLs', async () => { const id = 'abcdefghjabcdefghjabcdefghjabcdefghj'; await supertest.post('/api/saved_objects/url/' + id).send({ diff --git a/test/api_integration/apis/short_url/resolve_short_url/main.ts b/test/api_integration/apis/short_url/resolve_short_url/main.ts index a1cf693bd4a53..a0745ee506cb8 100644 --- a/test/api_integration/apis/short_url/resolve_short_url/main.ts +++ b/test/api_integration/apis/short_url/resolve_short_url/main.ts @@ -26,6 +26,12 @@ export default function ({ getService }: FtrProviderContext) { expect(response2.body).to.eql(response1.body); }); + it('returns 404 error when short URL does not exist', async () => { + const response = await supertest.get('/api/short_url/_slug/not-existing-slug'); + + expect(response.status).to.be(404); + }); + it('can resolve a short URL by its slug, when slugs are similar', async () => { const rnd = Math.round(Math.random() * 1e6) + 1; const now = Date.now(); From f79dcd81d3288b78cb93c76483df0e39b49599ac Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 30 Mar 2022 09:22:58 -0400 Subject: [PATCH 071/108] [Response Ops] Add warnings to execution log (#128821) * Retrieving warnings in exec log and exec log errors * Adding column for num scheduled actions * Fixing functional test * PR feedback --- .../alerting/common/execution_log_types.ts | 2 + .../lib/get_execution_log_aggregation.test.ts | 39 ++++++++++++++++++- .../lib/get_execution_log_aggregation.ts | 10 +++++ .../routes/get_rule_execution_log.test.ts | 2 + .../server/routes/get_rule_execution_log.ts | 1 + .../server/rules_client/rules_client.ts | 2 +- .../tests/get_execution_log.test.ts | 18 ++++++--- .../alerting/server/task_runner/fixtures.ts | 13 ++++++- .../server/task_runner/task_runner.ts | 2 +- .../public/application/constants/index.ts | 1 + .../components/rule_event_log_list.tsx | 10 +++++ .../tests/alerting/get_execution_log.ts | 9 +++-- 12 files changed, 96 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/alerting/common/execution_log_types.ts b/x-pack/plugins/alerting/common/execution_log_types.ts index 0307985265160..e5047aae9f154 100644 --- a/x-pack/plugins/alerting/common/execution_log_types.ts +++ b/x-pack/plugins/alerting/common/execution_log_types.ts @@ -12,6 +12,7 @@ export const executionLogSortableColumns = [ 'es_search_duration', 'schedule_delay', 'num_triggered_actions', + 'num_scheduled_actions', ] as const; export type ExecutionLogSortFields = typeof executionLogSortableColumns[number]; @@ -26,6 +27,7 @@ export interface IExecutionLog { num_new_alerts: number; num_recovered_alerts: number; num_triggered_actions: number; + num_scheduled_actions: number; num_succeeded_actions: number; num_errored_actions: number; total_search_duration_ms: number; diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts index 6d6871b7ac111..75022427bea27 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts @@ -82,7 +82,7 @@ describe('getExecutionLogAggregation', () => { sort: [{ notsortable: { order: 'asc' } }], }); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,execution_duration,total_search_duration,es_search_duration,schedule_delay,num_triggered_actions]"` + `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,execution_duration,total_search_duration,es_search_duration,schedule_delay,num_triggered_actions,num_scheduled_actions]"` ); }); @@ -94,7 +94,7 @@ describe('getExecutionLogAggregation', () => { sort: [{ notsortable: { order: 'asc' } }, { timestamp: { order: 'asc' } }], }); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,execution_duration,total_search_duration,es_search_duration,schedule_delay,num_triggered_actions]"` + `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,execution_duration,total_search_duration,es_search_duration,schedule_delay,num_triggered_actions,num_scheduled_actions]"` ); }); @@ -195,6 +195,9 @@ describe('getExecutionLogAggregation', () => { numTriggeredActions: { max: { field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions' }, }, + numScheduledActions: { + max: { field: 'kibana.alert.rule.execution.metrics.number_of_scheduled_actions' }, + }, executionDuration: { max: { field: 'event.duration' } }, outcomeAndMessage: { top_hits: { @@ -262,6 +265,9 @@ describe('formatExecutionLogResult', () => { numTriggeredActions: { value: 5.0, }, + numScheduledActions: { + value: 5.0, + }, outcomeAndMessage: { hits: { total: { @@ -344,6 +350,9 @@ describe('formatExecutionLogResult', () => { numTriggeredActions: { value: 5.0, }, + numScheduledActions: { + value: 5.0, + }, outcomeAndMessage: { hits: { total: { @@ -420,6 +429,7 @@ describe('formatExecutionLogResult', () => { num_new_alerts: 5, num_recovered_alerts: 0, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, @@ -438,6 +448,7 @@ describe('formatExecutionLogResult', () => { num_new_alerts: 5, num_recovered_alerts: 5, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, @@ -484,6 +495,9 @@ describe('formatExecutionLogResult', () => { numTriggeredActions: { value: 5.0, }, + numScheduledActions: { + value: 5.0, + }, outcomeAndMessage: { hits: { total: { @@ -569,6 +583,9 @@ describe('formatExecutionLogResult', () => { numTriggeredActions: { value: 5.0, }, + numScheduledActions: { + value: 5.0, + }, outcomeAndMessage: { hits: { total: { @@ -645,6 +662,7 @@ describe('formatExecutionLogResult', () => { num_new_alerts: 5, num_recovered_alerts: 0, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, @@ -663,6 +681,7 @@ describe('formatExecutionLogResult', () => { num_new_alerts: 5, num_recovered_alerts: 5, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, @@ -709,6 +728,9 @@ describe('formatExecutionLogResult', () => { numTriggeredActions: { value: 0.0, }, + numScheduledActions: { + value: 0.0, + }, outcomeAndMessage: { hits: { total: { @@ -786,6 +808,9 @@ describe('formatExecutionLogResult', () => { numTriggeredActions: { value: 5.0, }, + numScheduledActions: { + value: 5.0, + }, outcomeAndMessage: { hits: { total: { @@ -862,6 +887,7 @@ describe('formatExecutionLogResult', () => { num_new_alerts: 0, num_recovered_alerts: 0, num_triggered_actions: 0, + num_scheduled_actions: 0, num_succeeded_actions: 0, num_errored_actions: 0, total_search_duration_ms: 0, @@ -880,6 +906,7 @@ describe('formatExecutionLogResult', () => { num_new_alerts: 5, num_recovered_alerts: 5, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, @@ -926,6 +953,9 @@ describe('formatExecutionLogResult', () => { numTriggeredActions: { value: 5.0, }, + numScheduledActions: { + value: 5.0, + }, outcomeAndMessage: { hits: { total: { @@ -1008,6 +1038,9 @@ describe('formatExecutionLogResult', () => { numTriggeredActions: { value: 5.0, }, + numScheduledActions: { + value: 5.0, + }, outcomeAndMessage: { hits: { total: { @@ -1084,6 +1117,7 @@ describe('formatExecutionLogResult', () => { num_new_alerts: 5, num_recovered_alerts: 5, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 0, num_errored_actions: 5, total_search_duration_ms: 0, @@ -1102,6 +1136,7 @@ describe('formatExecutionLogResult', () => { num_new_alerts: 5, num_recovered_alerts: 5, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts index d090e7f649228..6f8d0d8059b69 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts @@ -26,6 +26,8 @@ const ES_SEARCH_DURATION_FIELD = 'kibana.alert.rule.execution.metrics.es_search_ const TOTAL_SEARCH_DURATION_FIELD = 'kibana.alert.rule.execution.metrics.total_search_duration_ms'; const NUMBER_OF_TRIGGERED_ACTIONS_FIELD = 'kibana.alert.rule.execution.metrics.number_of_triggered_actions'; +const NUMBER_OF_SCHEDULED_ACTIONS_FIELD = + 'kibana.alert.rule.execution.metrics.number_of_scheduled_actions'; const EXECUTION_UUID_FIELD = 'kibana.alert.rule.execution.uuid'; const Millis2Nanos = 1000 * 1000; @@ -57,6 +59,7 @@ interface IExecutionUuidAggBucket extends estypes.AggregationsStringTermsBucketK esSearchDuration: estypes.AggregationsMaxAggregate; totalSearchDuration: estypes.AggregationsMaxAggregate; numTriggeredActions: estypes.AggregationsMaxAggregate; + numScheduledActions: estypes.AggregationsMaxAggregate; outcomeAndMessage: estypes.AggregationsTopHitsAggregate; }; alertCounts: IAlertCounts; @@ -82,6 +85,7 @@ const ExecutionLogSortFields: Record = { es_search_duration: 'ruleExecution>esSearchDuration', schedule_delay: 'ruleExecution>scheduleDelay', num_triggered_actions: 'ruleExecution>numTriggeredActions', + num_scheduled_actions: 'ruleExecution>numScheduledActions', }; export function getExecutionLogAggregation({ page, perPage, sort }: IExecutionLogAggOptions) { @@ -182,6 +186,11 @@ export function getExecutionLogAggregation({ page, perPage, sort }: IExecutionLo field: NUMBER_OF_TRIGGERED_ACTIONS_FIELD, }, }, + numScheduledActions: { + max: { + field: NUMBER_OF_SCHEDULED_ACTIONS_FIELD, + }, + }, executionDuration: { max: { field: DURATION_FIELD, @@ -256,6 +265,7 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio num_new_alerts: bucket?.alertCounts?.buckets?.newAlerts?.doc_count ?? 0, num_recovered_alerts: bucket?.alertCounts?.buckets?.recoveredAlerts?.doc_count ?? 0, num_triggered_actions: bucket?.ruleExecution?.numTriggeredActions?.value ?? 0, + num_scheduled_actions: bucket?.ruleExecution?.numScheduledActions?.value ?? 0, num_succeeded_actions: actionExecutionSuccess, num_errored_actions: actionExecutionError, total_search_duration_ms: bucket?.ruleExecution?.totalSearchDuration?.value ?? 0, diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts index 19a2885dadaf7..f304c7be86131 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts @@ -38,6 +38,7 @@ describe('getRuleExecutionLogRoute', () => { num_new_alerts: 5, num_recovered_alerts: 0, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, @@ -56,6 +57,7 @@ describe('getRuleExecutionLogRoute', () => { num_new_alerts: 5, num_recovered_alerts: 5, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.ts index 845c14ecf0ea4..066f72e4f9459 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.ts @@ -25,6 +25,7 @@ const sortFieldSchema = schema.oneOf([ schema.object({ es_search_duration: schema.object({ order: sortOrderSchema }) }), schema.object({ schedule_delay: schema.object({ order: sortOrderSchema }) }), schema.object({ num_triggered_actions: schema.object({ order: sortOrderSchema }) }), + schema.object({ num_scheduled_actions: schema.object({ order: sortOrderSchema }) }), ]); const sortFieldsSchema = schema.arrayOf(sortFieldSchema, { diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index ab34158861ad2..901d7102f40c6 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -753,7 +753,7 @@ export class RulesClient { start: parsedDateStart.toISOString(), end: parsedDateEnd.toISOString(), per_page: 500, - filter: `(event.action:execute AND event.outcome:failure) OR (event.action:execute-timeout)`, + filter: `(event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout)`, sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], }, rule.legacyId !== null ? [rule.legacyId] : undefined diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts index 53c8884798325..8a16bcb2d2fd7 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts @@ -128,6 +128,9 @@ const aggregateResults = { numTriggeredActions: { value: 5.0, }, + numScheduledActions: { + value: 5.0, + }, outcomeAndMessage: { hits: { total: { @@ -210,6 +213,9 @@ const aggregateResults = { numTriggeredActions: { value: 5.0, }, + numScheduledActions: { + value: 5.0, + }, outcomeAndMessage: { hits: { total: { @@ -625,6 +631,7 @@ describe('getExecutionLogForRule()', () => { num_new_alerts: 5, num_recovered_alerts: 0, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, @@ -643,6 +650,7 @@ describe('getExecutionLogForRule()', () => { num_new_alerts: 5, num_recovered_alerts: 5, num_triggered_actions: 5, + num_scheduled_actions: 5, num_succeeded_actions: 5, num_errored_actions: 0, total_search_duration_ms: 0, @@ -731,7 +739,7 @@ describe('getExecutionLogForRule()', () => { ['1'], { per_page: 500, - filter: `(event.action:execute AND event.outcome:failure) OR (event.action:execute-timeout)`, + filter: `(event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout)`, sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], end: mockedDateString, start: '2019-02-12T20:01:22.479Z', @@ -771,7 +779,7 @@ describe('getExecutionLogForRule()', () => { ['1'], { per_page: 500, - filter: `(event.action:execute AND event.outcome:failure) OR (event.action:execute-timeout)`, + filter: `(event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout)`, sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], end: mockedDateString, start: '2019-02-12T20:01:22.479Z', @@ -811,7 +819,7 @@ describe('getExecutionLogForRule()', () => { ['1'], { per_page: 500, - filter: `(event.action:execute AND event.outcome:failure) OR (event.action:execute-timeout)`, + filter: `(event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout)`, sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], end: '2019-02-12T20:16:22.479Z', start: '2019-02-12T20:01:22.479Z', @@ -852,7 +860,7 @@ describe('getExecutionLogForRule()', () => { ['1'], { per_page: 500, - filter: `(event.action:execute AND event.outcome:failure) OR (event.action:execute-timeout)`, + filter: `(event.action:execute AND (event.outcome:failure OR kibana.alerting.status:warning)) OR (event.action:execute-timeout)`, sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], end: mockedDateString, start: '2019-02-12T20:01:22.479Z', @@ -917,7 +925,7 @@ describe('getExecutionLogForRule()', () => { getExecutionLogByIdParams({ sort: [{ foo: { order: 'desc' } }] }) ) ).rejects.toMatchInlineSnapshot( - `[Error: Invalid sort field "foo" - must be one of [timestamp,execution_duration,total_search_duration,es_search_duration,schedule_delay,num_triggered_actions]]` + `[Error: Invalid sort field "foo" - must be one of [timestamp,execution_duration,total_search_duration,es_search_duration,schedule_delay,num_triggered_actions,num_scheduled_actions]]` ); }); diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index d8db61cdddc0d..1c8e1776a523f 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -6,7 +6,12 @@ */ import { isNil } from 'lodash'; -import { Alert, AlertTypeParams, RecoveredActionGroup } from '../../common'; +import { + Alert, + AlertExecutionStatusWarningReasons, + AlertTypeParams, + RecoveredActionGroup, +} from '../../common'; import { getDefaultRuleMonitoring } from './task_runner'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { TaskStatus } from '../../../task_manager/server'; @@ -328,6 +333,12 @@ const generateMessage = ({ if (actionGroupId === 'recovered') { return `rule-name' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${actionId}`; } + if ( + status === 'warning' && + reason === AlertExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS + ) { + return `The maximum number of actions for this rule type was reached; excess actions were not triggered.`; + } return `rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`; } }; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 1ddca46d17418..d3be5e3e6623d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -812,7 +812,7 @@ export class TaskRunner< } else { if (executionStatus.warning) { set(event, 'event.reason', executionStatus.warning?.reason || 'unknown'); - set(event, 'message', event?.message || executionStatus.warning.message); + set(event, 'message', executionStatus.warning?.message || event?.message); } set( event, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 0d26abc3bc67b..c6da598a18f8e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -50,6 +50,7 @@ export const RULE_EXECUTION_LOG_COLUMN_IDS = [ 'num_new_alerts', 'num_recovered_alerts', 'num_triggered_actions', + 'num_scheduled_actions', 'num_succeeded_actions', 'num_errored_actions', 'total_search_duration', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list.tsx index 9a6814d1dd9c4..7b9ade9b5f192 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list.tsx @@ -142,6 +142,16 @@ const columns = [ ), isSortable: getIsColumnSortable('num_triggered_actions'), }, + { + id: 'num_scheduled_actions', + displayAsText: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions', + { + defaultMessage: 'Scheduled actions', + } + ), + isSortable: getIsColumnSortable('num_scheduled_actions'), + }, { id: 'num_succeeded_actions', displayAsText: i18n.translate( diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts index c7bb15ba12a98..17e2a4c395989 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts @@ -26,8 +26,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo const dateStart = new Date(Date.now() - 600000).toISOString(); - // FLAKY: https://github.com/elastic/kibana/issues/128225 - describe.skip('getExecutionLog', () => { + describe('getExecutionLog', () => { const objectRemover = new ObjectRemover(supertest); beforeEach(async () => { @@ -95,6 +94,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo expect(log.num_new_alerts).to.equal(0); expect(log.num_recovered_alerts).to.equal(0); expect(log.num_triggered_actions).to.equal(0); + expect(log.num_scheduled_actions).to.equal(0); expect(log.num_succeeded_actions).to.equal(0); expect(log.num_errored_actions).to.equal(0); @@ -108,7 +108,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestRuleData({ schedule: { interval: '15s' } })) + .send(getTestRuleData({ enabled: false, schedule: { interval: '15s' } })) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -169,6 +169,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo expect(log.num_new_alerts).to.equal(0); expect(log.num_recovered_alerts).to.equal(0); expect(log.num_triggered_actions).to.equal(0); + expect(log.num_scheduled_actions).to.equal(0); expect(log.num_succeeded_actions).to.equal(0); expect(log.num_errored_actions).to.equal(0); @@ -323,6 +324,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo expect(log.num_new_alerts).to.equal(1); expect(log.num_recovered_alerts).to.equal(0); expect(log.num_triggered_actions).to.equal(1); + expect(log.num_scheduled_actions).to.equal(1); expect(log.num_succeeded_actions).to.equal(1); expect(log.num_errored_actions).to.equal(0); } @@ -380,6 +382,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo expect(log.num_new_alerts).to.equal(1); expect(log.num_recovered_alerts).to.equal(0); expect(log.num_triggered_actions).to.equal(1); + expect(log.num_scheduled_actions).to.equal(1); expect(log.num_succeeded_actions).to.equal(0); expect(log.num_errored_actions).to.equal(1); } From 09218a8fed9b2e52f54e86ddd847ee2afc7534a8 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Wed, 30 Mar 2022 08:42:33 -0500 Subject: [PATCH 072/108] [Security Solution] add warning message for duplicate blocklist values (#128708) Co-authored-by: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> --- .../pages/blocklist/translations.ts | 16 +++- .../view/components/blocklist_form.tsx | 74 ++++++++++++++----- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts b/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts index c7537243abc68..4043d4bc09b93 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts @@ -113,13 +113,25 @@ export const ERRORS = { INVALID_HASH: i18n.translate('xpack.securitySolution.blocklists.errors.values.invalidHash', { defaultMessage: 'Invalid hash value', }), - INVALID_PATH: i18n.translate('xpack.securitySolution.blocklists.errors.values.invalidPath', { + INVALID_PATH: i18n.translate('xpack.securitySolution.blocklists.warnings.values.invalidPath', { defaultMessage: 'Path may be formed incorrectly; verify value', }), WILDCARD_PRESENT: i18n.translate( - 'xpack.securitySolution.blocklists.errors.values.wildcardPresent', + 'xpack.securitySolution.blocklists.warnings.values.wildcardPresent', { defaultMessage: "A wildcard in the filename will affect the endpoint's performance", } ), + DUPLICATE_VALUE: i18n.translate( + 'xpack.securitySolution.blocklists.warnings.values.duplicateValue', + { + defaultMessage: 'This value already exists', + } + ), + DUPLICATE_VALUES: i18n.translate( + 'xpack.securitySolution.blocklists.warnings.values.duplicateValues', + { + defaultMessage: 'One or more duplicate values removed', + } + ), }; diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx index 9a6be2814a396..8d56c5842df02 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx @@ -74,9 +74,15 @@ interface BlocklistEntry { value: string[]; } +type ERROR_KEYS = keyof typeof ERRORS; + +type ItemValidationNodes = { + [K in ERROR_KEYS]?: React.ReactNode; +}; + interface ItemValidation { - name?: React.ReactNode[]; - value?: React.ReactNode[]; + name: ItemValidationNodes; + value: ItemValidationNodes; } function createValidationMessage(message: string): React.ReactNode { @@ -95,7 +101,7 @@ function getDropdownDisplay(field: BlocklistConditionEntryField): React.ReactNod } function isValid(itemValidation: ItemValidation): boolean { - return !Object.values(itemValidation).some((error) => error.length); + return !Object.values(itemValidation).some((errors) => Object.keys(errors).length); } export const BlockListForm = memo( @@ -104,8 +110,8 @@ export const BlockListForm = memo( name: false, value: false, }); - const warningsRef = useRef({}); - const errorsRef = useRef({}); + const warningsRef = useRef({ name: {}, value: {} }); + const errorsRef = useRef({ name: {}, value: {} }); const [selectedPolicies, setSelectedPolicies] = useState([]); const isPlatinumPlus = useLicense().isPlatinumPlus(); const isGlobal = useMemo(() => isArtifactGlobal(item as ExceptionListItemSchema), [item]); @@ -208,30 +214,30 @@ export const BlockListForm = memo( value: values = [], } = (nextItem.entries[0] ?? {}) as BlocklistEntry; - const newValueWarnings: React.ReactNode[] = []; - const newNameErrors: React.ReactNode[] = []; - const newValueErrors: React.ReactNode[] = []; + const newValueWarnings: ItemValidationNodes = {}; + const newNameErrors: ItemValidationNodes = {}; + const newValueErrors: ItemValidationNodes = {}; // error if name empty if (!nextItem.name.trim()) { - newNameErrors.push(createValidationMessage(ERRORS.NAME_REQUIRED)); + newNameErrors.NAME_REQUIRED = createValidationMessage(ERRORS.NAME_REQUIRED); } // error if no values if (!values.length) { - newValueErrors.push(createValidationMessage(ERRORS.VALUE_REQUIRED)); + newValueErrors.VALUE_REQUIRED = createValidationMessage(ERRORS.VALUE_REQUIRED); } // error if invalid hash if (field === 'file.hash.*' && values.some((value) => !isValidHash(value))) { - newValueErrors.push(createValidationMessage(ERRORS.INVALID_HASH)); + newValueErrors.INVALID_HASH = createValidationMessage(ERRORS.INVALID_HASH); } const isInvalidPath = values.some((value) => !isPathValid({ os, field, type, value })); // warn if invalid path if (field !== 'file.hash.*' && isInvalidPath) { - newValueWarnings.push(createValidationMessage(ERRORS.INVALID_PATH)); + newValueWarnings.INVALID_PATH = createValidationMessage(ERRORS.INVALID_PATH); } // warn if wildcard @@ -240,10 +246,15 @@ export const BlockListForm = memo( !isInvalidPath && values.some((value) => !hasSimpleExecutableName({ os, type, value })) ) { - newValueWarnings.push(createValidationMessage(ERRORS.WILDCARD_PRESENT)); + newValueWarnings.WILDCARD_PRESENT = createValidationMessage(ERRORS.WILDCARD_PRESENT); + } + + // warn if duplicates + if (values.length !== uniq(values).length) { + newValueWarnings.DUPLICATE_VALUES = createValidationMessage(ERRORS.DUPLICATE_VALUES); } - warningsRef.current = { ...warningsRef, value: newValueWarnings }; + warningsRef.current = { ...warningsRef.current, value: newValueWarnings }; errorsRef.current = { name: newNameErrors, value: newValueErrors }; }, []); @@ -331,6 +342,27 @@ export const BlockListForm = memo( [validateValues, onChange, item, blocklistEntry] ); + const handleOnValueTextChange = useCallback( + (value: string) => { + const nextWarnings = { ...warningsRef.current.value }; + + if (blocklistEntry.value.includes(value)) { + nextWarnings.DUPLICATE_VALUE = createValidationMessage(ERRORS.DUPLICATE_VALUE); + } else { + delete nextWarnings.DUPLICATE_VALUE; + } + + warningsRef.current = { + ...warningsRef.current, + value: nextWarnings, + }; + + // trigger re-render without modifying item + setVisited((prevVisited) => ({ ...prevVisited })); + }, + [blocklistEntry] + ); + // only triggered on remove / clear const handleOnValueChange = useCallback( (change: Array>) => { @@ -353,7 +385,7 @@ export const BlockListForm = memo( const handleOnValueAdd = useCallback( (option: string) => { const splitValues = option.split(',').filter((value) => value.trim()); - const value = uniq([...blocklistEntry.value, ...splitValues]); + const value = [...blocklistEntry.value, ...splitValues]; const nextItem = { ...item, @@ -361,6 +393,7 @@ export const BlockListForm = memo( }; validateValues(nextItem); + nextItem.entries[0].value = uniq(nextItem.entries[0].value); setVisited((prevVisited) => ({ ...prevVisited, value: true })); onChange({ @@ -409,8 +442,8 @@ export const BlockListForm = memo( Date: Wed, 30 Mar 2022 16:46:02 +0200 Subject: [PATCH 073/108] [Lens] fix displaying position options for ref lines (#128778) * [Lens] fix displaying position options for ref lines * fix types * move annotation config panel * annotations functional tests * fix dark theme style Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/helpers/annotations.tsx | 9 +- .../plugins/lens/public/app_plugin/app.scss | 2 +- .../shared_components/dimension_section.scss | 6 +- .../shared_components/dimension_section.tsx | 2 +- .../annotations/config_panel/index.scss | 3 - .../xy_visualization/annotations/helpers.tsx | 5 +- .../public/xy_visualization/to_expression.ts | 3 +- .../public/xy_visualization/visualization.tsx | 3 +- .../annotations_config_panel}/icon_set.ts | 0 .../annotations_config_panel}/index.tsx | 24 ++-- .../xy_config_panel/dimension_editor.tsx | 2 +- .../xy_config_panel/reference_line_panel.tsx | 11 +- .../shared/marker_decoration_settings.tsx | 135 ++++++++++-------- .../test/functional/apps/lens/annotations.ts | 74 ++++++++++ x-pack/test/functional/apps/lens/index.ts | 1 + 15 files changed, 198 insertions(+), 82 deletions(-) delete mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.scss rename x-pack/plugins/lens/public/xy_visualization/{annotations/config_panel => xy_config_panel/annotations_config_panel}/icon_set.ts (100%) rename x-pack/plugins/lens/public/xy_visualization/{annotations/config_panel => xy_config_panel/annotations_config_panel}/index.tsx (91%) create mode 100644 x-pack/test/functional/apps/lens/annotations.ts diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx index 5035855647147..9050bdee4a365 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx @@ -76,7 +76,11 @@ export function MarkerBody({ } if (isHorizontal) { return ( -
+
{label}
); @@ -84,6 +88,7 @@ export function MarkerBody({ return (
@@ -139,6 +145,7 @@ export const AnnotationIcon = ({ return (
{title && ( - +

{title}

)} diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.scss b/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.scss deleted file mode 100644 index d84543e4b881b..0000000000000 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -.lnsXyConfigHeading { - padding-bottom: 16px; -} diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx index 8f18450ba5a21..c7370c17c6fec 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -21,11 +21,14 @@ import { import { LensIconChartBarAnnotations } from '../../assets/chart_bar_annotations'; import { generateId } from '../../id_generator'; import { defaultAnnotationColor } from '../../../../../../src/plugins/event_annotation/public'; -import { defaultAnnotationLabel } from './config_panel'; const MAX_DATE = 8640000000000000; const MIN_DATE = -8640000000000000; +export const defaultAnnotationLabel = i18n.translate('xpack.lens.xyChart.defaultAnnotationLabel', { + defaultMessage: 'Event', +}); + export function getStaticDate( dataLayers: XYDataLayerConfig[], activeData: FramePublicAPI['activeData'] diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 86ae7e0bc328e..ef3ec089e8110 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -31,8 +31,7 @@ import { getReferenceLayers, getAnnotationsLayers, } from './visualization_helpers'; -import { defaultAnnotationLabel } from './annotations/config_panel'; -import { getUniqueLabels } from './annotations/helpers'; +import { getUniqueLabels, defaultAnnotationLabel } from './annotations/helpers'; import { layerTypes } from '../../common'; export const getSortedAccessors = ( diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 1a6af0dc36475..95d9e8283fb86 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -67,8 +67,9 @@ import { import { groupAxesByType } from './axes_configuration'; import { XYState } from './types'; import { ReferenceLinePanel } from './xy_config_panel/reference_line_panel'; +import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel'; import { DimensionTrigger } from '../shared_components/dimension_trigger'; -import { AnnotationsPanel, defaultAnnotationLabel } from './annotations/config_panel'; +import { defaultAnnotationLabel } from './annotations/helpers'; export const getXyVisualization = ({ paletteService, diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/icon_set.ts b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/icon_set.ts similarity index 100% rename from x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/icon_set.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/icon_set.ts diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx similarity index 91% rename from x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx index c27165accb81d..b683548cd2517 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback } from 'react'; -import './index.scss'; import { i18n } from '@kbn/i18n'; import { EuiDatePicker, EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import type { PaletteRegistry } from 'src/plugins/charts/public'; @@ -15,18 +14,15 @@ import { EventAnnotationConfig } from 'src/plugins/event_annotation/common/types import type { VisualizationDimensionEditorProps } from '../../../types'; import { State, XYState, XYAnnotationLayerConfig } from '../../types'; import { FormatFactory } from '../../../../common'; -import { ColorPicker } from '../../xy_config_panel/color_picker'; import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components'; import { isHorizontalChart } from '../../state_helpers'; -import { MarkerDecorationSettings } from '../../xy_config_panel/shared/marker_decoration_settings'; -import { LineStyleSettings } from '../../xy_config_panel/shared/line_style_settings'; -import { updateLayer } from '../../xy_config_panel'; +import { defaultAnnotationLabel } from '../../annotations/helpers'; +import { ColorPicker } from '../color_picker'; +import { IconSelectSetting, TextDecorationSetting } from '../shared/marker_decoration_settings'; +import { LineStyleSettings } from '../shared/line_style_settings'; +import { updateLayer } from '..'; import { annotationsIconSet } from './icon_set'; -export const defaultAnnotationLabel = i18n.translate('xpack.lens.xyChart.defaultAnnotationLabel', { - defaultMessage: 'Event', -}); - export const AnnotationsPanel = ( props: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; @@ -101,8 +97,7 @@ export const AnnotationsPanel = ( setAnnotations({ label: value }); }} /> - + = T extends Array ? P : T; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx index ffca2c0531b7c..fbb8920aec49b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx @@ -22,7 +22,11 @@ import { updateLayer } from '.'; import { useDebouncedValue } from '../../shared_components'; import { idPrefix } from './dimension_editor'; import { isHorizontalChart } from '../state_helpers'; -import { MarkerDecorationSettings } from './shared/marker_decoration_settings'; +import { + IconSelectSetting, + MarkerDecorationPosition, + TextDecorationSetting, +} from './shared/marker_decoration_settings'; import { LineStyleSettings } from './shared/line_style_settings'; export const ReferenceLinePanel = ( @@ -72,8 +76,9 @@ export const ReferenceLinePanel = ( return ( <> - {' '} - + + void; - isHorizontal: boolean; customIconSet?: IconSet; }) => { return ( - <> - + - { - setConfig({ textVisibility: id === `${idPrefix}name` }); - }} - isFullWidth - /> - - - { - setConfig({ icon: newIcon }); - }} - /> - - {currentConfig?.iconPosition && - (hasIcon(currentConfig?.icon) || currentConfig?.textVisibility) ? ( + data-test-subj="lns-lineMarker-text-visibility" + name="textVisibilityStyle" + buttonSize="compressed" + options={[ + { + id: `${idPrefix}none`, + label: i18n.translate('xpack.lens.xyChart.lineMarker.textVisibility.none', { + defaultMessage: 'None', + }), + 'data-test-subj': 'lnsXY_textVisibility_none', + }, + { + id: `${idPrefix}name`, + label: i18n.translate('xpack.lens.xyChart.lineMarker.textVisibility.name', { + defaultMessage: 'Name', + }), + 'data-test-subj': 'lnsXY_textVisibility_name', + }, + ]} + idSelected={`${idPrefix}${Boolean(currentConfig?.textVisibility) ? 'name' : 'none'}`} + onChange={(id) => { + setConfig({ textVisibility: id === `${idPrefix}name` }); + }} + isFullWidth + /> + + ); +}; + +export const IconSelectSetting = ({ + currentConfig, + setConfig, + customIconSet, +}: { + currentConfig?: MarkerDecorationConfig; + setConfig: (config: MarkerDecorationConfig) => void; + customIconSet?: IconSet; +}) => { + return ( + + { + setConfig({ icon: newIcon }); + }} + /> + + ); +}; + +export const MarkerDecorationPosition = ({ + currentConfig, + setConfig, + isHorizontal, +}: { + currentConfig?: MarkerDecorationConfig; + setConfig: (config: MarkerDecorationConfig) => void; + isHorizontal: boolean; +}) => { + return ( + <> + {hasIcon(currentConfig?.icon) || currentConfig?.textVisibility ? ( { + it('should show a disabled annotation layer button if there is no date histogram in data layer', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.dragFieldToWorkspace('geo.src', 'xyVisChart'); + await testSubjects.click('lnsLayerAddButton'); + await retry.waitFor('wait for layer popup to appear', async () => + testSubjects.exists(`lnsLayerAddButton-annotations`) + ); + expect( + await (await testSubjects.find(`lnsLayerAddButton-annotations`)).getAttribute('disabled') + ).to.be('true'); + }); + + it('should add manual annotation layer with static date and allow edition', async () => { + await PageObjects.lens.removeLayer(); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.dragFieldToWorkspace('@timestamp', 'xyVisChart'); + + await PageObjects.lens.createLayer('annotations'); + + expect((await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length).to.eql(2); + expect( + await ( + await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger') + ).getVisibleText() + ).to.eql('Event'); + await testSubjects.click('lnsXY_xAnnotationsPanel > lns-dimensionTrigger'); + await testSubjects.click('lnsXY_textVisibility_name'); + await PageObjects.lens.closeDimensionEditor(); + + await testSubjects.existOrFail('xyVisAnnotationIcon'); + await testSubjects.existOrFail('xyVisAnnotationText'); + }); + + it('should duplicate the style when duplicating an annotation and group them in the chart', async () => { + // drag and drop to the empty field to generate a duplicate + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_xAnnotationsPanel > lns-dimensionTrigger', + 'lnsXY_xAnnotationsPanel > lns-empty-dimension' + ); + + await ( + await find.byCssSelector( + '[data-test-subj="lnsXY_xAnnotationsPanel"]:nth-child(2) [data-test-subj="lns-dimensionTrigger"]' + ) + ).click(); + expect( + await find.existsByCssSelector( + '[data-test-subj="lnsXY_textVisibility_name"][class$="isSelected"]' + ) + ).to.be(true); + await PageObjects.lens.closeDimensionEditor(); + await testSubjects.existOrFail('xyVisAnnotationText'); + await testSubjects.existOrFail('xyVisGroupedAnnotationIcon'); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index f66f6cf2f30e5..76a193c8a8b25 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -75,6 +75,7 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid loadTestFile(require.resolve('./gauge')); loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./reference_lines')); + loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./inspector')); loadTestFile(require.resolve('./error_handling')); loadTestFile(require.resolve('./lens_tagging')); From 6ea7541adab0f79030e1e7672a031bcf23e645b0 Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Wed, 30 Mar 2022 16:48:45 +0200 Subject: [PATCH 074/108] Fix metrics to uptime redirection with locator (#125098) * move locator registration to plugin setup * make locator naming consistent * use locator in inventory view * update locator to handle supported host types * try another import * remove locator constant * Revert "remove locator constant" This reverts commit 84416b00caa85943d969d893921308515c0fd784. * Revert "try another import" This reverts commit b42ac97b4096cf0b1d25379c692a58406e237de8. * add share plugin type * reduce constant import scope * fix tests * use uptime locator in waffle context menu * remove obsolete create_uptime_link files * use host.ip instead of monitor.ip * align locator to infra implementation * navigate_to_uptime helper * use navigate_to_uptime helper * fix waffle link color * lint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/infra/kibana.json | 1 + .../components/node_details/overlay.tsx | 9 +- .../components/waffle/node_context_menu.tsx | 12 +- .../lib/create_uptime_link.test.ts | 113 ------------------ .../inventory_view/lib/create_uptime_link.ts | 35 ------ .../inventory_view/lib/navigate_to_uptime.ts | 19 +++ x-pack/plugins/infra/public/types.ts | 3 + x-pack/plugins/observability/common/index.ts | 2 +- .../public/apps/locators/overview.test.ts | 27 ++++- .../uptime/public/apps/locators/overview.ts | 24 +++- x-pack/plugins/uptime/public/apps/plugin.ts | 3 + .../plugins/uptime/public/apps/render_app.tsx | 3 - 12 files changed, 81 insertions(+), 170 deletions(-) delete mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts delete mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/navigate_to_uptime.ts diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index 763c96b415c13..833183ae88276 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -3,6 +3,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "requiredPlugins": [ + "share", "features", "usageCollection", "spaces", diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index 93b17bd8f42ba..5c416b8a10333 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -25,7 +25,8 @@ import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN } from './tabs/shared'; import { useLinkProps } from '../../../../../../../observability/public'; import { getNodeDetailUrl } from '../../../../link_to'; import { findInventoryModel } from '../../../../../../common/inventory_models'; -import { createUptimeLink } from '../../lib/create_uptime_link'; +import { navigateToUptime } from '../../lib/navigate_to_uptime'; +import { InfraClientCoreStart, InfraClientStartDeps } from '../../../../../types'; interface Props { isOpen: boolean; @@ -49,7 +50,8 @@ export const NodeContextPopover = ({ const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab, AnomaliesTab, OsqueryTab]; const inventoryModel = findInventoryModel(nodeType); const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; - const uiCapabilities = useKibana().services.application?.capabilities; + const { application, share } = useKibana().services; + const uiCapabilities = application?.capabilities; const canCreateAlerts = useMemo( () => Boolean(uiCapabilities?.infrastructure?.save), [uiCapabilities] @@ -91,7 +93,6 @@ export const NodeContextPopover = ({ kuery: `${apmField}:"${node.id}"`, }, }); - const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node)); if (!isOpen) { return null; @@ -164,7 +165,7 @@ export const NodeContextPopover = ({ defaultMessage="APM" /> - + navigateToUptime(share.url.locators, nodeType, node)}> {' '} = withTheme const [flyoutVisible, setFlyoutVisible] = useState(false); const inventoryModel = findInventoryModel(nodeType); const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; - const uiCapabilities = useKibana().services.application?.capabilities; + const { application, share } = useKibana() + .services; + const uiCapabilities = application?.capabilities; // Due to the changing nature of the fields between APM and this UI, // We need to have some exceptions until 7.0 & ECS is finalized. Reference // #26620 for the details for these fields. @@ -95,7 +98,6 @@ export const NodeContextMenu: React.FC = withTheme kuery: `${apmField}:"${node.id}"`, }, }); - const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node)); const nodeLogsMenuItem: SectionLinkProps = { label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { @@ -131,7 +133,7 @@ export const NodeContextMenu: React.FC = withTheme defaultMessage: '{inventoryName} in Uptime', values: { inventoryName: inventoryModel.singularDisplayName }, }), - ...uptimeMenuItemLinkProps, + onClick: () => navigateToUptime(share.url.locators, nodeType, node), isDisabled: !showUptimeLink, }; @@ -171,7 +173,7 @@ export const NodeContextMenu: React.FC = withTheme - + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts deleted file mode 100644 index af93f6c0d62ce..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts +++ /dev/null @@ -1,113 +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 { createUptimeLink } from './create_uptime_link'; -import { InfraWaffleMapOptions, InfraFormatterType } from '../../../../lib/lib'; -import { SnapshotMetricType } from '../../../../../common/inventory_models/types'; - -const options: InfraWaffleMapOptions = { - formatter: InfraFormatterType.percent, - formatTemplate: '{{value}}', - metric: { type: 'cpu' }, - groupBy: [], - sort: { by: 'name', direction: 'asc' }, - legend: { - type: 'gradient', - rules: [], - }, -}; - -describe('createUptimeLink()', () => { - it('should work for hosts with ip', () => { - const node = { - pathId: 'host-01', - id: 'host-01', - name: 'host-01', - ip: '10.0.1.2', - path: [], - metrics: [ - { - name: 'cpu' as SnapshotMetricType, - value: 0.5, - max: 0.8, - avg: 0.6, - }, - ], - }; - expect(createUptimeLink(options, 'host', node)).toStrictEqual({ - app: 'uptime', - hash: '/', - search: { search: 'host.ip:"10.0.1.2"' }, - }); - }); - - it('should work for hosts without ip', () => { - const node = { - pathId: 'host-01', - id: 'host-01', - name: 'host-01', - path: [], - metrics: [ - { - name: 'cpu' as SnapshotMetricType, - value: 0.5, - max: 0.8, - avg: 0.6, - }, - ], - }; - expect(createUptimeLink(options, 'host', node)).toStrictEqual({ - app: 'uptime', - hash: '/', - search: { search: 'host.name:"host-01"' }, - }); - }); - - it('should work for pods', () => { - const node = { - pathId: 'pod-01', - id: '29193-pod-02939', - name: 'pod-01', - path: [], - metrics: [ - { - name: 'cpu' as SnapshotMetricType, - value: 0.5, - max: 0.8, - avg: 0.6, - }, - ], - }; - expect(createUptimeLink(options, 'pod', node)).toStrictEqual({ - app: 'uptime', - hash: '/', - search: { search: 'kubernetes.pod.uid:"29193-pod-02939"' }, - }); - }); - - it('should work for container', () => { - const node = { - pathId: 'docker-01', - id: 'docker-1234', - name: 'docker-01', - path: [], - metrics: [ - { - name: 'cpu' as SnapshotMetricType, - value: 0.5, - max: 0.8, - avg: 0.6, - }, - ], - }; - expect(createUptimeLink(options, 'container', node)).toStrictEqual({ - app: 'uptime', - hash: '/', - search: { search: 'container.id:"docker-1234"' }, - }); - }); -}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts deleted file mode 100644 index 6154ed729ebdd..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts +++ /dev/null @@ -1,35 +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 { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib'; -import { InventoryItemType } from '../../../../../common/inventory_models/types'; -import { getFieldByType } from '../../../../../common/inventory_models'; -import { LinkDescriptor } from '../../../../../../observability/public'; - -export const createUptimeLink = ( - options: InfraWaffleMapOptions, - nodeType: InventoryItemType, - node: InfraWaffleMapNode -): LinkDescriptor => { - if (nodeType === 'host' && node.ip) { - return { - app: 'uptime', - hash: '/', - search: { - search: `host.ip:"${node.ip}"`, - }, - }; - } - const field = getFieldByType(nodeType); - return { - app: 'uptime', - hash: '/', - search: { - search: `${field ? field + ':' : ''}"${node.id}"`, - }, - }; -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/navigate_to_uptime.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/navigate_to_uptime.ts new file mode 100644 index 0000000000000..1ad00e82a6ac3 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/navigate_to_uptime.ts @@ -0,0 +1,19 @@ +/* + * 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 { InfraWaffleMapNode } from '../../../../lib/lib'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { uptimeOverviewLocatorID } from '../../../../../../observability/public'; +import { LocatorClient } from '../../../../../../../../src/plugins/share/common/url_service/locators'; + +export const navigateToUptime = ( + locators: LocatorClient, + nodeType: InventoryItemType, + node: InfraWaffleMapNode +) => { + return locators.get(uptimeOverviewLocatorID)!.navigate({ [nodeType]: node.id, ip: node.ip }); +}; diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 8c0033c1b79e5..4ac480484afbf 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -10,6 +10,7 @@ import { IHttpFetchError } from 'src/core/public'; import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import type { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import type { UsageCollectionSetup, UsageCollectionStart, @@ -54,6 +55,7 @@ export interface InfraClientSetupDeps { usageCollection: UsageCollectionSetup; ml: MlPluginSetup; embeddable: EmbeddableSetup; + share: SharePluginSetup; } export interface InfraClientStartDeps { @@ -66,6 +68,7 @@ export interface InfraClientStartDeps { ml: MlPluginStart; embeddable?: EmbeddableStart; osquery?: unknown; // OsqueryPluginStart; + share: SharePluginStart; } export type InfraClientCoreSetup = CoreSetup; diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 1ca110f40bdbf..8a2ee7c0f1718 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -33,4 +33,4 @@ export const casesPath = '/cases'; // Name of a locator created by the uptime plugin. Intended for use // by other plugins as well, so defined here to prevent cross-references. -export const uptimeOverviewLocatorID = 'uptime-overview-locator'; +export const uptimeOverviewLocatorID = 'UPTIME_OVERVIEW_LOCATOR'; diff --git a/x-pack/plugins/uptime/public/apps/locators/overview.test.ts b/x-pack/plugins/uptime/public/apps/locators/overview.test.ts index c414778f7769c..14c05192482a2 100644 --- a/x-pack/plugins/uptime/public/apps/locators/overview.test.ts +++ b/x-pack/plugins/uptime/public/apps/locators/overview.test.ts @@ -25,17 +25,34 @@ describe('uptimeOverviewNavigatorParams', () => { }); it('creates a path with expected search when hostname is specified', async () => { - const location = await uptimeOverviewNavigatorParams.getLocation({ hostname: 'elastic.co' }); - expect(location.path).toEqual(`${OVERVIEW_ROUTE}?search=url.domain: "elastic.co"`); + const location = await uptimeOverviewNavigatorParams.getLocation({ host: 'elastic.co' }); + expect(location.path).toEqual(`${OVERVIEW_ROUTE}?search=host.name: "elastic.co"`); }); - it('creates a path with expected search when multiple keys are specified', async () => { + it('creates a path with expected search when multiple host keys are specified', async () => { const location = await uptimeOverviewNavigatorParams.getLocation({ - hostname: 'elastic.co', + host: 'elastic.co', ip: '127.0.0.1', }); expect(location.path).toEqual( - `${OVERVIEW_ROUTE}?search=monitor.ip: "127.0.0.1" OR url.domain: "elastic.co"` + `${OVERVIEW_ROUTE}?search=host.name: "elastic.co" OR host.ip: "127.0.0.1"` ); }); + + it('creates a path with expected search when multiple kubernetes pod is specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ + pod: 'foo', + ip: '10.0.0.1', + }); + expect(location.path).toEqual( + `${OVERVIEW_ROUTE}?search=kubernetes.pod.uid: "foo" OR monitor.ip: "10.0.0.1"` + ); + }); + + it('creates a path with expected search when docker container is specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ + container: 'foo', + }); + expect(location.path).toEqual(`${OVERVIEW_ROUTE}?search=container.id: "foo"`); + }); }); diff --git a/x-pack/plugins/uptime/public/apps/locators/overview.ts b/x-pack/plugins/uptime/public/apps/locators/overview.ts index d7faf7b78f797..313383c8f0943 100644 --- a/x-pack/plugins/uptime/public/apps/locators/overview.ts +++ b/x-pack/plugins/uptime/public/apps/locators/overview.ts @@ -6,15 +6,31 @@ */ import { uptimeOverviewLocatorID } from '../../../../observability/public'; -import { OVERVIEW_ROUTE } from '../../../common/constants'; +import { OVERVIEW_ROUTE } from '../../../common/constants/ui'; const formatSearchKey = (key: string, value: string) => `${key}: "${value}"`; -async function navigate({ ip, hostname }: { ip?: string; hostname?: string }) { +async function navigate({ + ip, + host, + container, + pod, +}: { + ip?: string; + host?: string; + container?: string; + pod?: string; +}) { const searchParams: string[] = []; - if (ip) searchParams.push(formatSearchKey('monitor.ip', ip)); - if (hostname) searchParams.push(formatSearchKey('url.domain', hostname)); + if (host) searchParams.push(formatSearchKey('host.name', host)); + if (container) searchParams.push(formatSearchKey('container.id', container)); + if (pod) searchParams.push(formatSearchKey('kubernetes.pod.uid', pod)); + + if (ip) { + const root = host ? 'host' : 'monitor'; + searchParams.push(formatSearchKey(`${root}.ip`, ip)); + } const searchString = searchParams.join(' OR '); diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index 0751ea58cfd14..f0fd66b12525a 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -49,6 +49,7 @@ import { import { LazySyntheticsCustomAssetsExtension } from '../components/fleet_package/lazy_synthetics_custom_assets_extension'; import { Start as InspectorPluginStart } from '../../../../../src/plugins/inspector/public'; import { CasesUiStart } from '../../../cases/public'; +import { uptimeOverviewNavigatorParams } from './locators/overview'; export interface ClientPluginsSetup { home?: HomePublicPluginSetup; @@ -104,6 +105,8 @@ export class UptimePlugin return UptimeDataHelper(coreStart); }; + plugins.share.url.locators.create(uptimeOverviewNavigatorParams); + plugins.observability.dashboard.register({ appName: 'synthetics', hasData: async () => { diff --git a/x-pack/plugins/uptime/public/apps/render_app.tsx b/x-pack/plugins/uptime/public/apps/render_app.tsx index 653ac76c4c544..2e1a6edca6fd0 100644 --- a/x-pack/plugins/uptime/public/apps/render_app.tsx +++ b/x-pack/plugins/uptime/public/apps/render_app.tsx @@ -17,7 +17,6 @@ import { } from '../../common/constants'; import { UptimeApp, UptimeAppProps } from './uptime_app'; import { ClientPluginsSetup, ClientPluginsStart } from './plugin'; -import { uptimeOverviewNavigatorParams } from './locators/overview'; export function renderApp( core: CoreStart, @@ -41,8 +40,6 @@ export function renderApp( const canSave = (capabilities.uptime.save ?? false) as boolean; - plugins.share.url.locators.create(uptimeOverviewNavigatorParams); - const props: UptimeAppProps = { isDev, plugins, From 7d86eed782eb43646fbb69e03edd7d165958f69e Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 30 Mar 2022 08:49:22 -0600 Subject: [PATCH 075/108] [renovate] set stability days config for all non-elastic maintained packages --- renovate.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/renovate.json b/renovate.json index 0b6ca59edefe2..1474bfeedf71f 100644 --- a/renovate.json +++ b/renovate.json @@ -19,7 +19,8 @@ { "matchPackagePatterns": [".*"], "enabled": false, - "prCreation": "not-pending" + "prCreation": "not-pending", + "stabilityDays": 7 }, { "groupName": "@elastic/charts", @@ -28,7 +29,8 @@ "matchBaseBranches": ["main"], "labels": ["release_note:skip", "auto-backport", "Team:DataVis", "v8.1.0", "v7.17.0"], "draftPR": true, - "enabled": true + "enabled": true, + "prCreation": "immediate" }, { "groupName": "@elastic/elasticsearch", @@ -60,7 +62,8 @@ "reviewers": ["team:kibana-core"], "matchBaseBranches": ["main"], "labels": ["release_note:skip", "Team:Core", "backport:skip"], - "enabled": true + "enabled": true, + "prCreation": "immediate" }, { "groupName": "babel", @@ -69,8 +72,7 @@ "reviewers": ["team:kibana-operations"], "matchBaseBranches": ["main"], "labels": ["Team:Operations", "release_note:skip"], - "enabled": true, - "stabilityDays": 7 + "enabled": true }, { "groupName": "typescript", @@ -79,8 +81,7 @@ "reviewers": ["team:kibana-operations"], "matchBaseBranches": ["main"], "labels": ["Team:Operations", "release_note:skip"], - "enabled": true, - "stabilityDays": 7 + "enabled": true }, { "groupName": "polyfills", @@ -90,8 +91,7 @@ "reviewers": ["team:kibana-operations"], "matchBaseBranches": ["main"], "labels": ["Team:Operations", "release_note:skip"], - "enabled": true, - "stabilityDays": 7 + "enabled": true }, { "groupName": "vega related modules", From 3f4aa490ef2c92b07c1389da624d0e744a534b93 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 30 Mar 2022 16:53:32 +0200 Subject: [PATCH 076/108] [Uptime] Add summary exists filter in monitor list (#128640) --- .../server/lib/requests/search/find_potential_matches.ts | 2 ++ .../apis/uptime/rest/monitor_states_generated.ts | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 1963afaf89a34..f7fc89307145f 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -52,6 +52,8 @@ const queryBody = async (queryContext: QueryContext, searchAfter: any, size: num filters.push({ match: { 'monitor.status': queryContext.statusFilter } }); } + filters.push({ exists: { field: 'summary' } }); + filters.push(EXCLUDE_RUN_ONCE_FILTER); const body = { diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts index 05e79d91ddc3a..c9fd7c76e6d22 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts @@ -84,15 +84,6 @@ export default function ({ getService }: FtrProviderContext) { nonSummaryIp = checks[0][0].monitor.ip; }); - it('should match non summary documents without a status filter', async () => { - const filters = makeApiParams(testMonitorId, [{ match: { 'monitor.ip': nonSummaryIp } }]); - - const url = getBaseUrl(dateRangeStart, dateRangeEnd) + `&filters=${filters}`; - const apiResponse = await supertest.get(url); - const nonSummaryRes = apiResponse.body; - expect(nonSummaryRes.summaries.length).to.eql(1); - }); - it('should not match non summary documents if the check status does not match the document status', async () => { const filters = makeApiParams(testMonitorId, [{ match: { 'monitor.ip': nonSummaryIp } }]); const url = From dd0a19033fc74b85855b6bf7f6b3efa4b9ef6529 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 30 Mar 2022 17:22:40 +0200 Subject: [PATCH 077/108] Allow nested declaration for `exposeToBrowser` (#128864) * Allow nested declaration for `exposeToBrowser` * update generated doc * add utest --- ...-core-server.exposedtobrowserdescriptor.md | 16 ++ .../core/server/kibana-plugin-core-server.md | 1 + ....pluginconfigdescriptor.exposetobrowser.md | 4 +- ...ugin-core-server.pluginconfigdescriptor.md | 2 +- src/core/server/index.ts | 1 + .../plugins/create_browser_config.test.ts | 162 ++++++++++++++++++ .../server/plugins/create_browser_config.ts | 32 ++++ src/core/server/plugins/plugins_service.ts | 18 +- src/core/server/plugins/types.test.ts | 90 ++++++++++ src/core/server/plugins/types.ts | 19 +- src/core/server/server.api.md | 20 ++- 11 files changed, 341 insertions(+), 24 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.exposedtobrowserdescriptor.md create mode 100644 src/core/server/plugins/create_browser_config.test.ts create mode 100644 src/core/server/plugins/create_browser_config.ts create mode 100644 src/core/server/plugins/types.test.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.exposedtobrowserdescriptor.md b/docs/development/core/server/kibana-plugin-core-server.exposedtobrowserdescriptor.md new file mode 100644 index 0000000000000..b2bb3f5928dcc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.exposedtobrowserdescriptor.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExposedToBrowserDescriptor](./kibana-plugin-core-server.exposedtobrowserdescriptor.md) + +## ExposedToBrowserDescriptor type + +Type defining the list of configuration properties that will be exposed on the client-side Object properties can either be fully exposed + +Signature: + +```typescript +export declare type ExposedToBrowserDescriptor = { + [Key in keyof T]?: T[Key] extends Maybe ? boolean : T[Key] extends Maybe ? // can be nested for objects + ExposedToBrowserDescriptor | boolean : boolean; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 450af99a5b234..60bbd9af2c9d3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -265,6 +265,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) | Client used to query the elasticsearch cluster. | | [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) | Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | | [ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md) | | +| [ExposedToBrowserDescriptor](./kibana-plugin-core-server.exposedtobrowserdescriptor.md) | Type defining the list of configuration properties that will be exposed on the client-side Object properties can either be fully exposed | | [GetAuthHeaders](./kibana-plugin-core-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. | | [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) to represent the type of the context. | diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md index bf124b97502d4..212a0d1c9a26b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md +++ b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md @@ -9,7 +9,5 @@ List of configuration properties that will be available on the client-side plugi Signature: ```typescript -exposeToBrowser?: { - [P in keyof T]?: boolean; - }; +exposeToBrowser?: ExposedToBrowserDescriptor; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md index b9cf0eea3362d..f5d18c9f40f4d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md +++ b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md @@ -44,7 +44,7 @@ export const config: PluginConfigDescriptor = { | Property | Type | Description | | --- | --- | --- | | [deprecations?](./kibana-plugin-core-server.pluginconfigdescriptor.deprecations.md) | ConfigDeprecationProvider | (Optional) Provider for the to apply to the plugin configuration. | -| [exposeToBrowser?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md) | { \[P in keyof T\]?: boolean; } | (Optional) List of configuration properties that will be available on the client-side plugin. | +| [exposeToBrowser?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md) | ExposedToBrowserDescriptor<T> | (Optional) List of configuration properties that will be available on the client-side plugin. | | [exposeToUsage?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetousage.md) | MakeUsageFromSchema<T> | (Optional) Expose non-default configs to usage collection to be sent via telemetry. set a config to true to report the actual changed config value. set a config to false to report the changed config value as \[redacted\].All changed configs except booleans and numbers will be reported as \[redacted\] unless otherwise specified.[MakeUsageFromSchema](./kibana-plugin-core-server.makeusagefromschema.md) | | [schema](./kibana-plugin-core-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-core-server.pluginconfigschema.md) | diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 6907f7ef1238b..3912585b7b697 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -268,6 +268,7 @@ export type { PluginName, SharedGlobalConfig, MakeUsageFromSchema, + ExposedToBrowserDescriptor, } from './plugins'; export { diff --git a/src/core/server/plugins/create_browser_config.test.ts b/src/core/server/plugins/create_browser_config.test.ts new file mode 100644 index 0000000000000..8b27ba286c53f --- /dev/null +++ b/src/core/server/plugins/create_browser_config.test.ts @@ -0,0 +1,162 @@ +/* + * 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 { ExposedToBrowserDescriptor } from './types'; +import { createBrowserConfig } from './create_browser_config'; + +describe('createBrowserConfig', () => { + it('picks nothing by default', () => { + const config = { + foo: 'bar', + nested: { + str: 'string', + num: 42, + }, + }; + const descriptor: ExposedToBrowserDescriptor = {}; + + const browserConfig = createBrowserConfig(config, descriptor); + + expect(browserConfig).toEqual({}); + }); + + it('picks all the nested properties when using `true`', () => { + const config = { + foo: 'bar', + nested: { + str: 'string', + num: 42, + }, + }; + + const descriptor: ExposedToBrowserDescriptor = { + foo: true, + nested: true, + }; + + const browserConfig = createBrowserConfig(config, descriptor); + + expect(browserConfig).toEqual({ + foo: 'bar', + nested: { + str: 'string', + num: 42, + }, + }); + }); + + it('picks specific nested properties when using a nested declaration', () => { + const config = { + foo: 'bar', + nested: { + str: 'string', + num: 42, + }, + }; + + const descriptor: ExposedToBrowserDescriptor = { + foo: true, + nested: { + str: true, + num: false, + }, + }; + + const browserConfig = createBrowserConfig(config, descriptor); + + expect(browserConfig).toEqual({ + foo: 'bar', + nested: { + str: 'string', + }, + }); + }); + + it('accepts deeply nested structures', () => { + const config = { + foo: 'bar', + deeply: { + str: 'string', + nested: { + hello: 'dolly', + structure: { + propA: 'propA', + propB: 'propB', + }, + }, + }, + }; + + const descriptor: ExposedToBrowserDescriptor = { + foo: false, + deeply: { + str: false, + nested: { + hello: true, + structure: { + propA: true, + propB: false, + }, + }, + }, + }; + + const browserConfig = createBrowserConfig(config, descriptor); + + expect(browserConfig).toEqual({ + deeply: { + nested: { + hello: 'dolly', + structure: { + propA: 'propA', + }, + }, + }, + }); + }); + + it('only includes leaf properties that are `true` when in nested structures', () => { + const config = { + foo: 'bar', + deeply: { + str: 'string', + nested: { + hello: 'dolly', + structure: { + propA: 'propA', + propB: 'propB', + }, + }, + }, + }; + + const descriptor: ExposedToBrowserDescriptor = { + deeply: { + nested: { + hello: true, + structure: { + propA: true, + }, + }, + }, + }; + + const browserConfig = createBrowserConfig(config, descriptor); + + expect(browserConfig).toEqual({ + deeply: { + nested: { + hello: 'dolly', + structure: { + propA: 'propA', + }, + }, + }, + }); + }); +}); diff --git a/src/core/server/plugins/create_browser_config.ts b/src/core/server/plugins/create_browser_config.ts new file mode 100644 index 0000000000000..95c8de7f4c8cd --- /dev/null +++ b/src/core/server/plugins/create_browser_config.ts @@ -0,0 +1,32 @@ +/* + * 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 { ExposedToBrowserDescriptor } from './types'; + +export const createBrowserConfig = ( + config: T, + descriptor: ExposedToBrowserDescriptor +): unknown => { + return recursiveCreateConfig(config, descriptor); +}; + +const recursiveCreateConfig = ( + config: T, + descriptor: ExposedToBrowserDescriptor = {} +): unknown => { + return Object.entries(config || {}).reduce((browserConfig, [key, value]) => { + const exposedConfig = descriptor[key as keyof ExposedToBrowserDescriptor]; + if (exposedConfig && typeof exposedConfig === 'object') { + browserConfig[key] = recursiveCreateConfig(value, exposedConfig); + } + if (exposedConfig === true) { + browserConfig[key] = value; + } + return browserConfig; + }, {} as Record); +}; diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index cde34cea11192..f202f09735d45 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -9,7 +9,7 @@ import Path from 'path'; import { Observable } from 'rxjs'; import { filter, first, map, tap, toArray } from 'rxjs/operators'; -import { getFlattenedObject, pick } from '@kbn/std'; +import { getFlattenedObject } from '@kbn/std'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; @@ -26,6 +26,7 @@ import { } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; +import { createBrowserConfig } from './create_browser_config'; import { InternalCorePreboot, InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { IConfigService } from '../config'; import { InternalEnvironmentServicePreboot } from '../environment'; @@ -228,16 +229,11 @@ export class PluginsService implements CoreService - pick( - config || {}, - Object.entries(configDescriptor.exposeToBrowser!) - .filter(([_, exposed]) => exposed) - .map(([key, _]) => key) - ) - ) - ), + this.configService + .atPath(plugin.configPath) + .pipe( + map((config: any) => createBrowserConfig(config, configDescriptor.exposeToBrowser!)) + ), ]; }) ); diff --git a/src/core/server/plugins/types.test.ts b/src/core/server/plugins/types.test.ts new file mode 100644 index 0000000000000..4a0e6052a9901 --- /dev/null +++ b/src/core/server/plugins/types.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { ExposedToBrowserDescriptor } from './types'; + +describe('ExposedToBrowserDescriptor', () => { + interface ConfigType { + str: string; + array: number[]; + obj: { + sub1: string; + sub2: number; + }; + deep: { + foo: number; + nested: { + str: string; + arr: number[]; + }; + }; + } + + it('allows to use recursion on objects', () => { + const exposeToBrowser: ExposedToBrowserDescriptor = { + obj: { + sub1: true, + }, + }; + expect(exposeToBrowser).toBeDefined(); + }); + + it('allows to use recursion at multiple levels', () => { + const exposeToBrowser: ExposedToBrowserDescriptor = { + deep: { + foo: true, + nested: { + str: true, + }, + }, + }; + expect(exposeToBrowser).toBeDefined(); + }); + + it('does not allow to use recursion on arrays', () => { + const exposeToBrowser: ExposedToBrowserDescriptor = { + // @ts-expect-error Type '{ 0: true; }' is not assignable to type 'boolean | undefined'. + array: { + 0: true, + }, + }; + expect(exposeToBrowser).toBeDefined(); + }); + + it('does not allow to use recursion on arrays at lower levels', () => { + const exposeToBrowser: ExposedToBrowserDescriptor = { + deep: { + nested: { + // @ts-expect-error Type '{ 0: true; }' is not assignable to type 'boolean | undefined'. + arr: { + 0: true, + }, + }, + }, + }; + expect(exposeToBrowser).toBeDefined(); + }); + + it('allows to specify all the properties', () => { + const exposeToBrowser: ExposedToBrowserDescriptor = { + str: true, + array: false, + obj: { + sub1: true, + }, + deep: { + foo: true, + nested: { + arr: false, + str: true, + }, + }, + }; + expect(exposeToBrowser).toBeDefined(); + }); +}); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 991c5628993b0..9da4eb2742acf 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -26,6 +26,23 @@ type Maybe = T | undefined; */ export type PluginConfigSchema = Type; +/** + * Type defining the list of configuration properties that will be exposed on the client-side + * Object properties can either be fully exposed + * + * @public + */ +export type ExposedToBrowserDescriptor = { + [Key in keyof T]?: T[Key] extends Maybe + ? // handles arrays as primitive values + boolean + : T[Key] extends Maybe + ? // can be nested for objects + ExposedToBrowserDescriptor | boolean + : // primitives + boolean; +}; + /** * Describes a plugin configuration properties. * @@ -64,7 +81,7 @@ export interface PluginConfigDescriptor { /** * List of configuration properties that will be available on the client-side plugin. */ - exposeToBrowser?: { [P in keyof T]?: boolean }; + exposeToBrowser?: ExposedToBrowserDescriptor; /** * Schema to use to validate the plugin configuration. * diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 82b4012703be8..c89a5fc89d2fa 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1001,6 +1001,14 @@ export interface ExecutionContextSetup { // @public (undocumented) export type ExecutionContextStart = ExecutionContextSetup; +// Warning: (ae-forgotten-export) The symbol "Maybe" needs to be exported by the entry point index.d.ts +// +// @public +export type ExposedToBrowserDescriptor = { + [Key in keyof T]?: T[Key] extends Maybe ? boolean : T[Key] extends Maybe ? // can be nested for objects + ExposedToBrowserDescriptor | boolean : boolean; +}; + // @public export interface FakeRequest { headers: Headers_2; @@ -1454,8 +1462,6 @@ export { LogMeta } export { LogRecord } -// Warning: (ae-forgotten-export) The symbol "Maybe" needs to be exported by the entry point index.d.ts -// // @public export type MakeUsageFromSchema = { [Key in keyof T]?: T[Key] extends Maybe ? false : T[Key] extends Maybe ? boolean : T[Key] extends Maybe ? MakeUsageFromSchema | boolean : boolean; @@ -1647,9 +1653,7 @@ export { Plugin_2 as Plugin } export interface PluginConfigDescriptor { // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver deprecations?: ConfigDeprecationProvider; - exposeToBrowser?: { - [P in keyof T]?: boolean; - }; + exposeToBrowser?: ExposedToBrowserDescriptor; exposeToUsage?: MakeUsageFromSchema; schema: PluginConfigSchema; } @@ -3161,8 +3165,8 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/elasticsearch/client/types.ts:81:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts // src/core/server/http/router/response.ts:302:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:376:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:378:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:485:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:393:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:395:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:502:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` From b9c3aec20f0b01c134bf4ccce0245b9e599ca3cb Mon Sep 17 00:00:00 2001 From: liza-mae Date: Wed, 30 Mar 2022 09:53:01 -0600 Subject: [PATCH 078/108] Fix unhandled promise rejection in discover tests (#128806) * Fix unhandled promise rejection * Update methods in index.ts to be async * Update test/functional/apps/discover/index.ts Co-authored-by: Spencer * Update test/functional/apps/discover/index.ts Co-authored-by: Spencer Co-authored-by: Spencer Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/discover/_date_nanos_mixed.ts | 2 +- test/functional/apps/discover/_doc_table_newline.ts | 2 +- test/functional/apps/discover/index.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/functional/apps/discover/_date_nanos_mixed.ts b/test/functional/apps/discover/_date_nanos_mixed.ts index 5cd72a67f36b1..219f32fb259b5 100644 --- a/test/functional/apps/discover/_date_nanos_mixed.ts +++ b/test/functional/apps/discover/_date_nanos_mixed.ts @@ -33,7 +33,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await security.testUser.restoreDefaults(); - esArchiver.unload('test/functional/fixtures/es_archiver/date_nanos_mixed'); + await esArchiver.unload('test/functional/fixtures/es_archiver/date_nanos_mixed'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); }); diff --git a/test/functional/apps/discover/_doc_table_newline.ts b/test/functional/apps/discover/_doc_table_newline.ts index cdb1496413484..94bf23a70bc60 100644 --- a/test/functional/apps/discover/_doc_table_newline.ts +++ b/test/functional/apps/discover/_doc_table_newline.ts @@ -29,7 +29,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { await security.testUser.restoreDefaults(); - esArchiver.unload('test/functional/fixtures/es_archiver/message_with_newline'); + await esArchiver.unload('test/functional/fixtures/es_archiver/message_with_newline'); await kibanaServer.uiSettings.unset('defaultIndex'); await kibanaServer.uiSettings.unset('doc_table:legacy'); }); diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index d2b627c175fcc..c9497e872d7b9 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -15,12 +15,12 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('discover app', function () { this.tags('ciGroup6'); - before(function () { - return browser.setWindowSize(1300, 800); + before(async function () { + await browser.setWindowSize(1300, 800); }); - after(function unloadMakelogs() { - return esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + after(async function unloadMakelogs() { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); }); loadTestFile(require.resolve('./_saved_queries')); From 8eadbc655d19542d40ba438498303489a3c32da4 Mon Sep 17 00:00:00 2001 From: liza-mae Date: Wed, 30 Mar 2022 09:53:31 -0600 Subject: [PATCH 079/108] Fix upgrade maps smoke tests (#128696) * Fix upgrade maps smoke tests * Review updates * Update maps services name to be more specific * Rename maps_services file --- .../upgrade/apps/maps/maps_smoke_tests.ts | 9 +-- x-pack/test/upgrade/config.ts | 4 +- x-pack/test/upgrade/maps_upgrade_services.ts | 63 +++++++++++++++++++ x-pack/test/upgrade/services.ts | 2 + 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/upgrade/maps_upgrade_services.ts diff --git a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts index 22e081e88bfc4..673b7e31c231d 100644 --- a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts @@ -16,6 +16,7 @@ export default function ({ updateBaselines, }: FtrProviderContext & { updateBaselines: boolean }) { const PageObjects = getPageObjects(['common', 'maps', 'header', 'home', 'timePicker']); + const mapsHelper = getService('mapsHelper'); const screenshot = getService('screenshots'); const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); @@ -111,7 +112,7 @@ export default function ({ ); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.maps.waitForLayersToLoad(); - await PageObjects.maps.toggleLayerVisibility('Road map - desaturated'); + await mapsHelper.toggleLayerVisibilityRoadMap(); await PageObjects.maps.toggleLayerVisibility('United Kingdom'); await PageObjects.maps.toggleLayerVisibility('France'); await PageObjects.maps.toggleLayerVisibility('United States'); @@ -141,7 +142,7 @@ export default function ({ ); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.maps.waitForLayersToLoad(); - await PageObjects.maps.toggleLayerVisibility('Road map - desaturated'); + await mapsHelper.toggleLayerVisibilityRoadMap(); await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); await PageObjects.maps.closeLegend(); @@ -167,8 +168,8 @@ export default function ({ ); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.maps.waitForLayersToLoad(); - await PageObjects.maps.toggleLayerVisibility('Road map'); - await PageObjects.maps.toggleLayerVisibility('Total Requests by Country'); + await mapsHelper.toggleLayerVisibilityRoadMap(); + await mapsHelper.toggleLayerVisibilityTotalRequests(); await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); await PageObjects.maps.closeLegend(); diff --git a/x-pack/test/upgrade/config.ts b/x-pack/test/upgrade/config.ts index dee3afb63e020..7722c244223cf 100644 --- a/x-pack/test/upgrade/config.ts +++ b/x-pack/test/upgrade/config.ts @@ -8,6 +8,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { ReportingAPIProvider } from './reporting_services'; +import { MapsHelper } from './maps_upgrade_services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); @@ -29,10 +30,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...apiConfig.get('services'), ...functionalConfig.get('services'), reportingAPI: ReportingAPIProvider, + mapsHelper: MapsHelper, }, junit: { - reportName: 'Upgrade Tests', + reportName: 'Kibana Core Tests', }, timeouts: { diff --git a/x-pack/test/upgrade/maps_upgrade_services.ts b/x-pack/test/upgrade/maps_upgrade_services.ts new file mode 100644 index 0000000000000..b5553eeb9366d --- /dev/null +++ b/x-pack/test/upgrade/maps_upgrade_services.ts @@ -0,0 +1,63 @@ +/* + * 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 { FtrProviderContext } from './ftr_provider_context'; + +export function MapsHelper({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['maps']); + const testSubjects = getService('testSubjects'); + + return { + // In v8.0, the default base map switched from bright to desaturated. + // https://github.com/elastic/kibana/pull/116179 + // Maps created before this change will have a base map called "Road map" + // Maps created after this change will have a base map called "Road map - desaturated" + // toggleLayerVisibilityRoadMap will toggle layer visibility for either value + async toggleLayerVisibilityRoadMap() { + const isRoadMapDesaturated = await testSubjects.exists( + 'layerTocActionsPanelToggleButtonRoad_map_-_desaturated' + ); + const isRoadMap = await testSubjects.exists('layerTocActionsPanelToggleButtonRoad_map'); + if (!isRoadMapDesaturated && !isRoadMap) { + throw new Error('Layer road map not found'); + } + if (isRoadMapDesaturated) { + await PageObjects.maps.toggleLayerVisibility('Road map - desaturated'); + } + if (isRoadMap) { + await PageObjects.maps.toggleLayerVisibility('Road map'); + } + }, + + // In v7.16, e-commerce sample data was re-worked so that geo.src field to match country code of geo.coordinates + // https://github.com/elastic/kibana/pull/110885 + // Maps created before this change will have a layer called "Total Requests by Country" + // Maps created after this change will have a layer called "Total Requests by Destination" + // toggleLayerVisibilityTotalRequests will toggle layer visibility for either value + async toggleLayerVisibilityTotalRequests() { + const isRequestByCountry = await testSubjects.exists( + 'layerTocActionsPanelToggleButtonTotal_Requests_by_Country' + ); + const isRequestByDestination = await testSubjects.exists( + 'layerTocActionsPanelToggleButtonTotal_Requests_by_Destination' + ); + if (!isRequestByCountry && !isRequestByDestination) { + throw new Error('Layer total requests not found'); + } + if (isRequestByCountry) { + await PageObjects.maps.toggleLayerVisibility('Total Requests by Country'); + } + if (isRequestByDestination) { + await PageObjects.maps.toggleLayerVisibility('Total Requests by Destination'); + } + }, + }; +} + +export const services = { + mapsHelper: MapsHelper, +}; diff --git a/x-pack/test/upgrade/services.ts b/x-pack/test/upgrade/services.ts index cb49abe5e2011..ca5c23ba335e3 100644 --- a/x-pack/test/upgrade/services.ts +++ b/x-pack/test/upgrade/services.ts @@ -7,8 +7,10 @@ import { services as functionalServices } from '../functional/services'; import { services as reportingServices } from './reporting_services'; +import { services as mapsUpgradeServices } from './maps_upgrade_services'; export const services = { ...functionalServices, ...reportingServices, + ...mapsUpgradeServices, }; From 0068a8c0def3e483429a0a25799339da196c3bfd Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 30 Mar 2022 10:55:42 -0500 Subject: [PATCH 080/108] [artifacts] Setup conditional release vs snapshot build (#128801) * [artifacts] Setup conditional release vs snapshot build * Update .buildkite/scripts/steps/artifacts/build.sh Co-authored-by: Brian Seeders Co-authored-by: Brian Seeders --- .buildkite/scripts/steps/artifacts/build.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.buildkite/scripts/steps/artifacts/build.sh b/.buildkite/scripts/steps/artifacts/build.sh index db1faa184b35a..8f928596f2574 100644 --- a/.buildkite/scripts/steps/artifacts/build.sh +++ b/.buildkite/scripts/steps/artifacts/build.sh @@ -4,8 +4,16 @@ set -euo pipefail .buildkite/scripts/bootstrap.sh +if [[ "${RELEASE_BUILD:-}" == "true" ]]; then + VERSION="$(jq -r '.version' package.json)" + RELEASE_ARG="--release" +else + VERSION="$(jq -r '.version' package.json)-SNAPSHOT" + RELEASE_ARG="" +fi + echo "--- Build Kibana Distribution" -node scripts/build --all-platforms --debug --docker-cross-compile --skip-docker-cloud +node scripts/build "$RELEASE_ARG" --all-platforms --debug --docker-cross-compile --skip-docker-cloud echo "--- Build dependencies report" -node scripts/licenses_csv_report --csv=target/dependencies_report.csv +node scripts/licenses_csv_report "--csv=target/dependencies-$VERSION.csv" From ea545247c6c5a3885ae0ade371afbd3aa463d4b8 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Wed, 30 Mar 2022 16:59:43 +0100 Subject: [PATCH 081/108] [Fleet] Add Data Streams Dev Doc (#128896) * add data streams dev doc * Update after Kyles feedback * fix case error * make difference between mappings and settings more clear Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/fleet/dev_docs/data_streams.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 x-pack/plugins/fleet/dev_docs/data_streams.md diff --git a/x-pack/plugins/fleet/dev_docs/data_streams.md b/x-pack/plugins/fleet/dev_docs/data_streams.md new file mode 100644 index 0000000000000..82899c6b6fe32 --- /dev/null +++ b/x-pack/plugins/fleet/dev_docs/data_streams.md @@ -0,0 +1,54 @@ +# Data Streams + +Packages use [data streams](https://www.elastic.co/guide/en/elasticsearch/reference/current/data-streams.html) to ingest data into elasticsearch. These data streams follow the [data stream naming format](https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme). Data streams are defined in the package and constructed by Fleet during package install. Mappings are generally derived from `/data_stream//fields/*.yml` in the package, there is also the ability for packages to set custom mappings or settings directly, e.g APM sets dynamic mapping [here](https://github.com/elastic/package-storage/blob/production/packages/apm/0.4.0/data_stream/app_metrics/manifest.yml#L8) + + +## Template Structure + +### Index Template +A data stream is an index template with the data stream flag set to true. Each data stream has one index template. For Fleet data streams the index template should remain as empty as possible, with settings, mappings etc being applied in component templates. Only applying settings and mappings in component templates means we can: +- create more granular index templates in the future (e.g namespace specific) that can use the same component templates (keeping one source of truth) +- allow users to override any setting by using the component template hierarchy (index template settings and mappings cannot be overridden by a component template) + +Other details to note about the index template: +- we set priority to 200, this is to beat the generic `logs-*-*`, `metrics-*-*`, `synthetics-*-*` index templates. We advise users set their own index template priority below 100 [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html). +- Fleet index templates are set to managed to deter users from editing them. However it is not necessarily safe to assume that Fleet index templates (or any managed asset) haven't been modified by the user, but if they have been modified we do not have to preserve these changes. +### Component Templates (as of 8.2) +In order of priority from highest to lowest: + - `.fleet_agent_id_verification-1` - added when agent id verification is enabled, sets the `.fleet_final_pipeline-1` and agent ID mappings. ([we plan to remove the ability to disable agent ID verification](https://github.com/elastic/kibana/issues/127041) ) + - `.fleet_globals-1` - contains fleet global settings and mappings, applied to every data stream + - `@custom` component template - empty, available as an escape hatch for user to apply custom settings + - `@package` component template - fleet default settings and mappings plus any settings and mappings defined by the integration. + +### `_meta` Fields + +All component and index templates have [_meta](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-meta-field.html) fields defined. This allows us to mark them up with: + +- package name - the package associated with the data stream +- managed - not editable by the user +- managed by - managed by the fleet plugin + +example: +```JSON +"_meta" : { + "package" : { + "name" : "system" + }, + "managed_by" : "fleet", + "managed" : true +}, +``` + +## Making Changes to Template Structure + +When making changes to the template structure (e.g [#124013](https://github.com/elastic/kibana/pull/124013)), this will need to be applied to all installed packages on upgrade to retain consistency. On startup we have [a check](https://github.com/elastic/kibana/blob/a52ba7cefe1a04ef6eafa32d5e410a3a901169b2/x-pack/plugins/fleet/server/services/setup.ts#L151) to see if any of the global assets have changed. If they have changed then we attempt to reinstall every package. This will in most cases cause a rollover of all datastreams so shouldn't be treated lightly. + + +## Pre 8.2 Template Structure + +Pre 8.2 the template structure was as follows (in order of precedence): + - index template - All package mappings (moved to @package component template), plus fleet default dynamic mappings (moved to .fleet_globals-1) + - `.fleet_component_template-1` - set agent ID verification if enabled (now moved to `.fleet_agent_id_verification-1`) + - `@custom` component template - empty, available for user to apply custom settings + - `@settings` component template - any custom settings specified by the package (e.g by specifying `elasticsearch.index_template.settings.some_setting` in manifest.yml ) + - `@mappings` component template - any custom mappings specified by the package (e.g by specifying `elasticsearch.index_template.mappings.some_mapping` in manifest.yml ) \ No newline at end of file From df8eb33fc2d80b8de77998f4e20e956d897bd121 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 30 Mar 2022 09:05:27 -0700 Subject: [PATCH 082/108] [DOCS] Get case status API (#128802) --- docs/api/cases.asciidoc | 3 +- docs/api/cases/cases-api-get-status.asciidoc | 60 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 docs/api/cases/cases-api-get-status.asciidoc diff --git a/docs/api/cases.asciidoc b/docs/api/cases.asciidoc index 88d4f4d668baa..ad0304ffa34b9 100644 --- a/docs/api/cases.asciidoc +++ b/docs/api/cases.asciidoc @@ -14,10 +14,10 @@ these APIs: * <> * {security-guide}/cases-api-get-case-activity.html[Get all case activity] * <> +* <> * <> * {security-guide}/cases-get-connector.html[Get current connector] * {security-guide}/cases-api-get-reporters.html[Get reporters] -* {security-guide}/cases-api-get-status.html[Get status] * {security-guide}/cases-api-get-tag.html[Get tags] * {security-guide}/cases-api-push.html[Push case] * {security-guide}/assign-connector.html[Set default Elastic Security UI connector] @@ -37,6 +37,7 @@ include::cases/cases-api-find-cases.asciidoc[leveloffset=+1] include::cases/cases-api-find-connectors.asciidoc[leveloffset=+1] //GET include::cases/cases-api-get-case.asciidoc[leveloffset=+1] +include::cases/cases-api-get-status.asciidoc[leveloffset=+1] include::cases/cases-api-get-comments.asciidoc[leveloffset=+1] //UPDATE include::cases/cases-api-update.asciidoc[leveloffset=+1] diff --git a/docs/api/cases/cases-api-get-status.asciidoc b/docs/api/cases/cases-api-get-status.asciidoc new file mode 100644 index 0000000000000..62a8181feba8e --- /dev/null +++ b/docs/api/cases/cases-api-get-status.asciidoc @@ -0,0 +1,60 @@ +[[cases-api-get-status]] +== Get case status API +++++ +Get case status +++++ + +Returns the number of cases that are open, closed, and in progress. + +deprecated::[8.1.0] + +=== Request + +`GET :/api/cases/status` + +`GET :/s//api/cases/status` + +=== Prerequisite + +You must have `read` privileges for the *Cases* feature in the *Management*, +*{observability}*, or *Security* section of the +<>, depending on the +`owner` of the cases you're seeking. + +=== Path parameters + +:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== Query parameters + +`owner`:: +(Optional, string or array of strings) A filter to limit the retrieved case +statistics to a specific set of applications. Valid values are: `cases`, +`observability`, and `securitySolution`. If this parameter is omitted, the +response contains all cases that the user has access to read. + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +[source,sh] +-------------------------------------------------- +GET api/cases/status +-------------------------------------------------- +// KIBANA + +The API returns the following type of information: + +[source,json] +-------------------------------------------------- +{ + "count_open_cases": 27, + "count_in_progress_cases": 50, + "count_closed_cases": 1198, +} +-------------------------------------------------- From 2d8ef46fb946a4bffee46f271f59d5c5047c511e Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 30 Mar 2022 12:32:19 -0400 Subject: [PATCH 083/108] [Fleet] Fix logstash config ssl_verification_mode (#128911) --- .../settings/components/logstash_instructions/helpers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/helpers.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/helpers.tsx index aecfe39c7e328..afb1919dcf03f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/helpers.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/helpers.tsx @@ -17,7 +17,7 @@ export function getLogstashPipeline(apiKey?: string) { ssl_certificate_authorities => [""] ssl_certificate => "" ssl_key => "" - ssl_verification_mode => "force-peer" + ssl_verify_mode => "force_peer" } } From 69b88670df652cce80a76784bfeb8477c9e6a3c7 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 30 Mar 2022 17:37:14 +0100 Subject: [PATCH 084/108] [ML] Fixing DFA map saved object sync warning (#128876) * [ML] Fixing DFA map saved object sync warning * updating pagination options --- .../pages/analytics_management/page.tsx | 6 +----- .../analytics_selector/analytics_id_selector.tsx | 4 ++-- .../data_frame_analytics/pages/job_map/job_map.tsx | 9 ++++++++- .../data_frame_analytics/pages/job_map/page.tsx | 7 ++----- .../components/jobs_list_view/jobs_list_view.js | 1 - .../trained_models/models_management/models_list.tsx | 6 +----- 6 files changed, 14 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 57904a206d281..26401c21af524 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -64,11 +64,7 @@ export const Page: FC = () => { - + {selectedTabId === 'map' && (mapJobId || mapModelId) && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector.tsx index 568971ba6d7e2..622da34f85545 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector.tsx @@ -184,8 +184,8 @@ export function AnalyticsIdSelector({ setAnalyticsId, jobsOnly = false }: Props) }, [selected?.model_id, selected?.job_id]); const pagination = { - initialPageSize: 5, - pageSizeOptions: [3, 5, 8], + initialPageSize: 20, + pageSizeOptions: [5, 10, 20, 50], }; const selectionValue = { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx index 64d235fb7e014..a2c51463cdc6e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx @@ -41,9 +41,10 @@ ${theme.euiColorLightShade}`, interface Props { analyticsId?: string; modelId?: string; + forceRefresh?: boolean; } -export const JobMap: FC = ({ analyticsId, modelId }) => { +export const JobMap: FC = ({ analyticsId, modelId, forceRefresh }) => { // itemsDeleted will reset to false when Controls component calls updateElements to remove nodes deleted from map const [itemsDeleted, setItemsDeleted] = useState(false); const [resetCyToggle, setResetCyToggle] = useState(false); @@ -111,6 +112,12 @@ export const JobMap: FC = ({ analyticsId, modelId }) => { fetchAndSetElementsWrapper({ analyticsId, modelId }); }, [analyticsId, modelId]); + useEffect(() => { + if (forceRefresh === true) { + fetchAndSetElementsWrapper({ analyticsId, modelId }); + } + }, [forceRefresh]); + useEffect(() => { if (message !== undefined) { notifications.toasts.add(message); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/page.tsx index 4f171d1108ad4..0ae82d72cecf2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/page.tsx @@ -125,17 +125,14 @@ export const Page: FC = () => { - + {mapJobId || mapModelId || analyticsId ? ( ) : ( getEmptyState() diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index e77f2d6c2aab9..7b5ea71a3b32b 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -474,7 +474,6 @@ export class JobsListView extends Component { diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx index 1604e265b1617..0d3b071d5063e 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx @@ -729,11 +729,7 @@ export const ModelsList: FC = ({ <> {isManagementTable ? null : ( <> - + )} From b080a4f4a47c556d0d2dd4e46be1223e29b1f238 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 30 Mar 2022 12:36:33 -0500 Subject: [PATCH 085/108] Remove gated Content plugin (#128939) --- .../enterprise_search/public/plugin.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 66767fe0384c7..5b193d3e80964 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -26,7 +26,6 @@ import { SecurityPluginSetup, SecurityPluginStart } from '../../security/public' import { APP_SEARCH_PLUGIN, - ENTERPRISE_SEARCH_CONTENT_PLUGIN, ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; @@ -90,32 +89,6 @@ export class EnterpriseSearchPlugin implements Plugin { }, }); - /* We are gating the Content plugin to develpers only until release */ - if (process.env.NODE_ENV === 'development') { - core.application.register({ - id: ENTERPRISE_SEARCH_CONTENT_PLUGIN.ID, - title: ENTERPRISE_SEARCH_CONTENT_PLUGIN.NAV_TITLE, - euiIconType: ENTERPRISE_SEARCH_CONTENT_PLUGIN.LOGO, - appRoute: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL, - category: DEFAULT_APP_CATEGORIES.enterpriseSearch, - mount: async (params: AppMountParameters) => { - const kibanaDeps = await this.getKibanaDeps(core, params, cloud); - const { chrome, http } = kibanaDeps.core; - chrome.docTitle.change(ENTERPRISE_SEARCH_CONTENT_PLUGIN.NAME); - - await this.getInitialData(http); - const pluginData = this.getPluginData(); - - const { renderApp } = await import('./applications'); - const { EnterpriseSearchContent } = await import( - './applications/enterprise_search_content' - ); - - return renderApp(EnterpriseSearchContent, kibanaDeps, pluginData); - }, - }); - } - core.application.register({ id: APP_SEARCH_PLUGIN.ID, title: APP_SEARCH_PLUGIN.NAME, From 5a17ada9d293af0fca78de2289af19185b1e9df8 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 30 Mar 2022 12:17:45 -0600 Subject: [PATCH 086/108] Move the control type tooltip to above the button (#128949) --- .../controls/public/control_group/editor/control_editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/controls/public/control_group/editor/control_editor.tsx b/src/plugins/controls/public/control_group/editor/control_editor.tsx index 1fb21aa9cf1bc..269c39a7cbf9e 100644 --- a/src/plugins/controls/public/control_group/editor/control_editor.tsx +++ b/src/plugins/controls/public/control_group/editor/control_editor.tsx @@ -127,7 +127,7 @@ export const ControlEditor = ({ ); return tooltip ? ( - + {menuPadItem} ) : ( From 33ed781ee4a9bf1aa7f2bd57a2874012602e333c Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Wed, 30 Mar 2022 19:36:24 +0100 Subject: [PATCH 087/108] Update filterlist for top-level alert fields + 'User Added to Privileged Group in Active Directory' (#128948) --- .../lib/telemetry/filterlists/index.test.ts | 16 +++ .../filterlists/prebuilt_rules_alerts.ts | 111 ++++++++---------- 2 files changed, 65 insertions(+), 62 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 d02c623bdb70e..7f3756c2971ef 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 @@ -16,6 +16,8 @@ describe('Security Telemetry filters', () => { c: { d: true, }, + 'kibana.alert.ancestors': true, + 'kibana.alert.original_event.module': true, }; it('filters top level', () => { @@ -126,5 +128,19 @@ describe('Security Telemetry filters', () => { b: 'b', }); }); + + it("copies long nested strings that shouldn't be broken up on customer deployments", () => { + const event = { + 'kibana.alert.ancestors': 'a', + 'kibana.alert.original_event.module': 'b', + 'kibana.random.long.alert.string': { + info: 'data', + }, + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + 'kibana.alert.ancestors': 'a', + 'kibana.alert.original_event.module': 'b', + }); + }); }); }); 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 e28ef55b4881b..e02e62417f63a 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 @@ -9,7 +9,54 @@ import type { AllowlistFields } from './types'; export const prebuiltRuleAllowlistFields: AllowlistFields = { _id: true, + id: true, '@timestamp': true, + // Base alert fields + 'kibana.alert.ancestors': true, + 'kibana.alert.depth': true, + 'kibana.alert.original_event.action': true, + 'kibana.alert.original_event.category': true, + 'kibana.alert.original_event.dataset': true, + 'kibana.alert.original_event.kind': true, + 'kibana.alert.original_event.module': true, + 'kibana.alert.original_event.type': true, + 'kibana.alert.original_time': true, + 'kibana.alert.reason': true, + 'kibana.alert.risk_score': true, + 'kibana.alert.rule.actions': true, + 'kibana.alert.rule.category': true, + 'kibana.alert.rule.consumer': true, + 'kibana.alert.rule.created_at': true, + 'kibana.alert.rule.description': true, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.exceptions_list': true, + 'kibana.alert.rule.execution.uuid': true, + 'kibana.alert.rule.false_positives': true, + 'kibana.alert.rule.from': true, + 'kibana.alert.rule.immutable': true, + 'kibana.alert.rule.interval': true, + 'kibana.alert.rule.name': true, + 'kibana.alert.rule.producer': true, + 'kibana.alert.rule.references': true, + 'kibana.alert.rule.risk_score_mapping': true, + 'kibana.alert.rule.rule_id': true, + 'kibana.alert.rule.rule_type_id': true, + 'kibana.alert.rule.severity': true, + 'kibana.alert.rule.severity_mapping': true, + 'kibana.alert.rule.tags': true, + 'kibana.alert.rule.threat': true, + 'kibana.alert.rule.timestamp_override': true, + 'kibana.alert.rule.type': true, + 'kibana.alert.rule.updated_at': true, + 'kibana.alert.rule.uuid': true, + 'kibana.alert.rule.version': true, + 'kibana.alert.severity': true, + 'kibana.alert.status': true, + 'kibana.alert.uuid': true, + 'kibana.alert.workflow_status': true, + 'kibana.space_ids': true, + 'kibana.version': true, + // Alert specific filter entries agent: { id: true, }, @@ -30,13 +77,7 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { group: { name: true, }, - host: { - id: true, - os: { - family: true, - name: true, - }, - }, + host: true, http: { request: { body: { @@ -120,30 +161,6 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { domain: true, id: true, }, - // Base alert fields - kibana: { - alert: { - ancestors: true, - depth: true, - original_time: true, - reason: true, - risk_score: true, - rule: { - enabled: true, - from: true, - interval: true, - max_signals: true, - name: true, - rule_id: true, - tags: true, - type: true, - uuid: true, - version: true, - severity: true, - workflow_status: true, - }, - }, - }, // aws rule fields aws: { cloudtrail: { @@ -257,37 +274,7 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { }, }, // winlog - winlog: { - event_data: { - AccessList: true, - AccessMask: true, - AllowedToDelegateTo: true, - AttributeLDAPDisplayName: true, - AttributeValue: true, - CallerProcessName: true, - CallTrace: true, - ClientProcessId: true, - GrantedAccess: true, - IntegrityLevel: true, - NewTargetUserName: true, - ObjectDN: true, - OldTargetUserName: true, - ParentProcessId: true, - PrivilegeList: true, - Properties: true, - RelativeTargetName: true, - ShareName: true, - SubjectLogonId: true, - SubjectUserName: true, - TargetImage: true, - TargetLogonId: true, - TargetProcessGUID: true, - TargetSid: true, - }, - logon: { - type: true, - }, - }, + winlog: true, // ml signal fields influencers: true, signal: { From e2e63c75be2f41fdd1d4f2379b50d825b3f1c591 Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Wed, 30 Mar 2022 20:39:36 +0200 Subject: [PATCH 088/108] define configuration to expose to the browser (#128938) --- x-pack/plugins/monitoring/server/index.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/server/index.ts b/x-pack/plugins/monitoring/server/index.ts index 44aaff7d51c4a..a8962b07ae419 100644 --- a/x-pack/plugins/monitoring/server/index.ts +++ b/x-pack/plugins/monitoring/server/index.ts @@ -20,7 +20,15 @@ export const config: PluginConfigDescriptor> = { schema: configSchema, deprecations, exposeToBrowser: { - ui: true, + ui: { + enabled: true, + min_interval_seconds: true, + show_license_expiration: true, + container: true, + ccs: { + enabled: true, + }, + }, kibana: true, }, }; From 1851e1bfcb3a526bb22d925df4bdf25c2419645a Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 30 Mar 2022 13:45:02 -0500 Subject: [PATCH 089/108] [RAM] Add Previous Snooze button (#128539) * Add Previous snooze button * Fix typo in i18n --- .../components/rule_status_dropdown.test.tsx | 1 + .../components/rule_status_dropdown.tsx | 151 ++++++++++++------ .../rules_list/components/rules_list.tsx | 9 +- 3 files changed, 109 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx index 4f7df21ee53e1..7873583131fdd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx @@ -22,6 +22,7 @@ describe('RuleStatusDropdown', () => { enableRule, snoozeRule, unsnoozeRule, + previousSnoozeInterval: null, item: { id: '1', name: 'test rule', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx index ff76abef65b60..38867b5d2fe6b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx @@ -27,6 +27,7 @@ import { EuiLink, EuiText, EuiToolTip, + EuiButtonEmpty, } from '@elastic/eui'; import { parseInterval } from '../../../../../common'; @@ -40,10 +41,18 @@ export interface ComponentOpts { onRuleChanged: () => void; enableRule: () => Promise; disableRule: () => Promise; - snoozeRule: (snoozeEndTime: string | -1) => Promise; + snoozeRule: (snoozeEndTime: string | -1, interval: string | null) => Promise; unsnoozeRule: () => Promise; + previousSnoozeInterval: string | null; } +const COMMON_SNOOZE_TIMES: Array<[number, SnoozeUnit]> = [ + [1, 'h'], + [3, 'h'], + [8, 'h'], + [1, 'd'], +]; + export const RuleStatusDropdown: React.FunctionComponent = ({ item, onRuleChanged, @@ -51,6 +60,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ enableRule, snoozeRule, unsnoozeRule, + previousSnoozeInterval, }: ComponentOpts) => { const [isEnabled, setIsEnabled] = useState(item.enabled); const [isSnoozed, setIsSnoozed] = useState(isItemSnoozed(item)); @@ -69,29 +79,35 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ const onChangeEnabledStatus = useCallback( async (enable: boolean) => { setIsUpdating(true); - if (enable) { - await enableRule(); - } else { - await disableRule(); + try { + if (enable) { + await enableRule(); + } else { + await disableRule(); + } + setIsEnabled(!isEnabled); + onRuleChanged(); + } finally { + setIsUpdating(false); } - setIsEnabled(!isEnabled); - onRuleChanged(); - setIsUpdating(false); }, [setIsUpdating, isEnabled, setIsEnabled, onRuleChanged, enableRule, disableRule] ); const onChangeSnooze = useCallback( async (value: number, unit?: SnoozeUnit) => { setIsUpdating(true); - if (value === -1) { - await snoozeRule(-1); - } else if (value !== 0) { - const snoozeEndTime = moment().add(value, unit).toISOString(); - await snoozeRule(snoozeEndTime); - } else await unsnoozeRule(); - setIsSnoozed(value !== 0); - onRuleChanged(); - setIsUpdating(false); + try { + if (value === -1) { + await snoozeRule(-1, null); + } else if (value !== 0) { + const snoozeEndTime = moment().add(value, unit).toISOString(); + await snoozeRule(snoozeEndTime, `${value}${unit}`); + } else await unsnoozeRule(); + setIsSnoozed(value !== 0); + onRuleChanged(); + } finally { + setIsUpdating(false); + } }, [setIsUpdating, setIsSnoozed, onRuleChanged, snoozeRule, unsnoozeRule] ); @@ -149,6 +165,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ isEnabled={isEnabled} isSnoozed={isSnoozed} snoozeEndTime={item.snoozeEndTime} + previousSnoozeInterval={previousSnoozeInterval} /> @@ -166,6 +183,7 @@ interface RuleStatusMenuProps { isEnabled: boolean; isSnoozed: boolean; snoozeEndTime?: Date | null; + previousSnoozeInterval: string | null; } const RuleStatusMenu: React.FunctionComponent = ({ @@ -175,6 +193,7 @@ const RuleStatusMenu: React.FunctionComponent = ({ isEnabled, isSnoozed, snoozeEndTime, + previousSnoozeInterval, }) => { const enableRule = useCallback(() => { if (isSnoozed) { @@ -242,6 +261,7 @@ const RuleStatusMenu: React.FunctionComponent = ({ applySnooze={onApplySnooze} interval={futureTimeToInterval(snoozeEndTime)} showCancel={isSnoozed} + previousSnoozeInterval={previousSnoozeInterval} /> ), }, @@ -254,12 +274,14 @@ interface SnoozePanelProps { interval?: string; applySnooze: (value: number | -1, unit?: SnoozeUnit) => void; showCancel: boolean; + previousSnoozeInterval: string | null; } const SnoozePanel: React.FunctionComponent = ({ interval = '3d', applySnooze, showCancel, + previousSnoozeInterval, }) => { const [intervalValue, setIntervalValue] = useState(parseInterval(interval).value); const [intervalUnit, setIntervalUnit] = useState(parseInterval(interval).unit); @@ -273,10 +295,6 @@ const SnoozePanel: React.FunctionComponent = ({ [setIntervalUnit] ); - const onApply1h = useCallback(() => applySnooze(1, 'h'), [applySnooze]); - const onApply3h = useCallback(() => applySnooze(3, 'h'), [applySnooze]); - const onApply8h = useCallback(() => applySnooze(8, 'h'), [applySnooze]); - const onApply1d = useCallback(() => applySnooze(1, 'd'), [applySnooze]); const onApplyIndefinite = useCallback(() => applySnooze(-1), [applySnooze]); const onClickApplyButton = useCallback( () => applySnooze(intervalValue, intervalUnit as SnoozeUnit), @@ -284,6 +302,33 @@ const SnoozePanel: React.FunctionComponent = ({ ); const onCancelSnooze = useCallback(() => applySnooze(0, 'm'), [applySnooze]); + const parsedPrevSnooze = previousSnoozeInterval ? parseInterval(previousSnoozeInterval) : null; + const prevSnoozeEqualsCurrentSnooze = + parsedPrevSnooze?.value === intervalValue && parsedPrevSnooze?.unit === intervalUnit; + const previousButton = parsedPrevSnooze && !prevSnoozeEqualsCurrentSnooze && ( + <> + + + applySnooze(parsedPrevSnooze.value, parsedPrevSnooze.unit as SnoozeUnit)} + > + {i18n.translate('xpack.triggersActionsUI.sections.rulesList.previousSnooze', { + defaultMessage: 'Previous', + })} + + + + + {durationToTextString(parsedPrevSnooze.value, parsedPrevSnooze.unit as SnoozeUnit)} + + + + + + ); + return ( @@ -325,6 +370,7 @@ const SnoozePanel: React.FunctionComponent = ({ + {previousButton} @@ -336,34 +382,13 @@ const SnoozePanel: React.FunctionComponent = ({ - - - {i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeOneHour', { - defaultMessage: '1 hour', - })} - - - - - {i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeThreeHours', { - defaultMessage: '3 hours', - })} - - - - - {i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeEightHours', { - defaultMessage: '8 hours', - })} - - - - - {i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeOneDay', { - defaultMessage: '1 day', - })} - - + {COMMON_SNOOZE_TIMES.map(([value, unit]) => ( + + applySnooze(value, unit)}> + {durationToTextString(value, unit)} + + + ))} @@ -435,6 +460,15 @@ const futureTimeToInterval = (time?: Date | null) => { return `${value}${unit}`; }; +const durationToTextString = (value: number, unit: SnoozeUnit) => { + // Moment.humanize will parse "1" as "a" or "an", e.g "an hour" + // Override this to output "1 hour" + if (value === 1) { + return ONE[unit]; + } + return moment.duration(value, unit).humanize(); +}; + const ENABLED = i18n.translate('xpack.triggersActionsUI.sections.rulesList.enabledRuleStatus', { defaultMessage: 'Enabled', }); @@ -478,3 +512,22 @@ const INDEFINITELY = i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.remainingSnoozeIndefinite', { defaultMessage: 'Indefinitely' } ); + +// i18n constants to override moment.humanize +const ONE: Record = { + m: i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeOneMinute', { + defaultMessage: '1 minute', + }), + h: i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeOneHour', { + defaultMessage: '1 hour', + }), + d: i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeOneDay', { + defaultMessage: '1 day', + }), + w: i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeOneWeek', { + defaultMessage: '1 week', + }), + M: i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeOneMonth', { + defaultMessage: '1 month', + }), +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index e3d14b51a6d6e..ba379046828b7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -160,6 +160,7 @@ export const RulesList: React.FunctionComponent = () => { const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1); + const [previousSnoozeInterval, setPreviousSnoozeInterval] = useState(null); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( {} ); @@ -352,12 +353,14 @@ export const RulesList: React.FunctionComponent = () => { await disableRule({ http, id: item.id })} enableRule={async () => await enableRule({ http, id: item.id })} - snoozeRule={async (snoozeEndTime: string | -1) => - await snoozeRule({ http, id: item.id, snoozeEndTime }) - } + snoozeRule={async (snoozeEndTime: string | -1, interval: string | null) => { + await snoozeRule({ http, id: item.id, snoozeEndTime }); + setPreviousSnoozeInterval(interval); + }} unsnoozeRule={async () => await unsnoozeRule({ http, id: item.id })} item={item} onRuleChanged={() => loadRulesData()} + previousSnoozeInterval={previousSnoozeInterval} /> ); }; From 14de3880a7ee1e6de00b08d88ebb3c93bb51cb9f Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Wed, 30 Mar 2022 14:53:13 -0400 Subject: [PATCH 090/108] Security Solution: Fix rule creation UI perf (#128953) --- .../__snapshots__/index.test.tsx.snap | 521 ++++++++++++++++ .../rules/step_define_rule/index.test.tsx | 7 + .../rules/step_define_rule/index.tsx | 41 +- .../step_define_rule/mock_browser_fields.json | 587 ++++++++++++++++++ 4 files changed, 1136 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/mock_browser_fields.json diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..80c1fc147e1e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/__snapshots__/index.test.tsx.snap @@ -0,0 +1,521 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`aggregatableFields 1`] = ` +Object { + "agent": Object { + "fields": Object { + "agent.ephemeral_id": Object { + "aggregatable": true, + "category": "agent", + "description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but \`agent.id\` does not.", + "example": "8a4f500f", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.ephemeral_id", + "searchable": true, + "type": "string", + }, + "agent.hostname": Object { + "aggregatable": true, + "category": "agent", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.hostname", + "searchable": true, + "type": "string", + }, + "agent.id": Object { + "aggregatable": true, + "category": "agent", + "description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.", + "example": "8a4f500d", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.id", + "searchable": true, + "type": "string", + }, + "agent.name": Object { + "aggregatable": true, + "category": "agent", + "description": "Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.", + "example": "foo", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.name", + "searchable": true, + "type": "string", + }, + }, + }, + "auditd": Object { + "fields": Object { + "auditd.data.a0": Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a0", + "searchable": true, + "type": "string", + }, + "auditd.data.a1": Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a1", + "searchable": true, + "type": "string", + }, + "auditd.data.a2": Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a2", + "searchable": true, + "type": "string", + }, + }, + }, + "base": Object { + "fields": Object { + "@timestamp": Object { + "aggregatable": true, + "category": "base", + "description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.", + "example": "2016-05-23T08:05:34.853Z", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "@timestamp", + "searchable": true, + "type": "date", + }, + }, + }, + "client": Object { + "fields": Object { + "client.address": Object { + "aggregatable": true, + "category": "client", + "description": "Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.address", + "searchable": true, + "type": "string", + }, + "client.bytes": Object { + "aggregatable": true, + "category": "client", + "description": "Bytes sent from the client to the server.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.bytes", + "searchable": true, + "type": "number", + }, + "client.domain": Object { + "aggregatable": true, + "category": "client", + "description": "Client domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.domain", + "searchable": true, + "type": "string", + }, + "client.geo.country_iso_code": Object { + "aggregatable": true, + "category": "client", + "description": "Country ISO code.", + "example": "CA", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.geo.country_iso_code", + "searchable": true, + "type": "string", + }, + }, + }, + "cloud": Object { + "fields": Object { + "cloud.account.id": Object { + "aggregatable": true, + "category": "cloud", + "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.", + "example": "666777888999", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.account.id", + "searchable": true, + "type": "string", + }, + "cloud.availability_zone": Object { + "aggregatable": true, + "category": "cloud", + "description": "Availability zone in which this host is running.", + "example": "us-east-1c", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.availability_zone", + "searchable": true, + "type": "string", + }, + }, + }, + "container": Object { + "fields": Object { + "container.id": Object { + "aggregatable": true, + "category": "container", + "description": "Unique container id.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.id", + "searchable": true, + "type": "string", + }, + "container.image.name": Object { + "aggregatable": true, + "category": "container", + "description": "Name of the image the container was built on.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.name", + "searchable": true, + "type": "string", + }, + "container.image.tag": Object { + "aggregatable": true, + "category": "container", + "description": "Container image tag.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.tag", + "searchable": true, + "type": "string", + }, + }, + }, + "destination": Object { + "fields": Object { + "destination.address": Object { + "aggregatable": true, + "category": "destination", + "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.address", + "searchable": true, + "type": "string", + }, + "destination.bytes": Object { + "aggregatable": true, + "category": "destination", + "description": "Bytes sent from the destination to the source.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.bytes", + "searchable": true, + "type": "number", + }, + "destination.domain": Object { + "aggregatable": true, + "category": "destination", + "description": "Destination domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.domain", + "searchable": true, + "type": "string", + }, + "destination.ip": Object { + "aggregatable": true, + "category": "destination", + "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.ip", + "searchable": true, + "type": "ip", + }, + "destination.port": Object { + "aggregatable": true, + "category": "destination", + "description": "Port of the destination.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.port", + "searchable": true, + "type": "long", + }, + }, + }, + "event": Object { + "fields": Object { + "event.action": Object { + "aggregatable": true, + "category": "event", + "description": "The action captured by the event. This describes the information in the event. It is more specific than \`event.category\`. Examples are \`group-add\`, \`process-started\`, \`file-created\`. The value is normally defined by the implementer.", + "example": "user-password-change", + "format": "string", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.action", + "searchable": true, + "type": "string", + }, + "event.category": Object { + "aggregatable": true, + "category": "event", + "description": "This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy. \`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. This field is an array. This will allow proper categorization of some events that fall in multiple categories.", + "example": "authentication", + "format": "string", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.category", + "searchable": true, + "type": "string", + }, + "event.end": Object { + "aggregatable": true, + "category": "event", + "description": "event.end contains the date when the event ended or when the activity was last observed.", + "example": null, + "format": "", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.end", + "searchable": true, + "type": "date", + }, + "event.severity": Object { + "aggregatable": true, + "category": "event", + "description": "The numeric severity of the event according to your event source. What the different severity values mean can be different between sources and use cases. It's up to the implementer to make sure severities are consistent across events from the same source. The Syslog severity belongs in \`log.syslog.severity.code\`. \`event.severity\` is meant to represent the severity according to the event source (e.g. firewall, IDS). If the event source does not publish its own severity, you may optionally copy the \`log.syslog.severity.code\` to \`event.severity\`.", + "example": 7, + "format": "number", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.severity", + "searchable": true, + "type": "number", + }, + }, + }, + "host": Object { + "fields": Object { + "host.name": Object { + "aggregatable": true, + "category": "host", + "description": "Name of the host. It 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.", + "format": "string", + "indexes": Array [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "host.name", + "searchable": true, + "type": "string", + }, + }, + }, + "nestedField": Object { + "fields": Object {}, + }, + "source": Object { + "fields": Object { + "source.ip": Object { + "aggregatable": true, + "category": "source", + "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.ip", + "searchable": true, + "type": "ip", + }, + "source.port": Object { + "aggregatable": true, + "category": "source", + "description": "Port of the source.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.port", + "searchable": true, + "type": "long", + }, + }, + }, + "user": Object { + "fields": Object { + "user.name": Object { + "aggregatable": true, + "category": "user", + "description": "Short name or login of the user.", + "example": "albert", + "format": "string", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "user.name", + "searchable": true, + "type": "string", + }, + }, + }, +} +`; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx index 7936c24e8635f..10d07d87b09fd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx @@ -9,9 +9,16 @@ import React from 'react'; import { shallow } from 'enzyme'; import { StepDefineRule } from './index'; +import mockBrowserFields from './mock_browser_fields.json'; + +import { aggregatableFields } from '.'; jest.mock('../../../../common/lib/kibana'); +test('aggregatableFields', function () { + expect(aggregatableFields(mockBrowserFields)).toMatchSnapshot(); +}); + describe('StepDefineRule', () => { it('renders correctly', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 7fbf5b74134a5..2113af02d0d06 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -191,24 +191,7 @@ const StepDefineRuleComponent: FC = ({ const anomalyThreshold = formAnomalyThreshold ?? initialState.anomalyThreshold; const ruleType = formRuleType || initialState.ruleType; const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(index); - const aggregatableFields = Object.entries(browserFields).reduce( - (groupAcc, [groupName, groupValue]) => { - return { - ...groupAcc, - [groupName]: { - fields: Object.entries(groupValue.fields ?? {}).reduce< - Record> - >((fieldAcc, [fieldName, fieldValue]) => { - if (fieldValue.aggregatable === true) { - fieldAcc[fieldName] = fieldValue; - } - return fieldAcc; - }, {}), - } as Partial, - }; - }, - {} - ); + const fields: Readonly = aggregatableFields(browserFields); const [ threatIndexPatternsLoading, @@ -307,14 +290,14 @@ const StepDefineRuleComponent: FC = ({ const ThresholdInputChildren = useCallback( ({ thresholdField, thresholdValue, thresholdCardinalityField, thresholdCardinalityValue }) => ( ), - [aggregatableFields] + [fields] ); const ThreatMatchInputChildren = useCallback( @@ -535,3 +518,21 @@ const StepDefineRuleComponent: FC = ({ }; export const StepDefineRule = memo(StepDefineRuleComponent); + +export function aggregatableFields(browserFields: BrowserFields): BrowserFields { + const result: Record> = {}; + for (const [groupName, groupValue] of Object.entries(browserFields)) { + const fields: Record> = {}; + if (groupValue.fields) { + for (const [fieldName, fieldValue] of Object.entries(groupValue.fields)) { + if (fieldValue.aggregatable === true) { + fields[fieldName] = fieldValue; + } + } + } + result[groupName] = { + fields, + }; + } + return result; +} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/mock_browser_fields.json b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/mock_browser_fields.json new file mode 100644 index 0000000000000..87f9b782d511e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/mock_browser_fields.json @@ -0,0 +1,587 @@ +{ + "agent": { + "fields": { + "agent.ephemeral_id": { + "aggregatable": true, + "category": "agent", + "description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.", + "example": "8a4f500f", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "agent.ephemeral_id", + "searchable": true, + "type": "string" + }, + "agent.hostname": { + "aggregatable": true, + "category": "agent", + "description": null, + "example": null, + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "agent.hostname", + "searchable": true, + "type": "string" + }, + "agent.id": { + "aggregatable": true, + "category": "agent", + "description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.", + "example": "8a4f500d", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "agent.id", + "searchable": true, + "type": "string" + }, + "agent.name": { + "aggregatable": true, + "category": "agent", + "description": "Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.", + "example": "foo", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "agent.name", + "searchable": true, + "type": "string" + } + } + }, + "auditd": { + "fields": { + "auditd.data.a0": { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": [ + "auditbeat" + ], + "name": "auditd.data.a0", + "searchable": true, + "type": "string" + }, + "auditd.data.a1": { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": [ + "auditbeat" + ], + "name": "auditd.data.a1", + "searchable": true, + "type": "string" + }, + "auditd.data.a2": { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": [ + "auditbeat" + ], + "name": "auditd.data.a2", + "searchable": true, + "type": "string" + } + } + }, + "base": { + "fields": { + "@timestamp": { + "aggregatable": true, + "category": "base", + "description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.", + "example": "2016-05-23T08:05:34.853Z", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "@timestamp", + "searchable": true, + "type": "date" + }, + "_id": { + "category": "base", + "description": "Each document has an _id that uniquely identifies it", + "example": "Y-6TfmcB0WOhS6qyMv3s", + "name": "_id", + "type": "string", + "searchable": true, + "aggregatable": false, + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ] + }, + "message": { + "category": "base", + "description": "For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.", + "example": "Hello World", + "name": "message", + "type": "string", + "searchable": true, + "aggregatable": false, + "format": "string", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ] + } + } + }, + "client": { + "fields": { + "client.address": { + "aggregatable": true, + "category": "client", + "description": "Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.", + "example": null, + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "client.address", + "searchable": true, + "type": "string" + }, + "client.bytes": { + "aggregatable": true, + "category": "client", + "description": "Bytes sent from the client to the server.", + "example": "184", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "client.bytes", + "searchable": true, + "type": "number" + }, + "client.domain": { + "aggregatable": true, + "category": "client", + "description": "Client domain.", + "example": null, + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "client.domain", + "searchable": true, + "type": "string" + }, + "client.geo.country_iso_code": { + "aggregatable": true, + "category": "client", + "description": "Country ISO code.", + "example": "CA", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "client.geo.country_iso_code", + "searchable": true, + "type": "string" + } + } + }, + "cloud": { + "fields": { + "cloud.account.id": { + "aggregatable": true, + "category": "cloud", + "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.", + "example": "666777888999", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "cloud.account.id", + "searchable": true, + "type": "string" + }, + "cloud.availability_zone": { + "aggregatable": true, + "category": "cloud", + "description": "Availability zone in which this host is running.", + "example": "us-east-1c", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "cloud.availability_zone", + "searchable": true, + "type": "string" + } + } + }, + "container": { + "fields": { + "container.id": { + "aggregatable": true, + "category": "container", + "description": "Unique container id.", + "example": null, + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "container.id", + "searchable": true, + "type": "string" + }, + "container.image.name": { + "aggregatable": true, + "category": "container", + "description": "Name of the image the container was built on.", + "example": null, + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "container.image.name", + "searchable": true, + "type": "string" + }, + "container.image.tag": { + "aggregatable": true, + "category": "container", + "description": "Container image tag.", + "example": null, + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "container.image.tag", + "searchable": true, + "type": "string" + } + } + }, + "destination": { + "fields": { + "destination.address": { + "aggregatable": true, + "category": "destination", + "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.", + "example": null, + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "destination.address", + "searchable": true, + "type": "string" + }, + "destination.bytes": { + "aggregatable": true, + "category": "destination", + "description": "Bytes sent from the destination to the source.", + "example": "184", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "destination.bytes", + "searchable": true, + "type": "number" + }, + "destination.domain": { + "aggregatable": true, + "category": "destination", + "description": "Destination domain.", + "example": null, + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "destination.domain", + "searchable": true, + "type": "string" + }, + "destination.ip": { + "aggregatable": true, + "category": "destination", + "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "destination.ip", + "searchable": true, + "type": "ip" + }, + "destination.port": { + "aggregatable": true, + "category": "destination", + "description": "Port of the destination.", + "example": "", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "destination.port", + "searchable": true, + "type": "long" + } + } + }, + "event": { + "fields": { + "event.end": { + "category": "event", + "description": "event.end contains the date when the event ended or when the activity was last observed.", + "example": null, + "format": "", + "indexes": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "name": "event.end", + "searchable": true, + "type": "date", + "aggregatable": true + }, + "event.action": { + "category": "event", + "description": "The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.", + "example": "user-password-change", + "name": "event.action", + "type": "string", + "searchable": true, + "aggregatable": true, + "format": "string", + "indexes": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ] + }, + "event.category": { + "category": "event", + "description": "This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy. `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. This field is an array. This will allow proper categorization of some events that fall in multiple categories.", + "example": "authentication", + "name": "event.category", + "type": "string", + "searchable": true, + "aggregatable": true, + "format": "string", + "indexes": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ] + }, + "event.severity": { + "category": "event", + "description": "The numeric severity of the event according to your event source. What the different severity values mean can be different between sources and use cases. It's up to the implementer to make sure severities are consistent across events from the same source. The Syslog severity belongs in `log.syslog.severity.code`. `event.severity` is meant to represent the severity according to the event source (e.g. firewall, IDS). If the event source does not publish its own severity, you may optionally copy the `log.syslog.severity.code` to `event.severity`.", + "example": 7, + "name": "event.severity", + "type": "number", + "format": "number", + "searchable": true, + "aggregatable": true, + "indexes": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ] + } + } + }, + "host": { + "fields": { + "host.name": { + "category": "host", + "description": "Name of the host. It 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.", + "name": "host.name", + "type": "string", + "searchable": true, + "aggregatable": true, + "format": "string", + "indexes": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ] + } + } + }, + "source": { + "fields": { + "source.ip": { + "aggregatable": true, + "category": "source", + "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "source.ip", + "searchable": true, + "type": "ip" + }, + "source.port": { + "aggregatable": true, + "category": "source", + "description": "Port of the source.", + "example": "", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "source.port", + "searchable": true, + "type": "long" + } + } + }, + "user": { + "fields": { + "user.name": { + "category": "user", + "description": "Short name or login of the user.", + "example": "albert", + "name": "user.name", + "type": "string", + "searchable": true, + "aggregatable": true, + "format": "string", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ] + } + } + }, + "nestedField": { + "fields": { + "nestedField.firstAttributes": { + "aggregatable": false, + "category": "nestedField", + "description": "", + "example": "", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "nestedField.firstAttributes", + "searchable": true, + "type": "string", + "subType": { + "nested": { + "path": "nestedField" + } + } + }, + "nestedField.secondAttributes": { + "aggregatable": false, + "category": "nestedField", + "description": "", + "example": "", + "format": "", + "indexes": [ + "auditbeat", + "filebeat", + "packetbeat" + ], + "name": "nestedField.secondAttributes", + "searchable": true, + "type": "string", + "subType": { + "nested": { + "path": "nestedField" + } + } + } + } + } +} From bb359029311ca37e19d49cebada27ad289466277 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 30 Mar 2022 12:58:14 -0600 Subject: [PATCH 091/108] skip failing suites (#128968) (#128967) --- .../functional/apps/monitoring/logstash/pipeline_viewer_mb.js | 3 ++- x-pack/test/functional/apps/transform/cloning.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer_mb.js b/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer_mb.js index bb94e49e34b11..35687ed113f5e 100644 --- a/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer_mb.js +++ b/x-pack/test/functional/apps/monitoring/logstash/pipeline_viewer_mb.js @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }) { const pipelinesList = getService('monitoringLogstashPipelines'); const pipelineViewer = getService('monitoringLogstashPipelineViewer'); - describe('Logstash pipeline viewer mb', () => { + // FAILING: https://github.com/elastic/kibana/issues/128968 + describe.skip('Logstash pipeline viewer mb', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); before(async () => { diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index 3cbb0892bd4ec..9d3ce49803d28 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -85,7 +85,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - describe('cloning', function () { + // FAILING: https://github.com/elastic/kibana/issues/128967 + describe.skip('cloning', function () { const transformConfigWithPivot = getTransformConfig(); const transformConfigWithRuntimeMapping = getTransformConfigWithRuntimeMappings(); const transformConfigWithLatest = getLatestTransformConfig('cloning'); From 4c4cba7cb604c4f22eeb5703b13e57c63f8773c6 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 30 Mar 2022 13:49:24 -0600 Subject: [PATCH 092/108] skip full transform suite, failures are spreading (#109687) (#128967) --- x-pack/test/functional/apps/transform/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index b716f5ecdc1b7..5f5f35ce1c2f8 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -15,7 +15,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - describe('transform', function () { + // FAILING TEST: https://github.com/elastic/kibana/issues/109687 + describe.skip('transform', function () { this.tags(['ciGroup21', 'transform']); before(async () => { From b41e83c2f7214b6fcc40d31c239aae449bb5be2d Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Wed, 30 Mar 2022 22:58:06 +0200 Subject: [PATCH 093/108] Update dependency @elastic/charts to v45.1.1 (main) (#128870) * Update elastic-charts to 45.1.0 * fix snapshot testing * Update to 45.1.1 * Merged core-js@^3.8.3 into resolved 3.21.1 --- package.json | 2 +- .../gauge_component.test.tsx.snap | 2 +- .../__snapshots__/donut_chart.test.tsx.snap | 11 +++++++ yarn.lock | 32 +++---------------- 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 735f388881999..8552d400e412c 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "@elastic/apm-rum": "^5.10.2", "@elastic/apm-rum-react": "^1.3.4", "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", - "@elastic/charts": "45.0.1", + "@elastic/charts": "45.1.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.1", "@elastic/ems-client": "8.2.0", diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap b/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap index 59aaa3677e9bc..49b102c82c312 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap @@ -18,7 +18,7 @@ exports[`GaugeComponent renders the chart 1`] = ` ] } /> - Date: Wed, 30 Mar 2022 17:19:34 -0400 Subject: [PATCH 094/108] add link to march newsletter (#128777) --- nav-kibana-dev.docnav.json | 1 + 1 file changed, 1 insertion(+) diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index 43ca1ed4bf813..96d7dec7e430a 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -68,6 +68,7 @@ { "label": "Contributors Newsletters", "items": [ + { "id": "kibMarch2022ContributorNewsletter" }, { "id": "kibFebruary2022ContributorNewsletter" }, { "id": "kibJanuary2022ContributorNewsletter" }, { "id": "kibDecember2021ContributorNewsletter" }, From 981004539e3b0b3490684c307781c0ea7c165223 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 30 Mar 2022 15:29:42 -0600 Subject: [PATCH 095/108] [Dashboard] [Controls] Fix mobile view of toolbar and controls callout (#128771) * Fix wrap of toolbar and controls callout * remove class Co-authored-by: andreadelrio --- .../public/control_group/control_group.scss | 47 ++++--------------- .../controls_callout/controls_callout.scss | 36 ++++++++++++++ .../controls_callout/controls_callout.tsx | 25 +++++----- .../controls_illustration.scss | 6 --- .../controls_illustration.tsx | 1 - .../solution_toolbar/solution_toolbar.tsx | 2 +- 6 files changed, 58 insertions(+), 59 deletions(-) create mode 100644 src/plugins/controls/public/controls_callout/controls_callout.scss delete mode 100644 src/plugins/controls/public/controls_callout/controls_illustration.scss diff --git a/src/plugins/controls/public/control_group/control_group.scss b/src/plugins/controls/public/control_group/control_group.scss index bd8974a4b7b06..6f185e9f992ab 100644 --- a/src/plugins/controls/public/control_group/control_group.scss +++ b/src/plugins/controls/public/control_group/control_group.scss @@ -7,36 +7,9 @@ $controlMinWidth: $euiSize * 14; min-height: $euiSize * 4; } -.controlsWrapper { - &--empty { - display: flex; - @include euiBreakpoint('m', 'l', 'xl') { - .addControlButton { - text-align: center; - } - .emptyStateText { - padding-left: $euiSize * 2; - } - height: $euiSize * 4; - overflow: hidden; - } - @include euiBreakpoint('xs', 's') { - .addControlButton { - text-align: center; - } - .emptyStateText { - text-align: center; - } - .controlsIllustration__container { - margin-bottom: 0 !important; - } - } - } - - &--twoLine { - .groupEditActions { - padding-top: $euiSize; - } +.controlsWrapper--twoLine { + .groupEditActions { + padding-top: $euiSize; } } @@ -75,7 +48,8 @@ $controlMinWidth: $euiSize * 14; @include euiFontSizeXS; } - .controlFrame__formControlLayout, .controlFrame__draggable { + .controlFrame__formControlLayout, + .controlFrame__draggable { .controlFrame__dragHandle { cursor: grabbing; } @@ -105,7 +79,7 @@ $controlMinWidth: $euiSize * 14; .controlFrame__formControlLayout { width: 100%; min-width: $controlMinWidth; - transition:background-color .1s, color .1s; + transition: background-color .1s, color .1s; &Label { @include euiTextTruncate; @@ -163,7 +137,6 @@ $controlMinWidth: $euiSize * 14; &--insertBefore { .controlFrame__formControlLayout:after { left: -$euiSizeXS - 1; - } } @@ -184,7 +157,7 @@ $controlMinWidth: $euiSize * 14; position: absolute; &--oneLine { - right:$euiSizeXS; + right: $euiSizeXS; top: -$euiSizeL; padding: $euiSizeXS; border-radius: $euiBorderRadius; @@ -193,14 +166,14 @@ $controlMinWidth: $euiSize * 14; } &--twoLine { - right:$euiSizeXS; + right: $euiSizeXS; top: -$euiSizeXS; } } &:hover { .controlFrameFloatingActions { - transition:visibility .1s, opacity .1s; + transition: visibility .1s, opacity .1s; visibility: visible; opacity: 1; } @@ -224,4 +197,4 @@ $controlMinWidth: $euiSize * 14; } } } -} \ No newline at end of file +} diff --git a/src/plugins/controls/public/controls_callout/controls_callout.scss b/src/plugins/controls/public/controls_callout/controls_callout.scss new file mode 100644 index 0000000000000..e0f7e1481d156 --- /dev/null +++ b/src/plugins/controls/public/controls_callout/controls_callout.scss @@ -0,0 +1,36 @@ +@include euiBreakpoint('xs', 's') { + .controlsIllustration { + display: none; + } +} + +.controlsWrapper { + &--empty { + display: flex; + overflow: hidden; + margin: 0 $euiSizeS 0 $euiSizeS; + + .addControlButton { + text-align: center; + } + + @include euiBreakpoint('m', 'l', 'xl') { + height: $euiSize * 4; + + .emptyStateText { + padding-left: $euiSize * 2; + } + } + @include euiBreakpoint('xs', 's') { + min-height: $euiSize * 4; + + .emptyStateText { + padding-left: 0; + text-align: center; + } + .controlsIllustration__container { + margin-bottom: 0 !important; + } + } + } +} diff --git a/src/plugins/controls/public/controls_callout/controls_callout.tsx b/src/plugins/controls/public/controls_callout/controls_callout.tsx index 096d47b470a9d..708b224187e1c 100644 --- a/src/plugins/controls/public/controls_callout/controls_callout.tsx +++ b/src/plugins/controls/public/controls_callout/controls_callout.tsx @@ -9,8 +9,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonEmpty, EuiPanel } from '@elastic/eui'; import React from 'react'; import useLocalStorage from 'react-use/lib/useLocalStorage'; -import classNames from 'classnames'; +import './controls_callout.scss'; import { ControlGroupStrings } from '../control_group/control_group_strings'; import { ControlsIllustration } from './controls_illustration'; @@ -32,15 +32,10 @@ export const ControlsCallout = ({ getCreateControlButton }: CalloutProps) => { if (controlsCalloutDismissed) return null; return ( - + - + @@ -49,13 +44,15 @@ export const ControlsCallout = ({ getCreateControlButton }: CalloutProps) => {

{ControlGroupStrings.emptyState.getCallToAction()}

- {getCreateControlButton ? ( - {getCreateControlButton()} - ) : null} - - {ControlGroupStrings.emptyState.getDismissButton()} - + + {getCreateControlButton && {getCreateControlButton()}} + + + {ControlGroupStrings.emptyState.getDismissButton()} + + +
diff --git a/src/plugins/controls/public/controls_callout/controls_illustration.scss b/src/plugins/controls/public/controls_callout/controls_illustration.scss deleted file mode 100644 index 589a584add493..0000000000000 --- a/src/plugins/controls/public/controls_callout/controls_illustration.scss +++ /dev/null @@ -1,6 +0,0 @@ -@include euiBreakpoint('xs', 's') { - .controlsIllustration { - width: $euiSize * 6; - height: $euiSize * 6; - } -} diff --git a/src/plugins/controls/public/controls_callout/controls_illustration.tsx b/src/plugins/controls/public/controls_callout/controls_illustration.tsx index 4b285ffcf17a8..925dd90fc8700 100644 --- a/src/plugins/controls/public/controls_callout/controls_illustration.tsx +++ b/src/plugins/controls/public/controls_callout/controls_illustration.tsx @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import './controls_illustration.scss'; import React from 'react'; export const ControlsIllustration = () => ( diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx index 141a5c16d7d95..219c582f26a3a 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx @@ -53,7 +53,7 @@ export const SolutionToolbar = ({ isDarkModeEnabled, children }: Props) => { > {primaryActionButton} - + {quickButtonGroup ? {quickButtonGroup} : null} {extra} From 79d49a36d57fe7779bd28b450f7936a0b49df4db Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Wed, 30 Mar 2022 17:37:44 -0400 Subject: [PATCH 096/108] adjust synthetics remote functional tests (#128978) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/scripts/steps/functional/synthetics.sh | 2 +- .../plugins/uptime/e2e/journeys/monitor_management.journey.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.buildkite/scripts/steps/functional/synthetics.sh b/.buildkite/scripts/steps/functional/synthetics.sh index 76d355d99c2e3..ecb2922f89c8d 100644 --- a/.buildkite/scripts/steps/functional/synthetics.sh +++ b/.buildkite/scripts/steps/functional/synthetics.sh @@ -14,4 +14,4 @@ echo "--- Uptime @elastic/synthetics Tests" cd "$XPACK_DIR" checks-reporter-with-killswitch "Uptime @elastic/synthetics Tests" \ - node plugins/uptime/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" --grep "MonitorManagement*" \ No newline at end of file + node plugins/uptime/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" --grep "MonitorManagement-monitor*" \ No newline at end of file diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts index 7dfc7e4e6ab66..0050f8635e35f 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts @@ -92,7 +92,7 @@ const createMonitorJourney = ({ monitorDetails: Record; }) => { journey( - `MonitorManagement-${monitorType}`, + `MonitorManagement-monitor-${monitorType}`, async ({ page, params }: { page: Page; params: any }) => { const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl }); const isRemote = process.env.SYNTHETICS_REMOTE_ENABLED; From 742d09bbb6dd5d2223db96e64ab416b06d673982 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 30 Mar 2022 14:39:20 -0700 Subject: [PATCH 097/108] Revert "[ci/es_snapshots] Build cloud image (#127154)" This reverts commit b8a03f980634e1ed00cdabd1bd211e611372bb75. --- .buildkite/scripts/steps/es_snapshots/build.sh | 18 +----------------- .../steps/es_snapshots/create_manifest.js | 13 ------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/.buildkite/scripts/steps/es_snapshots/build.sh b/.buildkite/scripts/steps/es_snapshots/build.sh index cdc1750e59bfc..c11f041836413 100755 --- a/.buildkite/scripts/steps/es_snapshots/build.sh +++ b/.buildkite/scripts/steps/es_snapshots/build.sh @@ -69,7 +69,6 @@ echo "--- Build Elasticsearch" :distribution:archives:darwin-aarch64-tar:assemble \ :distribution:archives:darwin-tar:assemble \ :distribution:docker:docker-export:assemble \ - :distribution:docker:cloud-docker-export:assemble \ :distribution:archives:linux-aarch64-tar:assemble \ :distribution:archives:linux-tar:assemble \ :distribution:archives:windows-zip:assemble \ @@ -80,26 +79,11 @@ find distribution -type f \( -name 'elasticsearch-*-*-*-*.tar.gz' -o -name 'elas ls -alh "$destination" -echo "--- Create docker default image archives" +echo "--- Create docker image archives" docker images "docker.elastic.co/elasticsearch/elasticsearch" docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 echo 'docker save docker.elastic.co/elasticsearch/elasticsearch:${0} | gzip > ../es-build/elasticsearch-${0}-docker-image.tar.gz' docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 bash -c 'docker save docker.elastic.co/elasticsearch/elasticsearch:${0} | gzip > ../es-build/elasticsearch-${0}-docker-image.tar.gz' -echo "--- Create kibana-ci docker cloud image archives" -ES_CLOUD_ID=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.ID}}") -ES_CLOUD_VERSION=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.Tag}}") -KIBANA_ES_CLOUD_VERSION="$ES_CLOUD_VERSION-$ELASTICSEARCH_GIT_COMMIT" -KIBANA_ES_CLOUD_IMAGE="docker.elastic.co/kibana-ci/elasticsearch-cloud:$KIBANA_ES_CLOUD_VERSION" - -docker tag "$ES_CLOUD_ID" "$KIBANA_ES_CLOUD_IMAGE" - -echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co -trap 'docker logout docker.elastic.co' EXIT -docker image push "$KIBANA_ES_CLOUD_IMAGE" - -export ELASTICSEARCH_CLOUD_IMAGE="$KIBANA_ES_CLOUD_IMAGE" -export ELASTICSEARCH_CLOUD_IMAGE_CHECKSUM="$(docker images "$KIBANA_ES_CLOUD_IMAGE" --format "{{.Digest}}")" - echo "--- Create checksums for snapshot files" cd "$destination" find ./* -exec bash -c "shasum -a 512 {} > {}.sha512" \; diff --git a/.buildkite/scripts/steps/es_snapshots/create_manifest.js b/.buildkite/scripts/steps/es_snapshots/create_manifest.js index 9357cd72fff06..cb4ea29a9c534 100644 --- a/.buildkite/scripts/steps/es_snapshots/create_manifest.js +++ b/.buildkite/scripts/steps/es_snapshots/create_manifest.js @@ -16,8 +16,6 @@ const { BASE_BUCKET_DAILY } = require('./bucket_config.js'); const destination = process.argv[2] || __dirname + '/test'; const ES_BRANCH = process.env.ELASTICSEARCH_BRANCH; - const ES_CLOUD_IMAGE = process.env.ELASTICSEARCH_CLOUD_IMAGE; - const ES_CLOUD_IMAGE_CHECKSUM = process.env.ELASTICSEARCH_CLOUD_IMAGE_CHECKSUM; const GIT_COMMIT = process.env.ELASTICSEARCH_GIT_COMMIT; const GIT_COMMIT_SHORT = process.env.ELASTICSEARCH_GIT_COMMIT_SHORT; @@ -61,17 +59,6 @@ const { BASE_BUCKET_DAILY } = require('./bucket_config.js'); }; }); - if (ES_CLOUD_IMAGE && ES_CLOUD_IMAGE_CHECKSUM) { - manifestEntries.push({ - checksum: ES_CLOUD_IMAGE_CHECKSUM, - url: ES_CLOUD_IMAGE, - version: VERSION, - platform: 'docker', - architecture: 'image', - license: 'default', - }); - } - const manifest = { id: SNAPSHOT_ID, bucket: `${BASE_BUCKET_DAILY}/${DESTINATION}`.toString(), From c6e9b7aefbe77703c971e25bc7f78eb1f3537473 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Wed, 30 Mar 2022 15:23:53 -0700 Subject: [PATCH 098/108] [Controls] Fix cut off range slider popover (#128855) * Fix cut off popover for right most range control * Reduce control label max-width and restore small control size * Disable responsive flex groups in range slider control * Update options list icon * Updated time slider icon --- .../controls/public/control_group/control_group.scss | 2 +- .../options_list/options_list_embeddable_factory.tsx | 2 +- .../public/control_types/range_slider/range_slider.scss | 1 + .../control_types/range_slider/range_slider_popover.tsx | 6 +++--- .../time_slider/time_slider_embeddable_factory.tsx | 1 + 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/plugins/controls/public/control_group/control_group.scss b/src/plugins/controls/public/control_group/control_group.scss index 6f185e9f992ab..efcb3d7af810a 100644 --- a/src/plugins/controls/public/control_group/control_group.scss +++ b/src/plugins/controls/public/control_group/control_group.scss @@ -57,7 +57,7 @@ $controlMinWidth: $euiSize * 14; } .controlFrame__labelToolTip { - max-width: 50%; + max-width: 40%; } .controlFrameWrapper { diff --git a/src/plugins/controls/public/control_types/options_list/options_list_embeddable_factory.tsx b/src/plugins/controls/public/control_types/options_list/options_list_embeddable_factory.tsx index 8c6b533fa06e9..9548c45cadd4e 100644 --- a/src/plugins/controls/public/control_types/options_list/options_list_embeddable_factory.tsx +++ b/src/plugins/controls/public/control_types/options_list/options_list_embeddable_factory.tsx @@ -51,7 +51,7 @@ export class OptionsListEmbeddableFactory public isEditable = () => Promise.resolve(false); public getDisplayName = () => OptionsListStrings.getDisplayName(); - public getIconType = () => 'list'; + public getIconType = () => 'editorChecklist'; public getDescription = () => OptionsListStrings.getDescription(); public inject = createOptionsListInject(); diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider.scss b/src/plugins/controls/public/control_types/range_slider/range_slider.scss index 82d892cd0b9c5..d1a360b465962 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider.scss +++ b/src/plugins/controls/public/control_types/range_slider/range_slider.scss @@ -31,6 +31,7 @@ .rangeSliderAnchor__delimiter { background-color: unset; + padding: $euiSizeS*1.5 0; } .rangeSliderAnchor__fieldNumber { font-weight: $euiFontWeightBold; diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx index a4ed84ec01a2e..a51b46d98ff85 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx @@ -110,7 +110,7 @@ export const RangeSliderPopover: FC = ({ className="rangeSliderAnchor__button" data-test-subj={`range-slider-control-${id}`} > - + = ({ panelClassName="rangeSlider__panelOverride" closePopover={() => setIsPopoverOpen(false)} anchorPosition="downCenter" - initialFocus={false} - repositionOnScroll + attachToAnchor={false} disableFocusTrap onPanelResize={() => { if (rangeRef?.current) { @@ -192,6 +191,7 @@ export const RangeSliderPopover: FC = ({ className="rangeSlider__actions" gutterSize="none" data-test-subj="rangeSlider-control-actions" + responsive={false} > Promise.resolve(false); public getDisplayName = () => TimeSliderStrings.getDisplayName(); + public getIconType = () => 'clock'; public getDescription = () => TimeSliderStrings.getDescription(); public inject = createOptionsListInject(); From 9dbbff1365b8e040bd1231e040f09e7d8c59ba78 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 30 Mar 2022 16:39:56 -0600 Subject: [PATCH 099/108] [ML][Maps] Anomaly Detection: ensure maps link only created when geo type chart (#128945) * only get mapsLink for geo charts * check component is mounted before updating state Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../explorer_charts/explorer_charts_container.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 79a1121a98a62..13800536f2fae 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -198,20 +198,21 @@ function ExplorerChartContainer({ useEffect( function getMapsPluginLink() { - if (!series) return; let isCancelled = false; - const generateLink = async () => { - if (!isCancelled) { + if (series && getChartType(series) === CHART_TYPE.GEO_MAP) { + const generateLink = async () => { try { const mapsLink = await getMapsLink(); - setMapsLink(mapsLink?.path); + if (!isCancelled) { + setMapsLink(mapsLink?.path); + } } catch (error) { console.error(error); setMapsLink(''); } - } - }; - generateLink().catch(console.error); + }; + generateLink().catch(console.error); + } return () => { isCancelled = true; }; From f634334be8c4dbb260e8b43e8d5f53fe73677902 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 30 Mar 2022 23:46:04 +0100 Subject: [PATCH 100/108] chore(NA): adds backport config for 8.3.0 bump (#128895) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .backportrc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.backportrc.json b/.backportrc.json index eab70a1fa4de1..8a52d4266ca8b 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "repoName": "kibana", "targetBranchChoices": [ "main", + "8.2", "8.1", "8.0", "7.17", @@ -38,7 +39,7 @@ "backport" ], "branchLabelMapping": { - "^v8.2.0$": "main", + "^v8.3.0$": "main", "^v(\\d+).(\\d+).\\d+$": "$1.$2" }, "autoMerge": true, From f6b392848956c3c630133c22fb513ff08942cf4e Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 30 Mar 2022 15:56:02 -0700 Subject: [PATCH 101/108] [Reporting] Add queue duration metric to event logging (#128325) * [Reporting] Add queue duration metric to event logging * fix export needed for return type of public method * rename metric property with ms suffix Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../reporting/server/lib/event_logger/logger.test.ts | 3 ++- .../plugins/reporting/server/lib/event_logger/logger.ts | 9 ++++++--- .../plugins/reporting/server/lib/event_logger/types.ts | 3 +++ x-pack/plugins/reporting/server/lib/store/store.ts | 6 +++++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts index c58777747c3fd..b389dd715f616 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts @@ -171,10 +171,11 @@ describe('Event Logger', () => { it(`logClaimTask`, () => { const logger = new factory(mockReport); - const result = logger.logClaimTask(); + const result = logger.logClaimTask({ queueDurationMs: 5500 }); expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(` Array [ Object { + "duration": 5500, "timezone": "UTC", }, Object { diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts index 965a55e24229a..82a089192b2fb 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts @@ -24,7 +24,10 @@ import { StartedExecution, } from './types'; -/** @internal */ +export interface ExecutionClaimMetrics extends TaskRunMetrics { + queueDurationMs: number; +} + export interface ExecutionCompleteMetrics extends TaskRunMetrics { byteSize: number; } @@ -44,7 +47,6 @@ export interface BaseEvent { user?: { name: string }; } -/** @internal */ export function reportingEventLoggerFactory(logger: Logger) { const genericLogger = new EcsLogAdapter(logger, { event: { provider: PLUGIN_ID } }); @@ -145,12 +147,13 @@ export function reportingEventLoggerFactory(logger: Logger) { return event; } - logClaimTask(): ClaimedTask { + logClaimTask({ queueDurationMs }: ExecutionClaimMetrics): ClaimedTask { const message = `claimed report ${this.report._id}`; const event = deepMerge( { message, kibana: { reporting: { actionType: ActionType.CLAIM_TASK } }, + event: { duration: queueDurationMs }, } as Partial, this.eventObj ); diff --git a/x-pack/plugins/reporting/server/lib/event_logger/types.ts b/x-pack/plugins/reporting/server/lib/event_logger/types.ts index 3094919da278d..950c3d89a184b 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/types.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/types.ts @@ -12,6 +12,9 @@ import { ActionType } from './'; export interface ReportingAction extends LogMeta { event: { timezone: string; + // Within ReportingEventLogger, duration is auto-calculated for "completion" event, manually calculated for + // "claimed" event. + duration?: number; }; message: string; kibana: { diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 7e920e718d51e..ffb4ba96bfd3c 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -5,6 +5,7 @@ * 2.0. */ +import moment from 'moment'; import { IndexResponse, UpdateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient, Logger } from 'kibana/server'; import { statuses } from '../'; @@ -296,7 +297,10 @@ export class ReportingStore { throw err; } - this.reportingCore.getEventLogger(report).logClaimTask(); + // log the amount of time the report waited in "pending" status + this.reportingCore.getEventLogger(report).logClaimTask({ + queueDurationMs: moment.utc().valueOf() - moment.utc(report.created_at).valueOf(), + }); return body; } From 1823ff7b6eb3fcacbd81ad9c5165fbd7e07a44c3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 31 Mar 2022 01:06:26 +0100 Subject: [PATCH 102/108] chore(NA): bump version to 8.3.0 (#128893) * chore(NA): bump version to 8.3.0 * chore(NA): update ingest pipeline version * chore(NA): update ingest pipeline version Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- .../validation/integration_tests/validator.test.ts | 2 +- x-pack/package.json | 2 +- x-pack/plugins/index_management/common/constants/plugin.ts | 2 +- x-pack/plugins/ingest_pipelines/kibana.json | 1 + x-pack/plugins/remote_clusters/common/constants.ts | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8552d400e412c..d809bb2e025f7 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dashboarding" ], "private": true, - "version": "8.2.0", + "version": "8.3.0", "branch": "main", "types": "./kibana.d.ts", "tsdocMetadata": "./build/tsdoc-metadata.json", diff --git a/src/core/server/saved_objects/validation/integration_tests/validator.test.ts b/src/core/server/saved_objects/validation/integration_tests/validator.test.ts index 21e8973769710..41b23f917afd2 100644 --- a/src/core/server/saved_objects/validation/integration_tests/validator.test.ts +++ b/src/core/server/saved_objects/validation/integration_tests/validator.test.ts @@ -191,7 +191,7 @@ describe('validates saved object types when a schema is provided', () => { { migrationVersion: { foo: '7.16.0' } } ); }).rejects.toThrowErrorMatchingInlineSnapshot( - `"Migration function for version 8.2.0 threw an error"` + `"Migration function for version 8.3.0 threw an error"` ); }); diff --git a/x-pack/package.json b/x-pack/package.json index ccfad71a4f7b3..182ee65c1d12e 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -1,6 +1,6 @@ { "name": "x-pack", - "version": "8.2.0", + "version": "8.3.0", "author": "Elastic", "private": true, "license": "Elastic-License", diff --git a/x-pack/plugins/index_management/common/constants/plugin.ts b/x-pack/plugins/index_management/common/constants/plugin.ts index 482661045b3fa..64619afcfb11b 100644 --- a/x-pack/plugins/index_management/common/constants/plugin.ts +++ b/x-pack/plugins/index_management/common/constants/plugin.ts @@ -22,4 +22,4 @@ export const PLUGIN = { // "PluginInitializerContext.env.packageInfo.version". In some cases it is not possible // to dynamically inject that version without a huge refactor on the code base. // We will then keep this single constant to declare on which major branch we are. -export const MAJOR_VERSION = '8.2.0'; +export const MAJOR_VERSION = '8.3.0'; diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 912584e808331..b43c7c20b9bc1 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -1,6 +1,7 @@ { "id": "ingestPipelines", "version": "8.2.0", + "kibanaVersion": "kibana", "server": true, "ui": true, "owner": { diff --git a/x-pack/plugins/remote_clusters/common/constants.ts b/x-pack/plugins/remote_clusters/common/constants.ts index 86910640191e3..fca751da37b90 100644 --- a/x-pack/plugins/remote_clusters/common/constants.ts +++ b/x-pack/plugins/remote_clusters/common/constants.ts @@ -20,7 +20,7 @@ export const PLUGIN = { }, }; -export const MAJOR_VERSION = '8.2.0'; +export const MAJOR_VERSION = '8.3.0'; export const API_BASE_PATH = '/api/remote_clusters'; From e7eea48a6cc8b51cdc3dd71cb1d4c7608e4a0b07 Mon Sep 17 00:00:00 2001 From: Andrew Tate Date: Wed, 30 Mar 2022 19:17:20 -0500 Subject: [PATCH 103/108] [Lens] Update show underlying data strings (#128923) --- docs/setup/settings.asciidoc | 2 +- x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts | 4 ++-- .../lens/public/trigger_actions/open_in_discover_action.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 23487f1ff3d88..b0f238124a008 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -675,7 +675,7 @@ out through *Advanced Settings*. *Default: `true`* sources and images. When false, Vega can only get data from {es}. *Default: `false`* | `xpack.ccr.ui.enabled` -Set this value to false to disable the Cross-Cluster Replication UI. +| Set this value to false to disable the Cross-Cluster Replication UI. *Default: `true`* |[[settings-explore-data-in-context]] `xpack.discoverEnhanced.actions.` diff --git a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts index 12cd5aac25552..305b74575ce81 100644 --- a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts +++ b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts @@ -21,8 +21,8 @@ import { TableInspectorAdapter } from '../editor_frame_service/types'; import { Datasource } from '../types'; export const getShowUnderlyingDataLabel = () => - i18n.translate('xpack.lens.app.openInDiscover', { - defaultMessage: 'Open in Discover', + i18n.translate('xpack.lens.app.exploreRawData', { + defaultMessage: 'Explore raw data', }); /** diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts index 947e01fd15bc9..03b917bb9482f 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts @@ -21,8 +21,8 @@ export const createOpenInDiscoverAction = (discover: DiscoverStart, hasDiscoverA order: 19, // right after Inspect which is 20 getIconType: () => 'popout', getDisplayName: () => - i18n.translate('xpack.lens.actions.openInDiscover', { - defaultMessage: 'Open in Discover', + i18n.translate('xpack.lens.actions.exploreRawData', { + defaultMessage: 'Explore raw data', }), isCompatible: async (context: { embeddable: IEmbeddable }) => { if (!hasDiscoverAccess) return false; From d0e4eefb472271b5a0b8bce70b7db853f3cb4930 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 30 Mar 2022 19:09:40 -0600 Subject: [PATCH 104/108] [Maps] remove Kibana 8.1 deprecated API usage (#128912) * [Maps] remove remaining 8.1.0 deprecations * fetch * review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../es_pew_pew_source/es_pew_pew_source.js | 12 ++++++---- .../es_search_source/es_search_source.tsx | 24 +++++++++++-------- .../classes/sources/es_source/es_source.ts | 12 ++++++---- .../classes/util/can_skip_fetch.test.ts | 2 +- x-pack/plugins/maps/public/locators.test.ts | 4 ++-- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index b3d2074c91667..73a267036044e 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -201,11 +201,13 @@ export class ESPewPewSource extends AbstractESAggSource { try { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); - const esResp = await searchSource.fetch({ - abortSignal: abortController.signal, - legacyHitsTotal: false, - executionContext: makePublicExecutionContext('es_pew_pew_source:bounds'), - }); + const { rawResponse: esResp } = await searchSource + .fetch$({ + abortSignal: abortController.signal, + legacyHitsTotal: false, + executionContext: makePublicExecutionContext('es_pew_pew_source:bounds'), + }) + .toPromise(); if (esResp.aggregations.destFitToBounds.bounds) { corners.push([ esResp.aggregations.destFitToBounds.bounds.top_left.lon, diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index e703561357a07..42fded4fbefb7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -597,10 +597,12 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource searchSource.setField('query', query); searchSource.setField('fieldsFromSource', this._getTooltipPropertyNames()); - const resp = await searchSource.fetch({ - legacyHitsTotal: false, - executionContext: makePublicExecutionContext('es_search_source:load_tooltip_properties'), - }); + const { rawResponse: resp } = await searchSource + .fetch$({ + legacyHitsTotal: false, + executionContext: makePublicExecutionContext('es_search_source:load_tooltip_properties'), + }) + .toPromise(); const hit = _.get(resp, 'hits.hits[0]'); if (!hit) { @@ -899,12 +901,14 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource const maxResultWindow = await this.getMaxResultWindow(); const searchSource = await this.makeSearchSource(searchFilters, 0); searchSource.setField('trackTotalHits', maxResultWindow + 1); - const resp = await searchSource.fetch({ - abortSignal: abortController.signal, - sessionId: searchFilters.searchSessionId, - legacyHitsTotal: false, - executionContext: makePublicExecutionContext('es_search_source:all_doc_counts'), - }); + const { rawResponse: resp } = await searchSource + .fetch$({ + abortSignal: abortController.signal, + sessionId: searchFilters.searchSessionId, + legacyHitsTotal: false, + executionContext: makePublicExecutionContext('es_search_source:all_doc_counts'), + }) + .toPromise(); return !isTotalHitsGreaterThan(resp.hits.total as unknown as TotalHits, maxResultWindow); } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 27c11d27673f2..ece1ec39f3425 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -279,11 +279,13 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource try { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); - const esResp = await searchSource.fetch({ - abortSignal: abortController.signal, - legacyHitsTotal: false, - executionContext: makePublicExecutionContext('es_source:bounds'), - }); + const { rawResponse: esResp } = await searchSource + .fetch$({ + abortSignal: abortController.signal, + legacyHitsTotal: false, + executionContext: makePublicExecutionContext('es_source:bounds'), + }) + .toPromise(); if (!esResp.aggregations) { return null; diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.ts index a564644df7af0..953c456d346b8 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.ts @@ -7,7 +7,7 @@ import { canSkipSourceUpdate, updateDueToExtent } from './can_skip_fetch'; import { DataRequest } from './data_request'; -import { Filter } from 'src/plugins/data/common'; +import { Filter } from '@kbn/es-query'; import { ISource } from '../sources/source'; describe('updateDueToExtent', () => { diff --git a/x-pack/plugins/maps/public/locators.test.ts b/x-pack/plugins/maps/public/locators.test.ts index aabae1a26c1df..cc954d5f73717 100644 --- a/x-pack/plugins/maps/public/locators.test.ts +++ b/x-pack/plugins/maps/public/locators.test.ts @@ -6,7 +6,7 @@ */ import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../common/constants'; -import { esFilters } from '../../../../src/plugins/data/public'; +import { FilterStateStore } from '@kbn/es-query'; import { MapsAppLocatorDefinition } from './locators'; import { SerializableRecord } from '@kbn/utility-types'; import { LayerDescriptor } from '../common/descriptor_types'; @@ -100,7 +100,7 @@ describe('visualize url generator', () => { }, query: { query: 'q1' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ], From 1168c117cc9df42a89b42229a099ceb4b753403d Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Wed, 30 Mar 2022 19:28:45 -0700 Subject: [PATCH 105/108] [Controls] Fix range filter ignoring global filter for the same field (#128856) * Snaps built range filter to available min and max * Restored filter meta key --- .../range_slider/range_slider_embeddable.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx index ef4bc41abeefc..965eb2da18e93 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx @@ -264,8 +264,10 @@ export class RangeSliderEmbeddable extends Embeddable { const { value: [selectedMin, selectedMax] = ['', ''], ignoreParentSettings } = this.getInput(); + const availableMin = this.componentState.min; + const availableMax = this.componentState.max; - const hasData = !isEmpty(this.componentState.min) && !isEmpty(this.componentState.max); + const hasData = !isEmpty(availableMin) && !isEmpty(availableMax); const hasLowerSelection = !isEmpty(selectedMin); const hasUpperSelection = !isEmpty(selectedMax); const hasEitherSelection = hasLowerSelection || hasUpperSelection; @@ -275,9 +277,9 @@ export class RangeSliderEmbeddable extends Embeddable parseFloat(selectedMax); const isLowerSelectionOutOfRange = - hasLowerSelection && parseFloat(selectedMin) > parseFloat(this.componentState.max); + hasLowerSelection && parseFloat(selectedMin) > parseFloat(availableMax); const isUpperSelectionOutOfRange = - hasUpperSelection && parseFloat(selectedMax) < parseFloat(this.componentState.min); + hasUpperSelection && parseFloat(selectedMax) < parseFloat(availableMin); const isSelectionOutOfRange = (!ignoreParentSettings?.ignoreValidations && hasData && isLowerSelectionOutOfRange) || isUpperSelectionOutOfRange; @@ -292,15 +294,18 @@ export class RangeSliderEmbeddable extends Embeddable Date: Wed, 30 Mar 2022 21:12:03 -0600 Subject: [PATCH 106/108] Adds more telemetry tests (#128997) ## Summary Adds more e2e telemetry tests for detection_rule_status ### 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 --- .../tests/telemetry/index.ts | 1 + .../usage_collector/detection_rule_status.ts | 810 ++++++++++++++++++ .../usage_collector/detection_rules.ts | 71 +- 3 files changed, 823 insertions(+), 59 deletions(-) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rule_status.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts index ce1966c3175a9..8936115ac6e59 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts @@ -14,6 +14,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { this.tags('ciGroup11'); loadTestFile(require.resolve('./usage_collector/all_types')); loadTestFile(require.resolve('./usage_collector/detection_rules')); + loadTestFile(require.resolve('./usage_collector/detection_rule_status')); loadTestFile(require.resolve('./task_based/all_types')); loadTestFile(require.resolve('./task_based/detection_rules')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rule_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rule_status.ts new file mode 100644 index 0000000000000..9092cacdad050 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rule_status.ts @@ -0,0 +1,810 @@ +/* + * 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 { MlJobUsageMetric } from '../../../../../../plugins/security_solution/server/usage/detections/ml_jobs/types'; +import type { RulesTypeUsage } from '../../../../../../plugins/security_solution/server/usage/detections/rules/types'; +import type { DetectionMetrics } from '../../../../../../plugins/security_solution/server/usage/detections/types'; +import type { + ThreatMatchCreateSchema, + ThresholdCreateSchema, +} from '../../../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import type { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getInitialMlJobUsage } from '../../../../../../plugins/security_solution/server/usage/detections/ml_jobs/get_initial_usage'; +import { + createRule, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getEqlRuleForSignalTesting, + getRuleForSignalTesting, + getSimpleThreatMatch, + getStats, + getThresholdRuleForSignalTesting, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, + deleteAllEventLogExecutionEvents, +} from '../../../../utils'; +import { getInitialDetectionMetrics } from '../../../../../../plugins/security_solution/server/usage/detections/get_initial_usage'; +import { + getInitialMaxAvgMin, + getInitialSingleEventLogUsage, + getInitialSingleEventMetric, +} from '../../../../../../plugins/security_solution/server/usage/detections/rules/get_initial_usage'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const log = getService('log'); + const retry = getService('retry'); + const es = getService('es'); + + // Note: We don't actually find signals well with ML tests at the moment so there are not tests for ML rule type for telemetry + describe('Detection rule status telemetry', async () => { + before(async () => { + // Just in case other tests do not clean up the event logs, let us clear them now and here only once. + await deleteAllEventLogExecutionEvents(es, log); + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + beforeEach(async () => { + await createSignalsIndex(supertest, log); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + await deleteAllEventLogExecutionEvents(es, log); + }); + + describe('"kql" rule type', () => { + let stats: DetectionMetrics | undefined; + before(async () => { + const rule = getRuleForSignalTesting(['telemetry']); + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + // get the stats for all the tests where we at least have the expected "query" to reduce chances of flake by checking that at least one custom rule passed + await retry.try(async () => { + stats = await getStats(supertest, log); + expect(stats.detection_rules.detection_rule_status.custom_rules.total.succeeded).to.eql( + 1 + ); + }); + }); + + it('should have an empty "ml_jobs"', () => { + const expectedMLJobs: MlJobUsageMetric = { + ml_job_usage: getInitialMlJobUsage(), + ml_job_metrics: [], + }; + expect(stats?.ml_jobs).to.eql(expectedMLJobs); + }); + + it('should have an empty "detection_rule_detail"', () => { + expect(stats?.detection_rules.detection_rule_detail).to.eql([]); + }); + + it('should have an active "detection_rule_usage" with non-zero values', () => { + const expectedRuleUsage: RulesTypeUsage = { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }; + expect(stats?.detection_rules.detection_rule_usage).to.eql(expectedRuleUsage); + }); + + it('should have zero values for "detection_rule_status.all_rules" rules that are not query based', () => { + expect(stats?.detection_rules.detection_rule_status.all_rules.eql).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.machine_learning).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.saved_query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.threat_match).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.threshold).to.eql( + getInitialSingleEventMetric() + ); + }); + + it('should have zero values for "detection_rule_status.custom_rules" rules that are not query based', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.eql).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.machine_learning).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.saved_query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.threat_match).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.threshold).to.eql( + getInitialSingleEventMetric() + ); + }); + + it('should have zero values for failures of the query based rule', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.query.failures).to.eql(0); + expect(stats?.detection_rules.detection_rule_status.custom_rules.query.top_failures).to.eql( + [] + ); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.partial_failures + ).to.eql([]); + }); + + it('should have zero values for gaps', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.query.gap_duration).to.eql( + getInitialMaxAvgMin() + ); + }); + + it('should have non zero values for "index_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.index_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.index_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.index_duration.min + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.min + ).to.be.above(1); + }); + + it('should have non zero values for "search_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.min + ).to.be.above(1); + }); + + it('should have non zero values for "succeeded"', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.query.succeeded).to.eql(1); + }); + + it('should have non zero values for "succeeded", "index_duration", and "search_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.index_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.index_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.index_duration.min + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.min + ).to.be.above(1); + }); + + it('should have a total value for "detection_rule_status.custom_rules" rule ', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.total).to.eql({ + failures: 0, + partial_failures: 0, + succeeded: 1, + }); + }); + + it('should have zero values for "detection_rule_status.elastic_rules"', async () => { + expect(stats?.detection_rules.detection_rule_status.elastic_rules).to.eql( + getInitialSingleEventLogUsage() + ); + }); + }); + + describe('"eql" rule type', () => { + let stats: DetectionMetrics | undefined; + before(async () => { + const rule = getEqlRuleForSignalTesting(['telemetry']); + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + // get the stats for all the tests where we at least have the expected "query" to reduce chances of flake by checking that at least one custom rule passed + await retry.try(async () => { + stats = await getStats(supertest, log); + expect(stats.detection_rules.detection_rule_status.custom_rules.total.succeeded).to.eql( + 1 + ); + }); + }); + + it('should have an empty "ml_jobs"', () => { + const expectedMLJobs: MlJobUsageMetric = { + ml_job_usage: getInitialMlJobUsage(), + ml_job_metrics: [], + }; + expect(stats?.ml_jobs).to.eql(expectedMLJobs); + }); + + it('should have an empty "detection_rule_detail"', () => { + expect(stats?.detection_rules.detection_rule_detail).to.eql([]); + }); + + it('should have an active "detection_rule_usage" with non-zero values', () => { + const expectedRuleUsage: RulesTypeUsage = { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }; + expect(stats?.detection_rules.detection_rule_usage).to.eql(expectedRuleUsage); + }); + + it('should have zero values for "detection_rule_status.all_rules" rules that are not eql based', () => { + expect(stats?.detection_rules.detection_rule_status.all_rules.query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.machine_learning).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.saved_query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.threat_match).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.threshold).to.eql( + getInitialSingleEventMetric() + ); + }); + + it('should have zero values for "detection_rule_status.custom_rules" rules that are not eql based', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.machine_learning).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.saved_query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.threat_match).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.threshold).to.eql( + getInitialSingleEventMetric() + ); + }); + + it('should have zero values for failures of the eql based rule', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.eql.failures).to.eql(0); + expect(stats?.detection_rules.detection_rule_status.custom_rules.eql.top_failures).to.eql( + [] + ); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.partial_failures + ).to.eql([]); + }); + + it('should have zero values for gaps', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.eql.gap_duration).to.eql( + getInitialMaxAvgMin() + ); + }); + + it('should have non zero values for "index_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.index_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.index_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.index_duration.min + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.min + ).to.be.above(1); + }); + + it('should have non zero values for "search_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.min + ).to.be.above(1); + }); + + it('should have non zero values for "succeeded"', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.eql.succeeded).to.eql(1); + }); + + it('should have non zero values for "succeeded", "index_duration", and "search_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.index_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.index_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.index_duration.min + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.min + ).to.be.above(1); + }); + + it('should have a total value for "detection_rule_status.custom_rules" rule ', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.total).to.eql({ + failures: 0, + partial_failures: 0, + succeeded: 1, + }); + }); + + it('should have zero values for "detection_rule_status.elastic_rules"', async () => { + expect(stats?.detection_rules.detection_rule_status.elastic_rules).to.eql( + getInitialSingleEventLogUsage() + ); + }); + }); + + describe('"threshold" rule type', () => { + let stats: DetectionMetrics | undefined; + before(async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(['telemetry']), + threshold: { + field: 'keyword', + value: 1, + }, + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + // get the stats for all the tests where we at least have the expected "query" to reduce chances of flake by checking that at least one custom rule passed + await retry.try(async () => { + stats = await getStats(supertest, log); + expect(stats.detection_rules.detection_rule_status.custom_rules.total.succeeded).to.eql( + 1 + ); + }); + }); + + it('should have an empty "ml_jobs"', () => { + const expectedMLJobs: MlJobUsageMetric = { + ml_job_usage: getInitialMlJobUsage(), + ml_job_metrics: [], + }; + expect(stats?.ml_jobs).to.eql(expectedMLJobs); + }); + + it('should have an empty "detection_rule_detail"', () => { + expect(stats?.detection_rules.detection_rule_detail).to.eql([]); + }); + + it('should have an active "detection_rule_usage" with non-zero values', () => { + const expectedRuleUsage: RulesTypeUsage = { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }; + expect(stats?.detection_rules.detection_rule_usage).to.eql(expectedRuleUsage); + }); + + it('should have zero values for "detection_rule_status.all_rules" rules that are not threshold based', () => { + expect(stats?.detection_rules.detection_rule_status.all_rules.query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.machine_learning).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.saved_query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.threat_match).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.eql).to.eql( + getInitialSingleEventMetric() + ); + }); + + it('should have zero values for "detection_rule_status.custom_rules" rules that are not threshold based', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.machine_learning).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.saved_query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.threat_match).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.eql).to.eql( + getInitialSingleEventMetric() + ); + }); + + it('should have zero values for failures of the threshold based rule', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.threshold.failures).to.eql( + 0 + ); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.top_failures + ).to.eql([]); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.partial_failures + ).to.eql([]); + }); + + it('should have zero values for gaps', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.gap_duration + ).to.eql(getInitialMaxAvgMin()); + }); + + it('should have non zero values for "index_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.index_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.index_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.index_duration.min + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.min + ).to.be.above(1); + }); + + it('should have non zero values for "search_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.min + ).to.be.above(1); + }); + + it('should have non zero values for "succeeded"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.succeeded + ).to.eql(1); + }); + + it('should have non zero values for "succeeded", "index_duration", and "search_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.index_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.index_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.index_duration.min + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.min + ).to.be.above(1); + }); + + it('should have a total value for "detection_rule_status.custom_rules" rule ', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.total).to.eql({ + failures: 0, + partial_failures: 0, + succeeded: 1, + }); + }); + + it('should have zero values for "detection_rule_status.elastic_rules"', async () => { + expect(stats?.detection_rules.detection_rule_status.elastic_rules).to.eql( + getInitialSingleEventLogUsage() + ); + }); + }); + + describe('"indicator_match/threat_match" rule type', () => { + let stats: DetectionMetrics | undefined; + before(async () => { + const rule: ThreatMatchCreateSchema = { + ...getSimpleThreatMatch('rule-1', true), + index: ['telemetry'], + threat_index: ['telemetry'], + threat_mapping: [ + { + entries: [ + { + field: 'keyword', + value: 'keyword', + type: 'mapping', + }, + ], + }, + ], + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + // get the stats for all the tests where we at least have the expected "query" to reduce chances of flake by checking that at least one custom rule passed + await retry.try(async () => { + stats = await getStats(supertest, log); + expect(stats.detection_rules.detection_rule_status.custom_rules.total.succeeded).to.eql( + 1 + ); + }); + }); + + it('should have an empty "ml_jobs"', () => { + const expectedMLJobs: MlJobUsageMetric = { + ml_job_usage: getInitialMlJobUsage(), + ml_job_metrics: [], + }; + expect(stats?.ml_jobs).to.eql(expectedMLJobs); + }); + + it('should have an empty "detection_rule_detail"', () => { + expect(stats?.detection_rules.detection_rule_detail).to.eql([]); + }); + + it('should have an active "detection_rule_usage" with non-zero values', () => { + const expectedRuleUsage: RulesTypeUsage = { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threat_match: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threat_match, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }; + expect(stats?.detection_rules.detection_rule_usage).to.eql(expectedRuleUsage); + }); + + it('should have zero values for "detection_rule_status.all_rules" rules that are not threat_match based', () => { + expect(stats?.detection_rules.detection_rule_status.all_rules.query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.machine_learning).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.saved_query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.threshold).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.all_rules.eql).to.eql( + getInitialSingleEventMetric() + ); + }); + + it('should have zero values for "detection_rule_status.custom_rules" rules that are not threat_match based', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.machine_learning).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.saved_query).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.threshold).to.eql( + getInitialSingleEventMetric() + ); + expect(stats?.detection_rules.detection_rule_status.custom_rules.eql).to.eql( + getInitialSingleEventMetric() + ); + }); + + it('should have zero values for failures of the threat_match based rule', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.failures + ).to.eql(0); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.top_failures + ).to.eql([]); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.partial_failures + ).to.eql([]); + }); + + it('should have zero values for gaps', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.gap_duration + ).to.eql(getInitialMaxAvgMin()); + }); + + it('should have non zero values for "index_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.index_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.index_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.index_duration.min + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.min + ).to.be.above(1); + }); + + it('should have non zero values for "search_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.min + ).to.be.above(1); + }); + + it('should have non zero values for "succeeded"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.succeeded + ).to.eql(1); + }); + + it('should have non zero values for "succeeded", "index_duration", and "search_duration"', () => { + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.index_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.index_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.index_duration.min + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.max + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.avg + ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.min + ).to.be.above(1); + }); + + it('should have a total value for "detection_rule_status.custom_rules" rule ', () => { + expect(stats?.detection_rules.detection_rule_status.custom_rules.total).to.eql({ + failures: 0, + partial_failures: 0, + succeeded: 1, + }); + }); + + it('should have zero values for "detection_rule_status.elastic_rules"', async () => { + expect(stats?.detection_rules.detection_rule_status.elastic_rules).to.eql( + getInitialSingleEventLogUsage() + ); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts index 41415e8bafc1e..d565960cb2442 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts @@ -32,6 +32,7 @@ import { waitForRuleSuccessOrStatus, waitForSignalsToBePresent, updateRule, + deleteAllEventLogExecutionEvents, } from '../../../../utils'; import { getInitialDetectionMetrics } from '../../../../../../plugins/security_solution/server/usage/detections/get_initial_usage'; import { getInitialEventLogUsage } from '../../../../../../plugins/security_solution/server/usage/detections/rules/get_initial_usage'; @@ -42,9 +43,12 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const log = getService('log'); const retry = getService('retry'); + const es = getService('es'); describe('Detection rule telemetry', async () => { before(async () => { + // Just in case other tests do not clean up the event logs, let us clear them now and here only once. + await deleteAllEventLogExecutionEvents(es, log); await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); }); @@ -59,6 +63,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await deleteSignalsIndex(supertest, log); await deleteAllAlerts(supertest, log); + await deleteAllEventLogExecutionEvents(es, log); }); describe('"kql" rule type', () => { @@ -67,10 +72,6 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -108,7 +109,7 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - // remove "detection_rule_status" from the test by resetting it to initial + // remove "detection_rule_status" from the test by resetting it to initial (see detection_rule_status.ts for more in-depth testing of this structure) stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); const expected: DetectionMetrics = { @@ -151,7 +152,7 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - // remove "detection_rule_status" from the test by resetting it to initial + // remove "detection_rule_status" from the test by resetting it to initial (see detection_rule_status.ts for more in-depth testing of this structure) stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); const expected: DetectionMetrics = { @@ -224,10 +225,6 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -262,7 +259,7 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - // remove "detection_rule_status" from the test by resetting it to initial + // remove "detection_rule_status" from the test by resetting it to initial (see detection_rule_status.ts for more in-depth testing of this structure) stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); const expected: DetectionMetrics = { @@ -297,10 +294,6 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -338,7 +331,7 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - // remove "detection_rule_status" from the test by resetting it to initial + // remove "detection_rule_status" from the test by resetting it to initial (see detection_rule_status.ts for more in-depth testing of this structure) stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); const expected: DetectionMetrics = { @@ -381,7 +374,7 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - // remove "detection_rule_status" from the test by resetting it to initial + // remove "detection_rule_status" from the test by resetting it to initial (see detection_rule_status.ts for more in-depth testing of this structure) stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); const expected: DetectionMetrics = { @@ -418,7 +411,7 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - // remove "detection_rule_status" from the test by resetting it to initial + // remove "detection_rule_status" from the test by resetting it to initial (see detection_rule_status.ts for more in-depth testing of this structure) stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); const expected: DetectionMetrics = { @@ -454,10 +447,6 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -492,7 +481,7 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - // remove "detection_rule_status" from the test by resetting it to initial + // remove "detection_rule_status" from the test by resetting it to initial (see detection_rule_status.ts for more in-depth testing of this structure) stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); const expected: DetectionMetrics = { @@ -533,10 +522,6 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -794,10 +779,6 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -1018,10 +999,6 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - const expected: DetectionMetrics = { ...getInitialDetectionMetrics(), detection_rules: { @@ -1292,10 +1269,6 @@ export default ({ getService }: FtrProviderContext) => { await installPrePackagedRules(supertest, log); await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); expect(stats.detection_rules.detection_rule_usage.elastic_total.disabled).above(0); expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); @@ -1329,10 +1302,6 @@ export default ({ getService }: FtrProviderContext) => { await installPrePackagedRules(supertest, log); await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id @@ -1374,10 +1343,6 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' @@ -1432,10 +1397,6 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' @@ -1490,10 +1451,6 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' @@ -1548,10 +1505,6 @@ export default ({ getService }: FtrProviderContext) => { await retry.try(async () => { const stats = await getStats(supertest, log); - - // remove "detection_rule_status" from the test by resetting it to initial - stats.detection_rules.detection_rule_status = getInitialEventLogUsage(); - // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id const foundRule = stats.detection_rules.detection_rule_detail.find( (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' From 1caaabaad14cf3c6f8b83d47f9ae15fbfaa600c6 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Thu, 31 Mar 2022 07:50:57 +0200 Subject: [PATCH 107/108] [Watcher] Remove `axios` dependency in tests (#128765) * wip start refactoring tests * commit using @elastic.co * Finish refactoring tests * Remove unused code * Add docs * Address CR changes --- .../helpers/app_context.mock.tsx | 11 - .../helpers/body_response.ts | 10 - .../helpers/http_requests.ts | 221 ++++++------- .../client_integration/helpers/index.ts | 1 - ...p_environment.ts => setup_environment.tsx} | 35 +- .../helpers/watch_create_json.helpers.ts | 9 +- .../helpers/watch_create_threshold.helpers.ts | 9 +- .../helpers/watch_edit.helpers.ts | 9 +- .../helpers/watch_list.helpers.ts | 8 +- .../helpers/watch_status.helpers.ts | 11 +- .../watch_create_json.test.ts | 91 +++--- .../watch_create_threshold.test.tsx | 306 +++++++++--------- .../client_integration/watch_edit.test.ts | 112 +++---- .../client_integration/watch_list.test.ts | 19 +- .../client_integration/watch_status.test.ts | 67 ++-- .../plugins/watcher/common/constants/index.ts | 2 +- .../watcher/common/constants/routes.ts | 4 +- 17 files changed, 440 insertions(+), 485 deletions(-) delete mode 100644 x-pack/plugins/watcher/__jest__/client_integration/helpers/body_response.ts rename x-pack/plugins/watcher/__jest__/client_integration/helpers/{setup_environment.ts => setup_environment.tsx} (54%) diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx b/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx index 8176d3fcbbca2..6e246380e7049 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx @@ -5,9 +5,7 @@ * 2.0. */ -import React from 'react'; import { of } from 'rxjs'; -import { ComponentType } from 'enzyme'; import { LocationDescriptorObject } from 'history'; import { @@ -17,7 +15,6 @@ import { httpServiceMock, scopedHistoryMock, } from '../../../../../../src/core/public/mocks'; -import { AppContextProvider } from '../../../public/application/app_context'; import { AppDeps } from '../../../public/application/app'; import { LicenseStatus } from '../../../common/types/license_status'; @@ -52,11 +49,3 @@ export const mockContextValue: AppDeps = { history, getUrlForApp: jest.fn(), }; - -export const withAppContext = (Component: ComponentType) => (props: any) => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/body_response.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/body_response.ts deleted file mode 100644 index dce7213297388..0000000000000 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/body_response.ts +++ /dev/null @@ -1,10 +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. - */ - -export const wrapBodyResponse = (obj: object) => JSON.stringify({ body: JSON.stringify(obj) }); - -export const unwrapBodyResponse = (string: string) => JSON.parse(JSON.parse(string).body); diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts index e98cd66a25684..31c82cc33cd59 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts @@ -5,123 +5,115 @@ * 2.0. */ -import sinon, { SinonFakeServer } from 'sinon'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; import { ROUTES } from '../../../common/constants'; const { API_ROOT } = ROUTES; type HttpResponse = Record | any[]; - -const mockResponse = (defaultResponse: HttpResponse, response: HttpResponse) => [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ ...defaultResponse, ...response }), -]; +type HttpMethod = 'GET' | 'PUT' | 'POST'; +export interface ResponseError { + statusCode: number; + message: string | Error; +} // Register helpers to mock HTTP Requests -const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setLoadWatchesResponse = (response: HttpResponse = {}) => { - const defaultResponse = { watches: [] }; - - server.respondWith('GET', `${API_ROOT}/watches`, mockResponse(defaultResponse, response)); - }; - - const setLoadWatchResponse = (response: HttpResponse = {}) => { - const defaultResponse = { watch: {} }; - server.respondWith('GET', `${API_ROOT}/watch/:id`, mockResponse(defaultResponse, response)); - }; - - const setLoadWatchHistoryResponse = (response: HttpResponse = {}) => { - const defaultResponse = { watchHistoryItems: [] }; - server.respondWith( - 'GET', - `${API_ROOT}/watch/:id/history`, - mockResponse(defaultResponse, response) - ); - }; - - const setLoadWatchHistoryItemResponse = (response: HttpResponse = {}) => { - const defaultResponse = { watchHistoryItem: {} }; - server.respondWith('GET', `${API_ROOT}/history/:id`, mockResponse(defaultResponse, response)); - }; - - const setDeleteWatchResponse = (response?: HttpResponse, error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - - server.respondWith('POST', `${API_ROOT}/watches/delete`, [ - status, - { 'Content-Type': 'application/json' }, - body, - ]); - }; - - const setSaveWatchResponse = (id: string, response?: HttpResponse, error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - - server.respondWith('PUT', `${API_ROOT}/watch/${id}`, [ - status, - { 'Content-Type': 'application/json' }, - body, - ]); - }; - - const setLoadExecutionResultResponse = (response: HttpResponse = {}) => { - const defaultResponse = { watchHistoryItem: {} }; - server.respondWith('PUT', `${API_ROOT}/watch/execute`, mockResponse(defaultResponse, response)); - }; - - const setLoadMatchingIndicesResponse = (response: HttpResponse = {}) => { - const defaultResponse = { indices: [] }; - server.respondWith('POST', `${API_ROOT}/indices`, mockResponse(defaultResponse, response)); - }; - - const setLoadEsFieldsResponse = (response: HttpResponse = {}) => { - const defaultResponse = { fields: [] }; - server.respondWith('POST', `${API_ROOT}/fields`, mockResponse(defaultResponse, response)); - }; - - const setLoadSettingsResponse = (response: HttpResponse = {}) => { - const defaultResponse = { action_types: {} }; - server.respondWith('GET', `${API_ROOT}/settings`, mockResponse(defaultResponse, response)); - }; - - const setLoadWatchVisualizeResponse = (response: HttpResponse = {}) => { - const defaultResponse = { visualizeData: {} }; - server.respondWith( - 'POST', - `${API_ROOT}/watch/visualize`, - mockResponse(defaultResponse, response) - ); - }; - - const setDeactivateWatchResponse = (response: HttpResponse = {}) => { - const defaultResponse = { watchStatus: {} }; - server.respondWith( +const registerHttpRequestMockHelpers = ( + httpSetup: ReturnType +) => { + const mockResponses = new Map>>( + ['GET', 'PUT', 'POST'].map( + (method) => [method, new Map()] as [HttpMethod, Map>] + ) + ); + + const mockMethodImplementation = (method: HttpMethod, path: string) => + mockResponses.get(method)?.get(path) ?? Promise.resolve({}); + + httpSetup.get.mockImplementation((path) => + mockMethodImplementation('GET', path as unknown as string) + ); + httpSetup.post.mockImplementation((path) => + mockMethodImplementation('POST', path as unknown as string) + ); + httpSetup.put.mockImplementation((path) => + mockMethodImplementation('PUT', path as unknown as string) + ); + + const mockResponse = (method: HttpMethod, path: string, response?: unknown, error?: unknown) => { + const defuse = (promise: Promise) => { + promise.catch(() => {}); + return promise; + }; + + return mockResponses + .get(method)! + .set(path, error ? defuse(Promise.reject(error)) : Promise.resolve(response)); + }; + + const setLoadWatchesResponse = (response?: HttpResponse, error?: ResponseError) => + mockResponse('GET', `${API_ROOT}/watches`, response, error); + + const setLoadWatchResponse = (watchId: string, response?: HttpResponse, error?: ResponseError) => + mockResponse('GET', `${API_ROOT}/watch/${watchId}`, response, error); + + const setLoadWatchHistoryResponse = ( + watchId: string, + response?: HttpResponse, + error?: ResponseError + ) => mockResponse('GET', `${API_ROOT}/watch/${watchId}/history`, response, error); + + const setLoadWatchHistoryItemResponse = ( + watchId: string, + response?: HttpResponse, + error?: ResponseError + ) => mockResponse('GET', `${API_ROOT}/watch/history/${watchId}`, response, error); + + const setDeleteWatchResponse = (response?: HttpResponse, error?: ResponseError) => + mockResponse('POST', `${API_ROOT}/watches/delete`, response, error); + + const setSaveWatchResponse = (watchId: string, response?: HttpResponse, error?: ResponseError) => + mockResponse('PUT', `${API_ROOT}/watch/${watchId}`, response, error); + + const setLoadExecutionResultResponse = (response?: HttpResponse, error?: ResponseError) => + mockResponse('PUT', `${API_ROOT}/watch/execute`, response, error); + + const setLoadMatchingIndicesResponse = (response?: HttpResponse, error?: ResponseError) => + mockResponse('PUT', `${API_ROOT}/indices`, response, error); + + const setLoadEsFieldsResponse = (response?: HttpResponse, error?: ResponseError) => + mockResponse('POST', `${API_ROOT}/fields`, response, error); + + const setLoadSettingsResponse = (response?: HttpResponse, error?: ResponseError) => + mockResponse('GET', `${API_ROOT}/settings`, response, error); + + const setLoadWatchVisualizeResponse = (response?: HttpResponse, error?: ResponseError) => + mockResponse('POST', `${API_ROOT}/watch/visualize`, response, error); + + const setDeactivateWatchResponse = ( + watchId: string, + response?: HttpResponse, + error?: ResponseError + ) => mockResponse('PUT', `${API_ROOT}/watch/${watchId}/deactivate`, response, error); + + const setActivateWatchResponse = ( + watchId: string, + response?: HttpResponse, + error?: ResponseError + ) => mockResponse('PUT', `${API_ROOT}/watch/${watchId}/activate`, response, error); + + const setAcknowledgeWatchResponse = ( + watchId: string, + actionId: string, + response?: HttpResponse, + error?: ResponseError + ) => + mockResponse( 'PUT', - `${API_ROOT}/watch/:id/deactivate`, - mockResponse(defaultResponse, response) + `${API_ROOT}/watch/${watchId}/action/${actionId}/acknowledge`, + response, + error ); - }; - - const setActivateWatchResponse = (response: HttpResponse = {}) => { - const defaultResponse = { watchStatus: {} }; - server.respondWith( - 'PUT', - `${API_ROOT}/watch/:id/activate`, - mockResponse(defaultResponse, response) - ); - }; - - const setAcknowledgeWatchResponse = (response: HttpResponse = {}) => { - const defaultResponse = { watchStatus: {} }; - server.respondWith( - 'PUT', - `${API_ROOT}/watch/:id/action/:actionId/acknowledge`, - mockResponse(defaultResponse, response) - ); - }; return { setLoadWatchesResponse, @@ -142,18 +134,11 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; export const init = () => { - const server = sinon.fakeServer.create(); - server.respondImmediately = true; - - // Define default response for unhandled requests. - // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, - // and we can mock them all with a 200 instead of mocking each one individually. - server.respondWith([200, {}, 'DefaultResponse']); - - const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + const httpSetup = httpServiceMock.createSetupContract(); + const httpRequestsMockHelpers = registerHttpRequestMockHelpers(httpSetup); return { - server, + httpSetup, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts index 07ced2096e696..4fbcb847022e9 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts @@ -13,7 +13,6 @@ import { setup as watchEditSetup } from './watch_edit.helpers'; export type { TestBed } from '@kbn/test-jest-helpers'; export { getRandomString, findTestSubject } from '@kbn/test-jest-helpers'; -export { wrapBodyResponse, unwrapBodyResponse } from './body_response'; export { setupEnvironment } from './setup_environment'; export const pageHelpers = { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.tsx similarity index 54% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts rename to x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.tsx index 5ba0387d21ba7..f42b452818cc5 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.tsx @@ -5,38 +5,33 @@ * 2.0. */ -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; +import React from 'react'; +import { HttpSetup } from 'src/core/public'; import { init as initHttpRequests } from './http_requests'; +import { mockContextValue } from './app_context.mock'; +import { AppContextProvider } from '../../../public/application/app_context'; import { setHttpClient, setSavedObjectsClient } from '../../../public/application/lib/api'; -const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); -mockHttpClient.interceptors.response.use( - (res) => { - return res.data; - }, - (rej) => { - return Promise.reject(rej); - } -); - const mockSavedObjectsClient = () => { return { find: (_params?: any) => {}, }; }; -export const setupEnvironment = () => { - const { server, httpRequestsMockHelpers } = initHttpRequests(); +export const WithAppDependencies = + (Component: any, httpSetup: HttpSetup) => (props: Record) => { + setHttpClient(httpSetup); - // @ts-ignore - setHttpClient(mockHttpClient); + return ( + + + + ); + }; +export const setupEnvironment = () => { setSavedObjectsClient(mockSavedObjectsClient() as any); - return { - server, - httpRequestsMockHelpers, - }; + return initHttpRequests(); }; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts index 16e4930510efa..4e76a1687114a 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts @@ -6,10 +6,12 @@ */ import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; + import { WatchEdit } from '../../../public/application/sections/watch_edit/components/watch_edit'; import { registerRouter } from '../../../public/application/lib/navigation'; import { ROUTES, WATCH_TYPES } from '../../../common/constants'; -import { withAppContext } from './app_context.mock'; +import { WithAppDependencies } from './setup_environment'; const testBedConfig: AsyncTestBedConfig = { memoryRouter: { @@ -20,8 +22,6 @@ const testBedConfig: AsyncTestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig); - export interface WatchCreateJsonTestBed extends TestBed { actions: { selectTab: (tab: 'edit' | 'simulate') => void; @@ -30,7 +30,8 @@ export interface WatchCreateJsonTestBed extends TestBed => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed(WithAppDependencies(WatchEdit, httpSetup), testBedConfig); const testBed = await initTestBed(); /** diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts index cbfdac67597e1..5a8d7b23e0b58 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts @@ -6,10 +6,12 @@ */ import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; + import { WatchEdit } from '../../../public/application/sections/watch_edit/components/watch_edit'; import { registerRouter } from '../../../public/application/lib/navigation'; import { ROUTES, WATCH_TYPES } from '../../../common/constants'; -import { withAppContext } from './app_context.mock'; +import { WithAppDependencies } from './setup_environment'; const testBedConfig: AsyncTestBedConfig = { memoryRouter: { @@ -20,8 +22,6 @@ const testBedConfig: AsyncTestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig); - export interface WatchCreateThresholdTestBed extends TestBed { actions: { clickSubmitButton: () => void; @@ -33,7 +33,8 @@ export interface WatchCreateThresholdTestBed extends TestBed => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed(WithAppDependencies(WatchEdit, httpSetup), testBedConfig); const testBed = await initTestBed(); /** diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts index 9f01750d43593..9eb35f3f1bb32 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts @@ -6,11 +6,13 @@ */ import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; + import { WatchEdit } from '../../../public/application/sections/watch_edit/components/watch_edit'; import { registerRouter } from '../../../public/application/lib/navigation'; import { ROUTES } from '../../../common/constants'; import { WATCH_ID } from './jest_constants'; -import { withAppContext } from './app_context.mock'; +import { WithAppDependencies } from './setup_environment'; const testBedConfig: AsyncTestBedConfig = { memoryRouter: { @@ -21,15 +23,14 @@ const testBedConfig: AsyncTestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig); - export interface WatchEditTestBed extends TestBed { actions: { clickSubmitButton: () => void; }; } -export const setup = async (): Promise => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed(WithAppDependencies(WatchEdit, httpSetup), testBedConfig); const testBed = await initTestBed(); /** diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts index 914eaca62465d..f7aca95039863 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts @@ -13,9 +13,10 @@ import { TestBed, AsyncTestBedConfig, } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; import { WatchList } from '../../../public/application/sections/watch_list/components/watch_list'; import { ROUTES, REFRESH_INTERVALS } from '../../../common/constants'; -import { withAppContext } from './app_context.mock'; +import { WithAppDependencies } from './setup_environment'; const testBedConfig: AsyncTestBedConfig = { memoryRouter: { @@ -24,8 +25,6 @@ const testBedConfig: AsyncTestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(withAppContext(WatchList), testBedConfig); - export interface WatchListTestBed extends TestBed { actions: { selectWatchAt: (index: number) => void; @@ -35,7 +34,8 @@ export interface WatchListTestBed extends TestBed { }; } -export const setup = async (): Promise => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed(WithAppDependencies(WatchList, httpSetup), testBedConfig); const testBed = await initTestBed(); /** diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts index 63892961d8b57..ab2204f4a6dfe 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts @@ -13,21 +13,23 @@ import { TestBed, AsyncTestBedConfig, } from '@kbn/test-jest-helpers'; +import { HttpSetup } from 'src/core/public'; + +import { registerRouter } from '../../../public/application/lib/navigation'; import { WatchStatus } from '../../../public/application/sections/watch_status/components/watch_status'; import { ROUTES } from '../../../common/constants'; import { WATCH_ID } from './jest_constants'; -import { withAppContext } from './app_context.mock'; +import { WithAppDependencies } from './setup_environment'; const testBedConfig: AsyncTestBedConfig = { memoryRouter: { + onRouter: (router) => registerRouter(router), initialEntries: [`${ROUTES.API_ROOT}/watches/watch/${WATCH_ID}/status`], componentRoutePath: `${ROUTES.API_ROOT}/watches/watch/:id/status`, }, doMountAsync: true, }; -const initTestBed = registerTestBed(withAppContext(WatchStatus), testBedConfig); - export interface WatchStatusTestBed extends TestBed { actions: { selectTab: (tab: 'execution history' | 'action statuses') => void; @@ -38,7 +40,8 @@ export interface WatchStatusTestBed extends TestBed { }; } -export const setup = async (): Promise => { +export const setup = async (httpSetup: HttpSetup): Promise => { + const initTestBed = registerTestBed(WithAppDependencies(WatchStatus, httpSetup), testBedConfig); const testBed = await initTestBed(); /** diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts index f9ea51a80ae76..fc518bcab882b 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts @@ -8,15 +8,16 @@ import { act } from 'react-dom/test-utils'; import { getExecuteDetails } from '../../__fixtures__'; +import { API_BASE_PATH } from '../../common/constants'; import { defaultWatch } from '../../public/application/models/watch'; -import { setupEnvironment, pageHelpers, wrapBodyResponse } from './helpers'; +import { setupEnvironment, pageHelpers } from './helpers'; import { WatchCreateJsonTestBed } from './helpers/watch_create_json.helpers'; import { WATCH } from './helpers/jest_constants'; const { setup } = pageHelpers.watchCreateJson; describe(' create route', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchCreateJsonTestBed; beforeAll(() => { @@ -25,12 +26,11 @@ describe(' create route', () => { afterAll(() => { jest.useRealTimers(); - server.restore(); }); describe('on component mount', () => { beforeEach(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); testBed.component.update(); }); @@ -94,31 +94,32 @@ describe(' create route', () => { actions.clickSubmitButton(); }); - const latestRequest = server.requests[server.requests.length - 1]; - const DEFAULT_LOGGING_ACTION_ID = 'logging_1'; const DEFAULT_LOGGING_ACTION_TYPE = 'logging'; const DEFAULT_LOGGING_ACTION_TEXT = 'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.'; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - id: watch.id, - name: watch.name, - type: watch.type, - isNew: true, - isActive: true, - actions: [ - { - id: DEFAULT_LOGGING_ACTION_ID, - type: DEFAULT_LOGGING_ACTION_TYPE, - text: DEFAULT_LOGGING_ACTION_TEXT, - [DEFAULT_LOGGING_ACTION_TYPE]: { + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/${watch.id}`, + expect.objectContaining({ + body: JSON.stringify({ + id: watch.id, + name: watch.name, + type: watch.type, + isNew: true, + isActive: true, + actions: [ + { + id: DEFAULT_LOGGING_ACTION_ID, + type: DEFAULT_LOGGING_ACTION_TYPE, text: DEFAULT_LOGGING_ACTION_TEXT, + [DEFAULT_LOGGING_ACTION_TYPE]: { + text: DEFAULT_LOGGING_ACTION_TEXT, + }, }, - }, - ], - watch: defaultWatch, + ], + watch: defaultWatch, + }), }) ); }); @@ -131,12 +132,13 @@ describe(' create route', () => { form.setInputValue('idInput', watch.id); const error = { - status: 400, + statusCode: 400, error: 'Bad request', message: 'Watch payload is invalid', + response: {}, }; - httpRequestsMockHelpers.setSaveWatchResponse(watch.id, undefined, { body: error }); + httpRequestsMockHelpers.setSaveWatchResponse(watch.id, undefined, error); await act(async () => { actions.clickSubmitButton(); @@ -169,8 +171,6 @@ describe(' create route', () => { actions.clickSimulateButton(); }); - const latestRequest = server.requests[server.requests.length - 1]; - const actionModes = Object.keys(defaultWatch.actions).reduce( (actionAccum: any, action) => { actionAccum[action] = 'simulate'; @@ -188,12 +188,15 @@ describe(' create route', () => { watch: defaultWatch, }; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - executeDetails: getExecuteDetails({ - actionModes, + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/execute`, + expect.objectContaining({ + body: JSON.stringify({ + executeDetails: getExecuteDetails({ + actionModes, + }), + watch: executedWatch, }), - watch: executedWatch, }) ); }); @@ -230,8 +233,6 @@ describe(' create route', () => { }); component.update(); - const latestRequest = server.requests[server.requests.length - 1]; - const actionModes = Object.keys(defaultWatch.actions).reduce( (actionAccum: any, action) => { actionAccum[action] = ACTION_MODE; @@ -252,19 +253,23 @@ describe(' create route', () => { const triggeredTime = `now+${TRIGGERED_TIME}s`; const scheduledTime = `now+${SCHEDULED_TIME}s`; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - executeDetails: getExecuteDetails({ - triggerData: { - triggeredTime, - scheduledTime, - }, - ignoreCondition: IGNORE_CONDITION, - actionModes, + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/execute`, + expect.objectContaining({ + body: JSON.stringify({ + executeDetails: getExecuteDetails({ + triggerData: { + triggeredTime, + scheduledTime, + }, + ignoreCondition: IGNORE_CONDITION, + actionModes, + }), + watch: executedWatch, }), - watch: executedWatch, }) ); + expect(exists('simulateResultsFlyout')).toBe(true); expect(find('simulateResultsFlyoutTitle').text()).toEqual('Simulation results'); }); diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index 52c3a69938d74..2a70b4852c77a 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import axios from 'axios'; +import { HttpFetchOptionsWithPath } from 'kibana/public'; +import { WATCH_ID } from './helpers/jest_constants'; import { getExecuteDetails } from '../../__fixtures__'; -import { WATCH_TYPES } from '../../common/constants'; -import { setupEnvironment, pageHelpers, wrapBodyResponse, unwrapBodyResponse } from './helpers'; +import { WATCH_TYPES, API_BASE_PATH } from '../../common/constants'; +import { setupEnvironment, pageHelpers } from './helpers'; import { WatchCreateThresholdTestBed } from './helpers/watch_create_threshold.helpers'; const WATCH_NAME = 'my_test_watch'; @@ -23,6 +23,18 @@ const MATCH_INDICES = ['index1']; const ES_FIELDS = [{ name: '@timestamp', type: 'date' }]; +// Since watchID's are dynamically created, we have to mock +// the function that generates them in order to be able to match +// against it. +jest.mock('uuid/v4', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { WATCH_ID: watchId } = require('./helpers/jest_constants'); + + return function () { + return watchId; + }; +}); + const SETTINGS = { action_types: { email: { enabled: true }, @@ -36,24 +48,15 @@ const SETTINGS = { }; const WATCH_VISUALIZE_DATA = { - count: [ - [1559404800000, 14], - [1559448000000, 196], - [1559491200000, 44], - ], + visualizeData: { + count: [ + [1559404800000, 14], + [1559448000000, 196], + [1559491200000, 44], + ], + }, }; -const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); - -jest.mock('../../public/application/lib/api', () => { - const original = jest.requireActual('../../public/application/lib/api'); - - return { - ...original, - getHttpClient: () => mockHttpClient, - }; -}); - jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -77,7 +80,7 @@ jest.mock('@elastic/eui', () => { const { setup } = pageHelpers.watchCreateThreshold; describe(' create route', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchCreateThresholdTestBed; beforeAll(() => { @@ -86,14 +89,15 @@ describe(' create route', () => { afterAll(() => { jest.useRealTimers(); - server.restore(); }); describe('on component mount', () => { beforeEach(async () => { - testBed = await setup(); - const { component } = testBed; - component.update(); + await act(async () => { + testBed = await setup(httpSetup); + }); + + testBed.component.update(); }); test('should set the correct page title', () => { @@ -159,6 +163,7 @@ describe(' create route', () => { find('indicesComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox form.setInputValue('watchTimeFieldSelect', '@timestamp'); }); + component.update(); expect(find('saveWatchButton').props().disabled).toBe(false); @@ -247,11 +252,8 @@ describe(' create route', () => { actions.clickSimulateButton(); }); - // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - const thresholdWatch = { - id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: WATCH_ID, name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -280,16 +282,19 @@ describe(' create route', () => { threshold: 1000, }; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - executeDetails: getExecuteDetails({ - actionModes: { - logging_1: 'force_execute', - }, - ignoreCondition: true, - recordExecution: false, + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/execute`, + expect.objectContaining({ + body: JSON.stringify({ + executeDetails: getExecuteDetails({ + actionModes: { + logging_1: 'force_execute', + }, + ignoreCondition: true, + recordExecution: false, + }), + watch: thresholdWatch, }), - watch: thresholdWatch, }) ); }); @@ -309,11 +314,8 @@ describe(' create route', () => { actions.clickSimulateButton(); }); - // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - const thresholdWatch = { - id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: WATCH_ID, name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -341,16 +343,19 @@ describe(' create route', () => { threshold: 1000, }; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - executeDetails: getExecuteDetails({ - actionModes: { - index_1: 'force_execute', - }, - ignoreCondition: true, - recordExecution: false, + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/execute`, + expect.objectContaining({ + body: JSON.stringify({ + executeDetails: getExecuteDetails({ + actionModes: { + index_1: 'force_execute', + }, + ignoreCondition: true, + recordExecution: false, + }), + watch: thresholdWatch, }), - watch: thresholdWatch, }) ); }); @@ -371,11 +376,8 @@ describe(' create route', () => { actions.clickSimulateButton(); }); - // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - const thresholdWatch = { - id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: WATCH_ID, name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -406,16 +408,19 @@ describe(' create route', () => { threshold: 1000, }; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - executeDetails: getExecuteDetails({ - actionModes: { - slack_1: 'force_execute', - }, - ignoreCondition: true, - recordExecution: false, + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/execute`, + expect.objectContaining({ + body: JSON.stringify({ + executeDetails: getExecuteDetails({ + actionModes: { + slack_1: 'force_execute', + }, + ignoreCondition: true, + recordExecution: false, + }), + watch: thresholdWatch, }), - watch: thresholdWatch, }) ); }); @@ -443,11 +448,8 @@ describe(' create route', () => { actions.clickSimulateButton(); }); - // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - const thresholdWatch = { - id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: WATCH_ID, name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -482,16 +484,19 @@ describe(' create route', () => { threshold: 1000, }; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - executeDetails: getExecuteDetails({ - actionModes: { - email_1: 'force_execute', - }, - ignoreCondition: true, - recordExecution: false, + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/execute`, + expect.objectContaining({ + body: JSON.stringify({ + executeDetails: getExecuteDetails({ + actionModes: { + email_1: 'force_execute', + }, + ignoreCondition: true, + recordExecution: false, + }), + watch: thresholdWatch, }), - watch: thresholdWatch, }) ); }); @@ -535,11 +540,8 @@ describe(' create route', () => { actions.clickSimulateButton(); }); - // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - const thresholdWatch = { - id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: WATCH_ID, name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -576,16 +578,19 @@ describe(' create route', () => { threshold: 1000, }; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - executeDetails: getExecuteDetails({ - actionModes: { - webhook_1: 'force_execute', - }, - ignoreCondition: true, - recordExecution: false, + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/execute`, + expect.objectContaining({ + body: JSON.stringify({ + executeDetails: getExecuteDetails({ + actionModes: { + webhook_1: 'force_execute', + }, + ignoreCondition: true, + recordExecution: false, + }), + watch: thresholdWatch, }), - watch: thresholdWatch, }) ); }); @@ -623,11 +628,8 @@ describe(' create route', () => { actions.clickSimulateButton(); }); - // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - const thresholdWatch = { - id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: WATCH_ID, name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -666,16 +668,19 @@ describe(' create route', () => { threshold: 1000, }; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - executeDetails: getExecuteDetails({ - actionModes: { - jira_1: 'force_execute', - }, - ignoreCondition: true, - recordExecution: false, + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/execute`, + expect.objectContaining({ + body: JSON.stringify({ + executeDetails: getExecuteDetails({ + actionModes: { + jira_1: 'force_execute', + }, + ignoreCondition: true, + recordExecution: false, + }), + watch: thresholdWatch, }), - watch: thresholdWatch, }) ); }); @@ -703,11 +708,8 @@ describe(' create route', () => { actions.clickSimulateButton(); }); - // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - const thresholdWatch = { - id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: WATCH_ID, name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -736,16 +738,19 @@ describe(' create route', () => { threshold: 1000, }; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - executeDetails: getExecuteDetails({ - actionModes: { - pagerduty_1: 'force_execute', - }, - ignoreCondition: true, - recordExecution: false, + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/execute`, + expect.objectContaining({ + body: JSON.stringify({ + executeDetails: getExecuteDetails({ + actionModes: { + pagerduty_1: 'force_execute', + }, + ignoreCondition: true, + recordExecution: false, + }), + watch: thresholdWatch, }), - watch: thresholdWatch, }) ); }); @@ -763,17 +768,14 @@ describe(' create route', () => { }); component.update(); - const latestReqToGetVisualizeData = server.requests.find( - (req) => req.method === 'POST' && req.url === '/api/watcher/watch/visualize' - ); - if (!latestReqToGetVisualizeData) { - throw new Error(`No request found to fetch visualize data.`); - } - - const requestBody = unwrapBodyResponse(latestReqToGetVisualizeData.requestBody); + const lastReq: HttpFetchOptionsWithPath[] = httpSetup.post.mock.calls.pop() || []; + const [requestUrl, watchBody] = lastReq; + // Options contains two dinamically computed timestamps, so it's simpler to just ignore those fields. + const { options, ...body } = JSON.parse((watchBody as Record).body).watch; - expect(requestBody.watch).toEqual({ - id: requestBody.watch.id, // id is dynamic + expect(requestUrl).toBe(`${API_BASE_PATH}/watch/visualize`); + expect(body).toEqual({ + id: WATCH_ID, name: 'my_test_watch', type: 'threshold', isNew: true, @@ -792,8 +794,6 @@ describe(' create route', () => { hasTermsAgg: false, threshold: 1000, }); - - expect(requestBody.options.interval).toBeDefined(); }); }); @@ -813,31 +813,31 @@ describe(' create route', () => { actions.clickSubmitButton(); }); - // Verify request - const latestRequest = server.requests[server.requests.length - 1]; - - const thresholdWatch = { - id: unwrapBodyResponse(latestRequest.requestBody).id, // watch ID is created dynamically - name: WATCH_NAME, - type: WATCH_TYPES.THRESHOLD, - isNew: true, - isActive: true, - actions: [], - index: MATCH_INDICES, - timeField: WATCH_TIME_FIELD, - triggerIntervalSize: 1, - triggerIntervalUnit: 'm', - aggType: 'count', - termSize: 5, - termOrder: 'desc', - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - hasTermsAgg: false, - threshold: 1000, - }; - - expect(latestRequest.requestBody).toEqual(wrapBodyResponse(thresholdWatch)); + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/${WATCH_ID}`, + expect.objectContaining({ + body: JSON.stringify({ + id: WATCH_ID, + name: WATCH_NAME, + type: WATCH_TYPES.THRESHOLD, + isNew: true, + isActive: true, + actions: [], + index: MATCH_INDICES, + timeField: WATCH_TIME_FIELD, + triggerIntervalSize: 1, + triggerIntervalUnit: 'm', + aggType: 'count', + termSize: 5, + termOrder: 'desc', + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + hasTermsAgg: false, + threshold: 1000, + }), + }) + ); }); }); }); diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts index 37f9838f176af..8b0ee0189695b 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts @@ -6,31 +6,18 @@ */ import { act } from 'react-dom/test-utils'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import axios from 'axios'; -import { getRandomString } from '@kbn/test-jest-helpers'; import { getWatch } from '../../__fixtures__'; import { defaultWatch } from '../../public/application/models/watch'; -import { setupEnvironment, pageHelpers, wrapBodyResponse } from './helpers'; +import { setupEnvironment, pageHelpers } from './helpers'; import { WatchEditTestBed } from './helpers/watch_edit.helpers'; -import { WATCH } from './helpers/jest_constants'; - -const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); - -jest.mock('../../public/application/lib/api', () => { - const original = jest.requireActual('../../public/application/lib/api'); - - return { - ...original, - getHttpClient: () => mockHttpClient, - }; -}); +import { WATCH, WATCH_ID } from './helpers/jest_constants'; +import { API_BASE_PATH } from '../../common/constants'; const { setup } = pageHelpers.watchEdit; describe('', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchEditTestBed; beforeAll(() => { @@ -39,14 +26,13 @@ describe('', () => { afterAll(() => { jest.useRealTimers(); - server.restore(); }); describe('Advanced watch', () => { beforeEach(async () => { - httpRequestsMockHelpers.setLoadWatchResponse(WATCH); + httpRequestsMockHelpers.setLoadWatchResponse(WATCH_ID, WATCH); - testBed = await setup(); + testBed = await setup(httpSetup); testBed.component.update(); }); @@ -82,31 +68,32 @@ describe('', () => { actions.clickSubmitButton(); }); - const latestRequest = server.requests[server.requests.length - 1]; - const DEFAULT_LOGGING_ACTION_ID = 'logging_1'; const DEFAULT_LOGGING_ACTION_TYPE = 'logging'; const DEFAULT_LOGGING_ACTION_TEXT = 'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.'; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - id: watch.id, - name: EDITED_WATCH_NAME, - type: watch.type, - isNew: false, - isActive: true, - actions: [ - { - id: DEFAULT_LOGGING_ACTION_ID, - type: DEFAULT_LOGGING_ACTION_TYPE, - text: DEFAULT_LOGGING_ACTION_TEXT, - [DEFAULT_LOGGING_ACTION_TYPE]: { + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/${watch.id}`, + expect.objectContaining({ + body: JSON.stringify({ + id: watch.id, + name: EDITED_WATCH_NAME, + type: watch.type, + isNew: false, + isActive: true, + actions: [ + { + id: DEFAULT_LOGGING_ACTION_ID, + type: DEFAULT_LOGGING_ACTION_TYPE, text: DEFAULT_LOGGING_ACTION_TEXT, + [DEFAULT_LOGGING_ACTION_TYPE]: { + text: DEFAULT_LOGGING_ACTION_TEXT, + }, }, - }, - ], - watch: defaultWatch, + ], + watch: defaultWatch, + }), }) ); }); @@ -115,7 +102,7 @@ describe('', () => { describe('Threshold watch', () => { const watch = getWatch({ - id: getRandomString(), + id: WATCH_ID, type: 'threshold', name: 'my_threshold_watch', timeField: '@timestamp', @@ -130,9 +117,9 @@ describe('', () => { }); beforeEach(async () => { - httpRequestsMockHelpers.setLoadWatchResponse({ watch }); + httpRequestsMockHelpers.setLoadWatchResponse(WATCH_ID, { watch }); - testBed = await setup(); + testBed = await setup(httpSetup); testBed.component.update(); }); @@ -161,8 +148,6 @@ describe('', () => { actions.clickSubmitButton(); }); - const latestRequest = server.requests[server.requests.length - 1]; - const { id, type, @@ -177,25 +162,28 @@ describe('', () => { threshold, } = watch; - expect(latestRequest.requestBody).toEqual( - wrapBodyResponse({ - id, - name: EDITED_WATCH_NAME, - type, - isNew: false, - isActive: true, - actions: [], - timeField, - triggerIntervalSize, - triggerIntervalUnit, - aggType, - termSize, - termOrder: 'desc', - thresholdComparator, - timeWindowSize, - timeWindowUnit, - hasTermsAgg: false, - threshold: threshold && threshold[0], + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/${watch.id}`, + expect.objectContaining({ + body: JSON.stringify({ + id, + name: EDITED_WATCH_NAME, + type, + isNew: false, + isActive: true, + actions: [], + timeField, + triggerIntervalSize, + triggerIntervalUnit, + aggType, + termSize, + termOrder: 'desc', + thresholdComparator, + timeWindowSize, + timeWindowUnit, + hasTermsAgg: false, + threshold: threshold && threshold[0], + }), }) ); }); diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts index 1a396a007dd0c..ac1e7291b187a 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts @@ -7,16 +7,14 @@ import { act } from 'react-dom/test-utils'; import * as fixtures from '../../__fixtures__'; -import { ROUTES } from '../../common/constants'; import { setupEnvironment, pageHelpers, getRandomString, findTestSubject } from './helpers'; import { WatchListTestBed } from './helpers/watch_list.helpers'; - -const { API_ROOT } = ROUTES; +import { API_BASE_PATH } from '../../common/constants'; const { setup } = pageHelpers.watchList; describe('', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchListTestBed; beforeAll(() => { @@ -25,7 +23,6 @@ describe('', () => { afterAll(() => { jest.useRealTimers(); - server.restore(); }); describe('on component mount', () => { @@ -35,7 +32,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadWatchesResponse({ watches: [] }); await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); }); @@ -73,7 +70,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadWatchesResponse({ watches }); await act(async () => { - testBed = await setup(); + testBed = await setup(httpSetup); }); testBed.component.update(); @@ -241,10 +238,10 @@ describe('', () => { confirmButton!.click(); }); - const latestRequest = server.requests[server.requests.length - 1]; - - expect(latestRequest.method).toBe('POST'); - expect(latestRequest.url).toBe(`${API_ROOT}/watches/delete`); + expect(httpSetup.post).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watches/delete`, + expect.anything() + ); }); }); }); diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts index 1b1b813617da6..901ebf156911f 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts @@ -8,12 +8,11 @@ import { act } from 'react-dom/test-utils'; import moment from 'moment'; import { getWatchHistory } from '../../__fixtures__'; -import { ROUTES, WATCH_STATES, ACTION_STATES } from '../../common/constants'; +import { WATCH_STATES, ACTION_STATES } from '../../common/constants'; import { setupEnvironment, pageHelpers } from './helpers'; import { WatchStatusTestBed } from './helpers/watch_status.helpers'; -import { WATCH } from './helpers/jest_constants'; - -const { API_ROOT } = ROUTES; +import { WATCH, WATCH_ID } from './helpers/jest_constants'; +import { API_BASE_PATH } from '../../common/constants'; const { setup } = pageHelpers.watchStatus; @@ -40,7 +39,7 @@ const watch = { }; describe('', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchStatusTestBed; beforeAll(() => { @@ -49,15 +48,14 @@ describe('', () => { afterAll(() => { jest.useRealTimers(); - server.restore(); }); describe('on component mount', () => { beforeEach(async () => { - httpRequestsMockHelpers.setLoadWatchResponse({ watch }); - httpRequestsMockHelpers.setLoadWatchHistoryResponse(watchHistoryItems); + httpRequestsMockHelpers.setLoadWatchResponse(WATCH_ID, { watch }); + httpRequestsMockHelpers.setLoadWatchHistoryResponse(WATCH_ID, watchHistoryItems); - testBed = await setup(); + testBed = await setup(httpSetup); testBed.component.update(); }); @@ -127,14 +125,14 @@ describe('', () => { const formattedStartTime = moment(watchHistoryItem.startTime).format(); - httpRequestsMockHelpers.setLoadWatchHistoryItemResponse({ watchHistoryItem }); + httpRequestsMockHelpers.setLoadWatchHistoryItemResponse(WATCH_ID, { watchHistoryItem }); await actions.clickWatchExecutionAt(0, formattedStartTime); - const latestRequest = server.requests[server.requests.length - 1]; - - expect(latestRequest.method).toBe('GET'); - expect(latestRequest.url).toBe(`${API_ROOT}/history/${watchHistoryItem.id}`); + expect(httpSetup.get).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/history/${watchHistoryItem.id}`, + expect.anything() + ); expect(exists('watchHistoryDetailFlyout')).toBe(true); }); @@ -179,10 +177,10 @@ describe('', () => { }); component.update(); - const latestRequest = server.requests[server.requests.length - 1]; - - expect(latestRequest.method).toBe('POST'); - expect(latestRequest.url).toBe(`${API_ROOT}/watches/delete`); + expect(httpSetup.post).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watches/delete`, + expect.anything() + ); }); }); @@ -190,7 +188,7 @@ describe('', () => { test('should send the correct HTTP request to deactivate and activate a watch', async () => { const { actions } = testBed; - httpRequestsMockHelpers.setDeactivateWatchResponse({ + httpRequestsMockHelpers.setDeactivateWatchResponse(WATCH_ID, { watchStatus: { state: WATCH_STATES.DISABLED, isActive: false, @@ -199,12 +197,12 @@ describe('', () => { await actions.clickToggleActivationButton(); - const deactivateRequest = server.requests[server.requests.length - 1]; - - expect(deactivateRequest.method).toBe('PUT'); - expect(deactivateRequest.url).toBe(`${API_ROOT}/watch/${watch.id}/deactivate`); + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/${watch.id}/deactivate`, + expect.anything() + ); - httpRequestsMockHelpers.setActivateWatchResponse({ + httpRequestsMockHelpers.setActivateWatchResponse(WATCH_ID, { watchStatus: { state: WATCH_STATES.FIRING, isActive: true, @@ -213,10 +211,10 @@ describe('', () => { await actions.clickToggleActivationButton(); - const activateRequest = server.requests[server.requests.length - 1]; - - expect(activateRequest.method).toBe('PUT'); - expect(activateRequest.url).toBe(`${API_ROOT}/watch/${watch.id}/activate`); + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/watch/${watch.id}/activate`, + expect.anything() + ); }); }); @@ -242,7 +240,7 @@ describe('', () => { test('should allow an action to be acknowledged', async () => { const { actions, table } = testBed; - httpRequestsMockHelpers.setAcknowledgeWatchResponse({ + httpRequestsMockHelpers.setAcknowledgeWatchResponse(WATCH_ID, ACTION_ID, { watchStatus: { state: WATCH_STATES.FIRING, isActive: true, @@ -259,11 +257,12 @@ describe('', () => { await actions.clickAcknowledgeButton(0); - const latestRequest = server.requests[server.requests.length - 1]; - - expect(latestRequest.method).toBe('PUT'); - expect(latestRequest.url).toBe( - `${API_ROOT}/watch/${watch.id}/action/${ACTION_ID}/acknowledge` + // In previous tests we make calls to activate and deactivate using the put method, + // so we need to expect that the acknowledge api call will be the third. + const indexOfAcknowledgeApiCall = 3; + expect(httpSetup.put).toHaveBeenNthCalledWith( + indexOfAcknowledgeApiCall, + `${API_BASE_PATH}/watch/${watch.id}/action/${ACTION_ID}/acknowledge` ); const { tableCellsValues } = table.getMetaData('watchActionStatusTable'); diff --git a/x-pack/plugins/watcher/common/constants/index.ts b/x-pack/plugins/watcher/common/constants/index.ts index 4d497ed1ea67f..153d4e087b064 100644 --- a/x-pack/plugins/watcher/common/constants/index.ts +++ b/x-pack/plugins/watcher/common/constants/index.ts @@ -16,7 +16,7 @@ export { LISTS } from './lists'; export { PAGINATION } from './pagination'; export { PLUGIN } from './plugin'; export { REFRESH_INTERVALS } from './refresh_intervals'; -export { ROUTES } from './routes'; +export { ROUTES, API_BASE_PATH } from './routes'; export { SORT_ORDERS } from './sort_orders'; export { TIME_UNITS } from './time_units'; export { WATCH_STATE_COMMENTS } from './watch_state_comments'; diff --git a/x-pack/plugins/watcher/common/constants/routes.ts b/x-pack/plugins/watcher/common/constants/routes.ts index c45c699c8e1bb..c7df203bb75da 100644 --- a/x-pack/plugins/watcher/common/constants/routes.ts +++ b/x-pack/plugins/watcher/common/constants/routes.ts @@ -5,6 +5,8 @@ * 2.0. */ +export const API_BASE_PATH = '/api/watcher'; + export const ROUTES: { [key: string]: string } = { - API_ROOT: '/api/watcher', + API_ROOT: API_BASE_PATH, }; From f4f145dedc812db22ed0e274bb0488239e552a89 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 31 Mar 2022 09:20:39 +0200 Subject: [PATCH 108/108] [Discover] Show a fallback empty message when no results are found (#128754) * [Discover] Show a fallback empty message in Discover UI when no results are found * [Discover] Update code style * [Discover] Refactor more and extract into separate components * [Discover] Revert test id * [Discover] Update code style Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/no_results/no_results.test.tsx | 12 ++- .../main/components/no_results/no_results.tsx | 16 ++- .../no_results/no_results_helper.tsx | 102 ------------------ .../no_results_suggestions/index.ts | 9 ++ .../no_results_suggestion_default.tsx | 24 +++++ .../no_results_suggestion_when_filters.tsx | 51 +++++++++ .../no_results_suggestion_when_query.tsx | 31 ++++++ .../no_results_suggestion_when_time_range.tsx | 34 ++++++ .../no_results_suggestions.tsx | 55 ++++++++++ 9 files changed, 219 insertions(+), 115 deletions(-) delete mode 100644 src/plugins/discover/public/application/main/components/no_results/no_results_helper.tsx create mode 100644 src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/index.ts create mode 100644 src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_default.tsx create mode 100644 src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_filters.tsx create mode 100644 src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_query.tsx create mode 100644 src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_time_range.tsx create mode 100644 src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx index 4dc1a5feda5dc..cbfd6e05cb646 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx @@ -34,10 +34,11 @@ function mountAndFindSubjects(props: Omit { Object { "adjustFilters": false, "adjustSearch": false, + "adjustTimeRange": false, + "checkIndices": true, "disableFiltersButton": false, "errorMsg": false, "mainMsg": true, - "timeFieldMsg": false, } `); }); @@ -68,10 +70,11 @@ describe('DiscoverNoResults', () => { Object { "adjustFilters": false, "adjustSearch": false, + "adjustTimeRange": true, + "checkIndices": false, "disableFiltersButton": false, "errorMsg": false, "mainMsg": true, - "timeFieldMsg": true, } `); }); @@ -101,10 +104,11 @@ describe('DiscoverNoResults', () => { Object { "adjustFilters": false, "adjustSearch": false, + "adjustTimeRange": false, + "checkIndices": false, "disableFiltersButton": false, "errorMsg": true, "mainMsg": false, - "timeFieldMsg": false, } `); }); diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx index aaaad49b1f611..223938dedf303 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx @@ -17,7 +17,7 @@ import { EuiTitle, } from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../data/public'; -import { AdjustSearch, getTimeFieldMessage } from './no_results_helper'; +import { NoResultsSuggestions } from './no_results_suggestions'; import './_no_results.scss'; import { NoResultsIllustration } from './assets/no_results_illustration'; @@ -54,14 +54,12 @@ export function DiscoverNoResults({ - {isTimeBased && getTimeFieldMessage()} - {(hasFilters || hasQuery) && ( - - )} + diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_helper.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_helper.tsx deleted file mode 100644 index b5a52d40e1939..0000000000000 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_helper.tsx +++ /dev/null @@ -1,102 +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 React, { Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiDescriptionList, - EuiDescriptionListTitle, - EuiLink, - EuiDescriptionListDescription, - EuiSpacer, -} from '@elastic/eui'; - -export function getTimeFieldMessage() { - return ( - - - - - - - - - - - ); -} - -interface AdjustSearchProps { - onDisableFilters: () => void; - hasFilters?: boolean; - hasQuery?: boolean; -} - -export function AdjustSearch({ hasFilters, hasQuery, onDisableFilters }: AdjustSearchProps) { - return ( - - {hasQuery && ( - <> - - - - - - - - - - - )} - {hasFilters && ( - <> - - - - - - - - - - ), - }} - /> - - - - )} - - ); -} diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/index.ts b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/index.ts new file mode 100644 index 0000000000000..89b11a4f9d66e --- /dev/null +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/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 { NoResultsSuggestions } from './no_results_suggestions'; diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_default.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_default.tsx new file mode 100644 index 0000000000000..b232b4138ea69 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_default.tsx @@ -0,0 +1,24 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { EuiDescriptionList, EuiDescriptionListDescription } from '@elastic/eui'; + +export function NoResultsSuggestionDefault() { + return ( + + + + + + ); +} diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_filters.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_filters.tsx new file mode 100644 index 0000000000000..b153f6046b104 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_filters.tsx @@ -0,0 +1,51 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiLink, + EuiDescriptionListDescription, +} from '@elastic/eui'; + +export interface NoResultsSuggestionWhenFiltersProps { + onDisableFilters: () => void; +} + +export function NoResultsSuggestionWhenFilters({ + onDisableFilters, +}: NoResultsSuggestionWhenFiltersProps) { + return ( + + + + + + + + + ), + }} + /> + + + ); +} diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_query.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_query.tsx new file mode 100644 index 0000000000000..166b2a7f742cd --- /dev/null +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_query.tsx @@ -0,0 +1,31 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; + +export function NoResultsSuggestionWhenQuery() { + return ( + + + + + + + + + ); +} diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_time_range.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_time_range.tsx new file mode 100644 index 0000000000000..434d6025b950e --- /dev/null +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_time_range.tsx @@ -0,0 +1,34 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; + +export function NoResultsSuggestionWhenTimeRange() { + return ( + + + + + + + + + ); +} diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx new file mode 100644 index 0000000000000..595ca61225ebb --- /dev/null +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiSpacer } from '@elastic/eui'; +import { NoResultsSuggestionDefault } from './no_results_suggestion_default'; +import { + NoResultsSuggestionWhenFilters, + NoResultsSuggestionWhenFiltersProps, +} from './no_results_suggestion_when_filters'; +import { NoResultsSuggestionWhenQuery } from './no_results_suggestion_when_query'; +import { NoResultsSuggestionWhenTimeRange } from './no_results_suggestion_when_time_range'; + +interface NoResultsSuggestionProps { + hasFilters?: boolean; + hasQuery?: boolean; + isTimeBased?: boolean; + onDisableFilters: NoResultsSuggestionWhenFiltersProps['onDisableFilters']; +} + +export function NoResultsSuggestions({ + isTimeBased, + hasFilters, + hasQuery, + onDisableFilters, +}: NoResultsSuggestionProps) { + const canAdjustSearchCriteria = isTimeBased || hasFilters || hasQuery; + + if (canAdjustSearchCriteria) { + return ( + <> + {isTimeBased && } + {hasQuery && ( + <> + + + + )} + {hasFilters && ( + <> + + + + )} + + ); + } + + return ; +}