From 54bf02575470b8f68c26b216814d27cff2e9577a Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Fri, 6 Oct 2023 15:41:14 +0200 Subject: [PATCH 01/43] feat/intial_timeline_ui --- .../components/and_or_badge/rounded_badge.tsx | 3 +- .../common/components/sourcerer/helpers.tsx | 6 +- .../common/components/sourcerer/trigger.tsx | 1 + .../flyout/add_to_case_button/index.tsx | 124 ++++++++++++++++ .../components/flyout/header/index.tsx | 83 ++++++++--- .../components/flyout/header/kpis_new.tsx | 140 ++++++++++++++++++ .../flyout/header/timeline_context_menu.tsx | 137 +++++++++++++++++ .../timeline/data_providers/index.tsx | 100 ++++++++++--- .../timeline/header/edit_timeline_button.tsx | 47 ++++++ .../components/timeline/header/index.tsx | 9 +- .../timelines/components/timeline/index.tsx | 3 +- .../timeline/properties/helpers.tsx | 25 ++++ .../timeline/query_tab_content/index.tsx | 23 +-- .../search_or_filter/search_or_filter.tsx | 65 +++++--- 14 files changed, 675 insertions(+), 91 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.tsx index 83e2ee4a69ff1..47d73a6d8262e 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.tsx @@ -22,7 +22,8 @@ const RoundBadge = styled(EuiBadge)` margin: 0 5px 0 5px; padding: 7px 6px 4px 6px; user-select: none; - width: 34px; + width: 40px; + height: 40px; .euiBadge__content { position: relative; top: -1px; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx index 87874da00ced9..0f6c37431c41c 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx @@ -7,7 +7,7 @@ import React from 'react'; import type { EuiSuperSelectOption, EuiFormRowProps } from '@elastic/eui'; -import { EuiIcon, EuiBadge, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import { EuiIcon, EuiBadge, EuiButtonEmpty, EuiFormRow, EuiButton } from '@elastic/eui'; import styled, { css } from 'styled-components'; import type { sourcererModel } from '../../store/sourcerer'; @@ -23,10 +23,10 @@ export const StyledFormRow = styled(EuiFormRow)` max-width: none; `; -export const StyledButton = styled(EuiButtonEmpty)` +export const StyledButton = styled(EuiButton)` &:enabled:focus, &:focus { - background-color: transparent; + /* background-color: transparent; */ } `; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx index e1c1e405bd52b..a0b5d7e4d69df 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx @@ -74,6 +74,7 @@ export const TriggerComponent: FC = ({ aria-label={i18n.DATA_VIEW} data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-trigger' : 'sourcerer-trigger'} flush="left" + color="primary" iconSide="right" iconType="arrowDown" disabled={disabled} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index c25d48ad03dd9..06cef8848bd18 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -27,6 +27,130 @@ interface Props { timelineId: string; } +export const useTimelineAddToCaseAction = ({ timelineId }: { timelineId: string }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { + cases, + application: { navigateToApp }, + } = useKibana().services; + const dispatch = useDispatch(); + const { + graphEventId, + savedObjectId, + status: timelineStatus, + title: timelineTitle, + timelineType, + } = useDeepEqualSelector((state) => + pick( + ['graphEventId', 'savedObjectId', 'status', 'title', 'timelineType'], + getTimeline(state, timelineId) ?? timelineDefaults + ) + ); + const [isPopoverOpen, setPopover] = useState(false); + const [isCaseModalOpen, openCaseModal] = useState(false); + + const onRowClick = useCallback( + async (theCase?: CaseUI) => { + openCaseModal(false); + await navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.case, + path: theCase != null ? getCaseDetailsUrl({ id: theCase.id }) : getCreateCaseUrl(), + }); + dispatch( + setInsertTimeline({ + graphEventId, + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle, + }) + ); + }, + [dispatch, graphEventId, navigateToApp, savedObjectId, timelineId, timelineTitle] + ); + + const userCasesPermissions = useGetUserCasesPermissions(); + + const handleButtonClick = useCallback(() => { + setPopover((currentIsOpen) => !currentIsOpen); + }, []); + + const handlePopoverClose = useCallback(() => setPopover(false), []); + + const handleNewCaseClick = useCallback(() => { + handlePopoverClose(); + + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.case, + path: getCreateCaseUrl(), + }).then(() => { + dispatch( + setInsertTimeline({ + graphEventId, + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, + }) + ); + dispatch(showTimeline({ id: TimelineId.active, show: false })); + }); + }, [ + dispatch, + graphEventId, + navigateToApp, + handlePopoverClose, + savedObjectId, + timelineId, + timelineTitle, + ]); + + const handleExistingCaseClick = useCallback(() => { + handlePopoverClose(); + openCaseModal(true); + }, [openCaseModal, handlePopoverClose]); + + const onCaseModalClose = useCallback(() => { + openCaseModal(false); + }, [openCaseModal]); + + const closePopover = useCallback(() => { + setPopover(false); + }, []); + + const button = useMemo( + () => ( + + {i18n.ATTACH_TO_CASE} + + ), + [handleButtonClick, timelineStatus, timelineType] + ); + + const casesModal = useMemo(() => { + return isCaseModalOpen + ? cases.ui.getAllCasesSelectorModal({ + onRowClick, + onClose: onCaseModalClose, + owner: [APP_ID], + permissions: userCasesPermissions, + }) + : null; + }, [onRowClick, isCaseModalOpen, userCasesPermissions, onCaseModalClose, cases.ui]); + + return { + handleNewCaseClick, + handleExistingCaseClick, + casesModal, + }; +}; + const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index bf481d2e21a3d..d9640dfab62e3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -56,6 +56,8 @@ import { TimelineKPIs } from './kpis'; import { setActiveTabTimeline } from '../../../store/timeline/actions'; import { useIsOverflow } from '../../../../common/hooks/use_is_overflow'; +import { TimelineKPIs2 } from './kpis_new'; +import { TimelineContextMenu } from './timeline_context_menu'; interface FlyoutHeaderProps { timelineId: string; @@ -71,7 +73,9 @@ const ActiveTimelinesContainer = styled(EuiFlexItem)` const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); - const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline); + const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView( + SourcererScopeName.timeline + ); const { uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -135,6 +139,40 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryTest] ); + const isActive = useMemo(() => timelineId === TimelineId.active, [timelineId]); + const getStartSelector = useMemo(() => startSelector(), []); + const getEndSelector = useMemo(() => endSelector(), []); + + const timerange: TimerangeInput = useDeepEqualSelector((state) => { + if (isActive) { + return { + from: getStartSelector(state.inputs.timeline), + to: getEndSelector(state.inputs.timeline), + interval: '', + }; + } else { + return { + from: getStartSelector(state.inputs.global), + to: getEndSelector(state.inputs.global), + interval: '', + }; + } + }); + + const isBlankTimeline: boolean = useMemo( + () => + (isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQueryTest.query)) || + combinedQueries?.filterQuery === undefined, + [dataProviders, filters, kqlQueryTest, combinedQueries] + ); + + const [loading, kpis] = useTimelineKpis({ + defaultIndex: selectedPatterns, + timerange, + isBlankTimeline, + filterQuery: combinedQueries?.filterQuery ?? '', + }); + const handleClose = useCallback(() => { dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); focusActiveTimelineButton(); @@ -163,21 +201,28 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline updated={updated} /> - {show && ( - - - {(activeTab === TimelineTabs.query || activeTab === TimelineTabs.eql) && ( - - - - )} + + {/* */} + {/* */} + {/* */} + + + + + + {show && (activeTab === TimelineTabs.query || activeTab === TimelineTabs.eql) && ( + + + + )} + {show && ( = ({ timeline /> - - - )} + )} + + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx new file mode 100644 index 0000000000000..018842fe62a2d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx @@ -0,0 +1,140 @@ +/* + * Copyright 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, useMemo } from 'react'; +import styled from 'styled-components'; + +import { EuiStat, EuiFlexItem, EuiFlexGroup, EuiToolTip, EuiBadge } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { useUiSetting$ } from '../../../../common/lib/kibana'; +import type { TimelineKpiStrategyResponse } from '../../../../../common/search_strategy'; +import { getEmptyValue } from '../../../../common/components/empty_value'; +import * as i18n from './translations'; + +const NoWrapEuiStat = styled(EuiStat)` + & .euiStat__description { + white-space: nowrap; + } +`; + +export const StatsContainer = styled.span` + font-size: ${euiThemeVars.euiFontSizeXS}; + font-weight: ${euiThemeVars.euiFontWeightSemiBold}; + border-right: ${euiThemeVars.euiBorderThin}; + padding-right: 16px; + .smallDot { + width: 3px !important; + display: inline-block; + } + .euiBadge__text { + text-align: center; + width: 100%; + } +`; + +export const TimelineKPIs2 = React.memo( + ({ kpis, isLoading }: { kpis: TimelineKpiStrategyResponse | null; isLoading: boolean }) => { + const kpiFormat = '0,0.[000]a'; + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const formattedKpis = useMemo(() => { + return { + process: kpis === null ? getEmptyValue() : numeral(kpis.processCount).format(kpiFormat), + user: kpis === null ? getEmptyValue() : numeral(kpis.userCount).format(kpiFormat), + host: kpis === null ? getEmptyValue() : numeral(kpis.hostCount).format(kpiFormat), + sourceIp: kpis === null ? getEmptyValue() : numeral(kpis.sourceIpCount).format(kpiFormat), + destinationIp: + kpis === null ? getEmptyValue() : numeral(kpis.destinationIpCount).format(kpiFormat), + }; + }, [kpis]); + const formattedKpiToolTips = useMemo(() => { + return { + process: numeral(kpis?.processCount).format(defaultNumberFormat), + user: numeral(kpis?.userCount).format(defaultNumberFormat), + host: numeral(kpis?.hostCount).format(defaultNumberFormat), + sourceIp: numeral(kpis?.sourceIpCount).format(defaultNumberFormat), + destinationIp: numeral(kpis?.destinationIpCount).format(defaultNumberFormat), + }; + }, [kpis, defaultNumberFormat]); + + const getColor = useCallback((count) => { + if (count === 0) { + return 'danger'; + } + return 'hollow'; + }, []); + + if (!kpis) return null; + + return ( + + + + {`${i18n.PROCESS_KPI_TITLE} : `} + + + {formattedKpis.process} + + + + + + + {`${i18n.USER_KPI_TITLE} : `} + + + {formattedKpis.user} + + + + + + + {`${i18n.HOST_KPI_TITLE} : `} + + + {formattedKpis.host} + + + + + + + {`${i18n.SOURCE_IP_KPI_TITLE} : `} + + + {formattedKpis.sourceIp} + + + + + + + {`${i18n.SOURCE_IP_KPI_TITLE} : `} + + + {formattedKpis.destinationIp} + + + + + + ); + } +); + +TimelineKPIs2.displayName = 'TimelineKPIs'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx new file mode 100644 index 0000000000000..b2d0ad7045654 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx @@ -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 { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiIcon, +} from '@elastic/eui'; +import React, { useState, useCallback, useMemo } from 'react'; +import { useEditTimelineOperation } from '../../timeline/header/edit_timeline_button'; +import { useTimelineAddToFavoriteAction } from '../../timeline/properties/helpers'; +import { useTimelineAddToCaseAction } from '../add_to_case_button'; + +interface Props { + timelineId: string; +} + +export const TimelineContextMenu = ({ timelineId }: Props) => { + const [isContextMenuVisible, setIsContextMenuVisible] = useState(false); + + const toggleContextMenu = useCallback(() => { + setIsContextMenuVisible((prev) => !prev); + }, []); + + const withContextMenuAction = useCallback( + (fn: unknown) => { + return () => { + if (typeof fn === 'function') { + fn(); + } + toggleContextMenu(); + }; + }, + [toggleContextMenu] + ); + + const { openEditTimeline, editTimelineModal } = useEditTimelineOperation({ + timelineId, + }); + + const editTimelineNameDesc = useMemo(() => { + return ( + + {'Edit Timeline'} + + ); + }, [openEditTimeline, withContextMenuAction]); + + const { toggleFavorite, isFavorite } = useTimelineAddToFavoriteAction({ + timelineId, + }); + + const toggleTimelineFavorite = useMemo(() => { + return ( + + {isFavorite ? 'Remove From Favorites' : 'Add to Favorites'} + + ); + }, [withContextMenuAction, toggleFavorite, isFavorite]); + + const { handleNewCaseClick, handleExistingCaseClick, casesModal } = useTimelineAddToCaseAction({ + timelineId, + }); + + const addToNewCase = useMemo(() => { + return ( + + {'Attach to a new Case'} + + ); + }, [handleNewCaseClick, withContextMenuAction]); + + const addToExistingCase = useMemo(() => { + return ( + + {'Attach to a existing Case'} + + ); + }, [handleExistingCaseClick, withContextMenuAction]); + + const contextMenuItems = useMemo( + () => [editTimelineNameDesc, toggleTimelineFavorite, addToNewCase, addToExistingCase], + [editTimelineNameDesc, toggleTimelineFavorite, addToNewCase, addToExistingCase] + ); + + return ( + <> + {casesModal} + {editTimelineModal} + {isFavorite && } + + } + isOpen={isContextMenuVisible} + closePopover={toggleContextMenu} + panelPaddingSize="none" + anchorPosition="downLeft" + panelProps={{ + 'data-test-subj': 'timeline-context-menu', + }} + > + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index 59c9aa8e24c06..ee9bca3031d64 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -6,11 +6,13 @@ */ import { rgba } from 'polished'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { v4 as uuidv4 } from 'uuid'; import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; +import { EuiToolTip, EuiSuperSelect } from '@elastic/eui'; +import { createGlobalStyle } from '@kbn/react-kibana-context-styled'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; @@ -23,12 +25,14 @@ import { timelineSelectors } from '../../../store/timeline'; import { timelineDefaults } from '../../../store/timeline/defaults'; import * as i18n from './translations'; +import { options } from '../search_or_filter/helpers'; interface Props { timelineId: string; } const DropTargetDataProvidersContainer = styled.div` + position: relative; padding: 2px 0 4px 0; .${IS_DRAGGING_CLASS_NAME} & .drop-target-data-providers { @@ -50,10 +54,11 @@ const DropTargetDataProviders = styled.div` flex-direction: column; justify-content: flex-start; padding-bottom: 2px; + padding-top: 20px; position: relative; border: 0.2rem dashed ${({ theme }) => theme.eui.euiColorMediumShade}; border-radius: 5px; - padding: ${({ theme }) => theme.eui.euiSizeXS} 0; + /* padding: ${({ theme }) => theme.eui.euiSizeXS} 0; */ margin: 2px 0 2px 0; max-height: 33vh; min-height: 100px; @@ -84,6 +89,33 @@ const getDroppableId = (id: string): string => * the user to drop anything with a facet count into * the data pro section. */ + +const timelineSelectModeItemsClassName = 'timelineSelectModeItemsClassName'; + +const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; +const searchOrFilterPopoverWidth = '352px'; + +const SearchOrFilterGlobalStyle = createGlobalStyle` + .${timelineSelectModeItemsClassName} { + width: 350px !important; + } + + .${searchOrFilterPopoverClassName}.euiPopover__panel { + width: ${searchOrFilterPopoverWidth} !important; + + .euiSuperSelect__listbox { + width: ${searchOrFilterPopoverWidth} !important; + } + } +`; + +const CustomTooltipDiv = styled.div` + position: absolute; + left: 20px; + transform: translateY(-35%); + z-index: 9999; +`; + export const DataProviders = React.memo(({ timelineId }) => { const { browserFields } = useSourcererDataView(SourcererScopeName.timeline); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -96,28 +128,52 @@ export const DataProviders = React.memo(({ timelineId }) => { ); const droppableId = useMemo(() => getDroppableId(timelineId), [timelineId]); + const handleChange = useCallback( + () => + // (mode: KqlMode) => updateKqlMode({ id: timelineId, kqlMode: mode }), + console.log(timelineId), + [timelineId] + ); + return ( - - + + - {dataProviders != null && dataProviders.length ? ( - - ) : ( - - - - )} - - + + + + + + + {dataProviders != null && dataProviders.length ? ( + + ) : ( + + + + )} + + + ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/edit_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/edit_timeline_button.tsx index ad59ffbbd6288..e9bbc179d6db6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/edit_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/edit_timeline_button.tsx @@ -23,6 +23,53 @@ export interface EditTimelineComponentProps { toolTip?: string; } +export interface UseEditTimelineArgs { + initialFocus?: 'title' | 'description'; + timelineId: string; +} + +export const useEditTimelineOperation = ({ + timelineId, + initialFocus = 'title', +}: UseEditTimelineArgs) => { + const dispatch = useDispatch(); + const getTimelineSaveModal = useMemo(() => getTimelineSaveModalByIdSelector(), []); + const show = useDeepEqualSelector((state) => getTimelineSaveModal(state, timelineId)); + const [showEditTimelineOverlay, setShowEditTimelineOverlay] = useState(false); + + const openEditTimeline = useCallback(() => { + setShowEditTimelineOverlay(true); + }, [setShowEditTimelineOverlay]); + + const closeEditTimeline = useCallback(() => { + setShowEditTimelineOverlay(false); + if (show) { + dispatch( + timelineActions.toggleModalSaveTimeline({ + id: TimelineId.active, + showModalSaveTimeline: false, + }) + ); + } + }, [dispatch, setShowEditTimelineOverlay, show]); + + const editTimelineModal = useMemo(() => { + return (initialFocus === 'title' && show) || showEditTimelineOverlay ? ( + + ) : null; + }, [closeEditTimeline, initialFocus, show, timelineId, showEditTimelineOverlay]); + + return { + openEditTimeline, + editTimelineModal, + }; +}; + export const EditTimelineButton = React.memo( ({ initialFocus, timelineId, toolTip }) => { const dispatch = useDispatch(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx index 56936c1840ba2..cc213c65fd467 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import React from 'react'; import type { FilterManager } from '@kbn/data-plugin/public'; @@ -32,6 +32,8 @@ const TimelineHeaderComponent: React.FC = ({ timelineId, }) => ( <> + + {showCallOutUnauthorizedMsg && ( = ({ size="s" /> )} - {show && } - + + + {show && } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 1bbeb32ae0e87..f856ba6eaef1e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -17,7 +17,7 @@ import { timelineDefaults } from '../../store/timeline/defaults'; import { defaultHeaders } from './body/column_headers/default_headers'; import type { CellValueElementProps } from './cell_rendering'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header'; +import { FlyoutHeaderPanel } from '../flyout/header'; import type { TimelineId, RowRenderer } from '../../../../common/types/timeline'; import { TimelineType } from '../../../../common/api/timeline'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; @@ -201,7 +201,6 @@ const StatefulTimelineComponent: React.FC = ({ data-test-subj="timeline-hide-show-container" > - { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + + const isFavorite = useShallowEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).isFavorite + ); + + const status = useShallowEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).status + ); + + const disableFavoriteButton = status === TimelineStatus.immutable; + const toggleFavorite = useCallback( + () => dispatch(timelineActions.updateIsFavorite({ id: timelineId, isFavorite: !isFavorite })), + [dispatch, timelineId, isFavorite] + ); + + return { + toggleFavorite, + isFavorite, + isFavouriteDisabled: disableFavoriteButton, + }; +}; + const AddToFavoritesButtonComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index e6707f239f399..0e6815ce2dad8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -46,7 +46,6 @@ import type { } from '../../../../../common/types/timeline'; import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; -import { SuperDatePicker } from '../../../../common/components/super_date_picker'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; import type { inputsModel, State } from '../../../../common/store'; import { inputsSelectors } from '../../../../common/store'; @@ -55,18 +54,16 @@ import { timelineDefaults } from '../../../store/timeline/defaults'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { useTimelineEventsCountPortal } from '../../../../common/hooks/use_timeline_events_count'; import type { TimelineModel } from '../../../store/timeline/model'; -import { TimelineDatePickerLock } from '../date_picker_lock'; import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen'; import { activeTimeline } from '../../../containers/active_timeline_context'; import { DetailsPanel } from '../../side_panel'; import { ExitFullScreen } from '../../../../common/components/exit_full_screen'; import { getDefaultControlColumn } from '../body/control_columns'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { Sourcerer } from '../../../../common/components/sourcerer'; import { useLicense } from '../../../../common/hooks/use_license'; import { HeaderActions } from '../../../../common/components/header_actions/header_actions'; const TimelineHeaderContainer = styled.div` - margin-top: 6px; + /* margin-top: 6px; */ width: 100%; `; @@ -80,7 +77,7 @@ const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` &.euiFlyoutHeader { ${({ theme }) => - `padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS};`} + `padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS};`} } `; @@ -361,22 +358,6 @@ export const QueryTabContentComponent: React.FC = ({ setFullScreen={setTimelineFullScreen} /> )} - - - - - - - - {activeTab === TimelineTabs.query && ( - - )} - ( return ( <> - - - - - - - + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + + + ( updateReduxTime={updateReduxTime} /> + + + + + + + From 69d65386c5d924a6a73a8dea7f74dfee42f247dc Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Thu, 12 Oct 2023 16:24:17 +0200 Subject: [PATCH 02/43] more experimental UI --- .../components/flyout/add_to_case_button/index.tsx | 13 +++++++++---- .../timelines/components/flyout/header/index.tsx | 14 ++++++++++++-- .../components/flyout/header/kpis_new.tsx | 1 + .../flyout/header/timeline_context_menu.tsx | 3 ++- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index 06cef8848bd18..cfee1618297bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -6,7 +6,13 @@ */ import { pick } from 'lodash/fp'; -import { EuiButton, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui'; +import { + EuiButton, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiPopover, + EuiButtonEmpty, +} from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -242,8 +248,7 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { const button = useMemo( () => ( - = ({ timelineId }) => { disabled={timelineStatus === TimelineStatus.draft || timelineType !== TimelineType.default} > {i18n.ATTACH_TO_CASE} - + ), [handleButtonClick, timelineStatus, timelineType] ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index d9640dfab62e3..c23e3b6eddcff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -180,6 +180,8 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline const { euiTheme } = useEuiTheme(); + const isUnsaved = useMemo(() => timelineStatus === TimelineStatus.draft, [timelineStatus]); + return ( = ({ timeline > + = ({ timeline updated={updated} /> - {/* */} {/* */} {/* */} - + + + {!isUnsaved ? : null} + {show && (activeTab === TimelineTabs.query || activeTab === TimelineTabs.eql) && ( { return { process: numeral(kpis?.processCount).format(defaultNumberFormat), diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx index b2d0ad7045654..36743aa055a9b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx @@ -19,9 +19,10 @@ import { useTimelineAddToCaseAction } from '../add_to_case_button'; interface Props { timelineId: string; + showIcons: number; } -export const TimelineContextMenu = ({ timelineId }: Props) => { +export const TimelineContextMenu = ({ timelineId, showIcons }: Props) => { const [isContextMenuVisible, setIsContextMenuVisible] = useState(false); const toggleContextMenu = useCallback(() => { From 1f4dd53dab6a13412d0d1422dcb9fa06879c3a70 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Mon, 23 Oct 2023 06:00:17 +0200 Subject: [PATCH 03/43] incremental save --- .../components/header_section/index.tsx | 3 +- .../components/toggle_container/index.tsx | 113 ++++++++++++++ .../toggle_container/translations.ts | 17 +++ .../components/timeline/kpi/collapsed.tsx | 0 .../components/timeline/kpi/index.tsx | 8 + .../timelines/components/timeline/kpi/kpi.tsx | 141 ++++++++++++++++++ .../timeline/query_tab_content/index.tsx | 2 + 7 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/toggle_container/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/toggle_container/translations.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx index ece266087aa89..70c6e6778a604 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx @@ -108,7 +108,7 @@ const HeaderSectionComponent: React.FC = ({ stackHeader, subtitle, title, - titleSize = 'm', + titleSize = 'l', toggleQuery, toggleStatus = true, tooltip, @@ -173,7 +173,6 @@ const HeaderSectionComponent: React.FC = ({ {title} {tooltip && ( <> - {' '} void; + toggleStatus?: boolean; + append?: React.ReactElement; + height?: number; +} + +const PANEL_HEIGHT = 300; +const MOBILE_PANEL_HEIGHT = 500; +const COLLAPSED_HEIGHT = 64; // px + +const StyledPanel = styled(EuiPanel)<{ + height?: number; + $toggleStatus: boolean; +}>` + display: flex; + flex-direction: column; + position: relative; + overflow-x: hidden; + overflow-y: ${({ $toggleStatus }) => ($toggleStatus ? 'auto' : 'hidden')}; + @media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.m}) { + ${({ height, $toggleStatus }) => { + const result = $toggleStatus + ? `height: ${height != null ? height : PANEL_HEIGHT}px;` + : `height: ${COLLAPSED_HEIGHT}px`; + return result; + }} + } + ${({ $toggleStatus }) => $toggleStatus && `height: ${MOBILE_PANEL_HEIGHT}px;`} +`; + +export const ToggleContainer = React.forwardRef< + HTMLDivElement, + PropsWithChildren +>(({ title, onToggle, toggleStatus, append, children, height }, ref) => { + const [localToggleStatus, setLocalToggleStatus] = useState(toggleStatus ?? false); + + useEffect(() => { + if (!toggleStatus) return; + setLocalToggleStatus(toggleStatus); + }, [toggleStatus]); + + const toggle = useCallback(() => { + setLocalToggleStatus((prev: boolean) => { + if (onToggle) onToggle(!prev); + return !prev; + }); + }, [onToggle]); + + return ( +
+ +
+ + + + + + + +

+ {title} +

+
+
+
+ + + {append} + + +
+
+
+
+ ); +}); + +ToggleContainer.displayName = 'ToggleContainer'; diff --git a/x-pack/plugins/security_solution/public/common/components/toggle_container/translations.ts b/x-pack/plugins/security_solution/public/common/components/toggle_container/translations.ts new file mode 100644 index 0000000000000..a4cb2936c9c08 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/toggle_container/translations.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const TOGGLE_CONTAINER_TITLE = (toggleStatus: boolean) => + toggleStatus + ? i18n.translate('xpack.securitySolution.components.toggleContainer.open', { + defaultMessage: 'Open', + }) + : i18n.translate('xpack.securitySolution.components.toggleContainer.closed', { + defaultMessage: 'Closed', + }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.tsx new file mode 100644 index 0000000000000..32a4f8f6e1935 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright 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 { TimelineKpi } from './kpi'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx new file mode 100644 index 0000000000000..b1d1f5e8239f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx @@ -0,0 +1,141 @@ +/* + * Copyright 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 { isEmpty, pick, get } from 'lodash'; +import { useSelector } from 'react-redux'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import type { TimerangeInput } from '@kbn/timelines-plugin/common'; +import { TimelineId } from '../../../../../common/types'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import type { State } from '../../../../common/store'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { ToggleContainer } from '../../../../common/components/toggle_container'; +import { TimelineKPIs2 } from '../../flyout/header/kpis_new'; +import { useTimelineKpis } from '../../../containers/kpis'; +import { useKibana } from '../../../../common/lib/kibana'; +import { timelineSelectors } from '../../../store/timeline'; +import { timelineDefaults } from '../../../store/timeline/defaults'; +import { combineQueries } from '../../../../common/lib/kuery'; +import { + endSelector, + startSelector, +} from '../../../../common/components/super_date_picker/selectors'; + +interface KpiExpandedProps { + timelineId: string; +} + +export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { + const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView( + SourcererScopeName.timeline + ); + + const { uiSettings } = useKibana().services; + const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { + activeTab, + dataProviders, + kqlQuery, + title: timelineTitle, + timelineType, + status: timelineStatus, + updated, + show, + filters, + kqlMode, + } = useDeepEqualSelector((state) => + pick( + [ + 'activeTab', + 'dataProviders', + 'kqlQuery', + 'status', + 'title', + 'timelineType', + 'updated', + 'show', + 'filters', + 'kqlMode', + ], + getTimeline(state, timelineId) ?? timelineDefaults + ) + ); + const isDataInTimeline = useMemo( + () => !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), + [dataProviders, kqlQuery] + ); + + const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterQuerySelector(), []); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)!); + + const kqlQueryExpression = + isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template' + ? ' ' + : kqlQueryTimeline; + const kqlQueryTest = useMemo( + () => ({ query: kqlQueryExpression, language: 'kuery' }), + [kqlQueryExpression] + ); + + const isActive = useMemo(() => timelineId === TimelineId.active, [timelineId]); + const getStartSelector = useMemo(() => startSelector(), []); + const getEndSelector = useMemo(() => endSelector(), []); + + const timerange: TimerangeInput = useDeepEqualSelector((state) => { + if (isActive) { + return { + from: getStartSelector(state.inputs.timeline), + to: getEndSelector(state.inputs.timeline), + interval: '', + }; + } else { + return { + from: getStartSelector(state.inputs.global), + to: getEndSelector(state.inputs.global), + interval: '', + }; + } + }); + + const combinedQueries = useMemo( + () => + combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters: filters ? filters : [], + kqlQuery: kqlQueryTest, + kqlMode, + }), + [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryTest] + ); + + const isBlankTimeline: boolean = useMemo( + () => + (isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQueryTest.query)) || + combinedQueries?.filterQuery === undefined, + [dataProviders, filters, kqlQueryTest, combinedQueries] + ); + + const [loading, kpis] = useTimelineKpis({ + defaultIndex: selectedPatterns, + timerange, + isBlankTimeline, + filterQuery: combinedQueries?.filterQuery ?? '', + }); + + const title = useMemo(() => { + return ; + }, [kpis, loading]); + + return ; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 0e6815ce2dad8..ba4a45762b729 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -62,6 +62,7 @@ import { getDefaultControlColumn } from '../body/control_columns'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { useLicense } from '../../../../common/hooks/use_license'; import { HeaderActions } from '../../../../common/components/header_actions/header_actions'; +import { TimelineKpi } from '../kpi'; const TimelineHeaderContainer = styled.div` /* margin-top: 6px; */ width: 100%; @@ -369,6 +370,7 @@ export const QueryTabContentComponent: React.FC = ({ />
+ Date: Tue, 24 Oct 2023 06:07:27 +0200 Subject: [PATCH 04/43] incremental save --- .../components/toggle_container/index.tsx | 57 +++--- .../visualization_embeddable.tsx | 2 + .../components/flyout/header/kpis_new.tsx | 2 +- .../timelines/components/timeline/kpi/kpi.tsx | 181 +++++++++++++++++- .../timeline/query_tab_content/index.tsx | 2 +- 5 files changed, 208 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/toggle_container/index.tsx b/x-pack/plugins/security_solution/public/common/components/toggle_container/index.tsx index 3e29064f3ba15..3fddc0f346cdd 100644 --- a/x-pack/plugins/security_solution/public/common/components/toggle_container/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/toggle_container/index.tsx @@ -77,33 +77,38 @@ export const ToggleContainer = React.forwardRef< className="header-section-titles" justifyContent="spaceBetween" > - - - - - - -

- {title} -

-
-
-
- - - {append} - - + + + + + + + +

+ {title} +

+
+
+
+
+ + + + {append} + + + + {localToggleStatus ? {children} : null} diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx index 580aad868d5c7..47ed5bfe05f1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx @@ -36,6 +36,7 @@ const VisualizationEmbeddableComponent: React.FC = queryId: id, }); const { indicesExist } = useSourcererDataView(lensProps.scopeId); + console.log('@@', { indicesExist }); const memorizedTimerange = useRef(lensProps.timerange); const getGlobalQuery = inputsSelectors.globalQueryByIdSelector(); @@ -53,6 +54,7 @@ const VisualizationEmbeddableComponent: React.FC = `; const onEmbeddableLoad = useCallback( ({ requests, responses, isLoading }: EmbeddableData) => { + console.log(`@@ embeddables loaded`, { request, response }); dispatch( inputsActions.setQuery({ inputId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx index 7dd45de476167..d8d69741a2cd5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx @@ -26,7 +26,7 @@ const NoWrapEuiStat = styled(EuiStat)` export const StatsContainer = styled.span` font-size: ${euiThemeVars.euiFontSizeXS}; font-weight: ${euiThemeVars.euiFontWeightSemiBold}; - border-right: ${euiThemeVars.euiBorderThin}; + /* border-right: ${euiThemeVars.euiBorderThin}; */ padding-right: 16px; .smallDot { width: 3px !important; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx index b1d1f5e8239f0..d5a3c61c277d5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx @@ -5,17 +5,28 @@ * 2.0. */ -import React, { useMemo } from 'react'; -import { isEmpty, pick, get } from 'lodash'; +import React, { useMemo, useState } from 'react'; +import { isEmpty, get, pick } from 'lodash/fp'; import { useSelector } from 'react-redux'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { TimerangeInput } from '@kbn/timelines-plugin/common'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import type { EuiButtonGroupOptionProps } from '@elastic/eui'; +import { EuiButtonGroup, EuiPanel } from '@elastic/eui'; +import { useLocalStorage } from 'react-use'; +import type { TimeRange, Filter } from '@kbn/es-query'; +import { getAlertsHistogramLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram'; +import { InputsModelId } from '../../../../common/store/inputs/constants'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; +import { FieldSelection } from '../../../../common/components/field_selection'; +import { useEuiComboBoxReset } from '../../../../common/components/use_combo_box_reset'; +import { ToggleContainer } from '../../../../common/components/toggle_container'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { TimelineId } from '../../../../../common/types'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import type { State } from '../../../../common/store'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import { ToggleContainer } from '../../../../common/components/toggle_container'; import { TimelineKPIs2 } from '../../flyout/header/kpis_new'; import { useTimelineKpis } from '../../../containers/kpis'; import { useKibana } from '../../../../common/lib/kibana'; @@ -31,6 +42,14 @@ interface KpiExpandedProps { timelineId: string; } +const StyledEuiPanel = euiStyled(EuiPanel)` + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + max-height: 308px; +`; + export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView( SourcererScopeName.timeline @@ -77,9 +96,10 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)!); const kqlQueryExpression = - isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template' - ? ' ' - : kqlQueryTimeline; + isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) ? ' ' : kqlQueryTimeline; + + console.log('@@', { kqlQueryTimeline, kqlQueryExpression }); + const kqlQueryTest = useMemo( () => ({ query: kqlQueryExpression, language: 'kuery' }), [kqlQueryExpression] @@ -119,6 +139,16 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryTest] ); + console.log('@@', { + dataProviders, + esQueryConfig, + indexPattern, + browserFields, + filters, + kqlQueryTest, + kqlMode, + }); + const isBlankTimeline: boolean = useMemo( () => (isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQueryTest.query)) || @@ -132,10 +162,145 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { isBlankTimeline, filterQuery: combinedQueries?.filterQuery ?? '', }); + const queryId = `TIMELINE_STATS`; + const { toggleStatus, setToggleStatus } = useQueryToggle(queryId); + const timelineStatsTypeOptions: string[] = ['trend', 'table', 'treemap']; + + const [statGroupSelection, setStatGroupSelection] = + useState('trend'); + + const statGroupOptions = timelineStatsTypeOptions.map((item) => getOptionProperties(item)); const title = useMemo(() => { + if (toggleStatus) { + return ( + setStatGroupSelection(id)} + buttonSize="compressed" + color="primary" + data-test-subj="chart-select-tabs" + /> + ); + } return ; - }, [kpis, loading]); + }, [kpis, loading, statGroupOptions, statGroupSelection, toggleStatus]); + + const { + comboboxRef: stackByField0ComboboxRef, + onReset: onResetStackByField0, + setComboboxInputRef: setStackByField0ComboboxRef, + } = useEuiComboBoxReset(); + + const { + comboboxRef: stackByField1ComboboxRef, + onReset: onResetStackByField1, + setComboboxInputRef: setStackByField1ComboboxRef, + } = useEuiComboBoxReset(); + + const [stackByField0, setStackByField0] = useLocalStorage( + 'timeline-stats-stack-by-0', + 'user.name' + ); + + const [stackByField1, setStackByField1] = useLocalStorage( + 'timeline-stats-stack-by-1', + 'host.name' + ); + + const Append = useMemo(() => { + if (toggleStatus) + return ( + + ); + }, [ + toggleStatus, + queryId, + stackByField0, + stackByField1, + setStackByField1, + setStackByField0, + stackByField0ComboboxRef, + stackByField1ComboboxRef, + setStackByField0ComboboxRef, + setStackByField1ComboboxRef, + ]); + + // return ( + // + // <> + // + // + // + // ); + return ( + + {statGroupSelection === 'trend' ? ( + + ) : null} + + ); +}; + +function getOptionProperties(optionId: string): EuiButtonGroupOptionProps { + const timelineStatsOption = { + id: optionId, + 'data-test-subj': `timeline-stat-group-select-${optionId}`, + label: optionId, + value: optionId, + }; + + return timelineStatsOption; +} - return ; +interface TimelineStatsProps { + stackByField: string; + timerange: Pick; + filters: Filter[]; +} + +const TimelineStatsTrend = (props: TimelineStatsProps) => { + const { stackByField, timerange, filters } = props; + + return ( + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index ba4a45762b729..431739c4dd353 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -370,7 +370,7 @@ export const QueryTabContentComponent: React.FC = ({ /> - + Date: Wed, 25 Oct 2023 11:52:16 +0200 Subject: [PATCH 05/43] feat: added timeline action menu + fixed filter badges --- .../common/components/inspect/index.tsx | 6 +- .../common/components/query_bar/index.tsx | 2 +- .../components/flyout/action_menu/index.tsx | 58 ++++++++ .../flyout/action_menu/new_timeline.tsx | 57 ++++++++ .../flyout/action_menu/open_timeline.tsx | 37 ++++++ .../flyout/action_menu/save_timeline.tsx | 28 ++++ .../components/flyout/header/index.tsx | 125 ++++++++---------- .../components/timeline/header/index.tsx | 5 +- .../timeline/kpi/timeline_stats_treemap.tsx | 10 ++ .../timeline/properties/helpers.tsx | 15 ++- .../timeline/query_tab_content/index.tsx | 3 +- .../timeline/search_or_filter/index.tsx | 99 +++++++++++--- 12 files changed, 346 insertions(+), 99 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/open_timeline.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/timeline_stats_treemap.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index b28d3c5fed944..8d2e09e8b6a13 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -80,9 +80,9 @@ const InspectButtonComponent: React.FC = ({ className={BUTTON_CLASS} aria-label={i18n.INSPECT} data-test-subj="inspect-empty-button" - color="text" - iconSide="left" - iconType="inspect" + // color="text" + // iconSide="left" + // iconType="inspect" isDisabled={isButtonDisabled} isLoading={loading} onClick={handleClick} diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index 08818172bca5a..900bc7a94f2aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -158,7 +158,7 @@ export const QueryBar = memo( onSavedQueryUpdated={onSavedQueryUpdated} refreshInterval={refreshInterval} showAutoRefreshOnly={false} - showFilterBar={!hideSavedQuery} + showFilterBar={false} showDatePicker={false} showQueryInput={true} showSaveQuery={true} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx new file mode 100644 index 0000000000000..a316fb2888577 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { TimelineTabs } from '@kbn/securitysolution-data-table'; +import React from 'react'; +import { InspectButton } from '../../../../common/components/inspect'; +import { InputsModelId } from '../../../../common/store/inputs/constants'; +import { AddToCaseButton } from '../add_to_case_button'; +import { NewTimelineAction } from './new_timeline'; +import { OpenTimelineAction } from './open_timeline'; +import { SaveTimelineAction } from './save_timeline'; + +interface TimelineActionMenuProps { + mode?: 'compact' | 'normal'; + timelineId: string; + showInspectButton: boolean; + isInspectButtonDisabled: boolean; + activeTab: TimelineTabs; +} + +export const TimelineActionMenu = ({ + mode = 'normal', + timelineId, + showInspectButton, + activeTab, + isInspectButtonDisabled, +}: TimelineActionMenuProps) => { + return ( + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx new file mode 100644 index 0000000000000..bb34228bfdfa1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx @@ -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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; +import React, { useMemo, useState, useCallback } from 'react'; +import { NewTimeline } from '../../timeline/properties/helpers'; +import { NewTemplateTimeline } from '../../timeline/properties/new_template_timeline'; + +interface NewTimelineActionProps { + timelineId: string; +} + +export const NewTimelineAction = ({ timelineId }: NewTimelineActionProps) => { + const [isPopoverOpen, setPopover] = useState(false); + + const closePopover = useCallback(() => { + setPopover(false); + }, []); + + const togglePopover = useCallback(() => setPopover((prev) => !prev), []); + + const onActionBtnClick = useCallback(() => { + togglePopover(); + }, [togglePopover]); + + const newTimelineActionbtn = useMemo(() => { + return ( + + {`New`} + + ); + }, [onActionBtnClick]); + + return ( + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/open_timeline.tsx new file mode 100644 index 0000000000000..b26cc376f6add --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/open_timeline.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 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 { EuiButtonEmpty } from '@elastic/eui'; +import React, { useState, useCallback } from 'react'; +import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal'; +import type { ActionTimelineToShow } from '../../open_timeline/types'; + +export interface OpenTimelineModalButtonProps {} + +const actionTimelineToHide: ActionTimelineToShow[] = ['createFrom']; + +export const OpenTimelineAction = React.memo(() => { + const [showTimelineModal, setShowTimelineModal] = useState(false); + const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); + const onOpenTimelineModal = useCallback(() => { + setShowTimelineModal(true); + }, []); + + return ( + <> + + {'Open'} + + + {showTimelineModal ? ( + + ) : null} + + ); +}); + +OpenTimelineAction.displayName = 'OpenTimelineModalButton'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx new file mode 100644 index 0000000000000..547540a2595aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.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 { EuiButton } from '@elastic/eui'; +import React from 'react'; +import { useEditTimelineOperation } from '../../timeline/header/edit_timeline_button'; + +interface SaveTimelineActionProps { + timelineId: string; +} + +export const SaveTimelineAction = ({ timelineId }: SaveTimelineActionProps) => { + const { openEditTimeline, editTimelineModal } = useEditTimelineOperation({ + timelineId, + }); + return ( + <> + {editTimelineModal} + + {'Save'} + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index c23e3b6eddcff..dc34a41455959 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -24,7 +24,6 @@ import styled from 'styled-components'; import { FormattedRelative } from '@kbn/i18n-react'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import { InputsModelId } from '../../../../common/store/inputs/constants'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { TimelineTabs, TimelineId } from '../../../../../common/types/timeline'; import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline'; @@ -34,10 +33,8 @@ import { timelineDefaults } from '../../../store/timeline/defaults'; import { AddToFavoritesButton } from '../../timeline/properties/helpers'; import type { TimerangeInput } from '../../../../../common/search_strategy'; import { AddToCaseButton } from '../add_to_case_button'; -import { AddTimelineButton } from '../add_timeline_button'; import { EditTimelineButton } from '../../timeline/header/edit_timeline_button'; import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; -import { InspectButton } from '../../../../common/components/inspect'; import { useTimelineKpis } from '../../../containers/kpis'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import type { TimelineModel } from '../../../store/timeline/model'; @@ -56,8 +53,7 @@ import { TimelineKPIs } from './kpis'; import { setActiveTabTimeline } from '../../../store/timeline/actions'; import { useIsOverflow } from '../../../../common/hooks/use_is_overflow'; -import { TimelineKPIs2 } from './kpis_new'; -import { TimelineContextMenu } from './timeline_context_menu'; +import { TimelineActionMenu } from '../action_menu'; interface FlyoutHeaderProps { timelineId: string; @@ -183,71 +179,66 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline const isUnsaved = useMemo(() => timelineStatus === TimelineStatus.draft, [timelineStatus]); return ( - - - - - - - - {/* */} - {/* */} - {/* */} - - - - - - - {!isUnsaved ? : null} - - {show && (activeTab === TimelineTabs.query || activeTab === TimelineTabs.eql) && ( + <> + + + + + + {/* */} + + {/* */} + + - - )} - {show && ( - - - - - - )} - - - - + {show && ( + + + + + + )} + + + + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx index cc213c65fd467..ceffb91731d81 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx @@ -33,7 +33,7 @@ const TimelineHeaderComponent: React.FC = ({ }) => ( <> - + {showCallOutUnauthorizedMsg && ( = ({ size="s" /> )} - - - {show && } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/timeline_stats_treemap.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/timeline_stats_treemap.tsx new file mode 100644 index 0000000000000..99efd83b38858 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/timeline_stats_treemap.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +type Props = {} + +const TimelineStatsTreemap = (props: Props) => { + return ( + ) +} + +export default TimelineStatsTreemap diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 2bdecb5e2301c..eaca275714929 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -27,6 +27,7 @@ NotesCountBadge.displayName = 'NotesCountBadge'; interface AddToFavoritesButtonProps { timelineId: string; + compact?: boolean; } export const useTimelineAddToFavoriteAction = ({ timelineId }: { timelineId: string }) => { @@ -54,7 +55,10 @@ export const useTimelineAddToFavoriteAction = ({ timelineId }: { timelineId: str }; }; -const AddToFavoritesButtonComponent: React.FC = ({ timelineId }) => { +const AddToFavoritesButtonComponent: React.FC = ({ + timelineId, + compact, +}) => { const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -73,7 +77,14 @@ const AddToFavoritesButtonComponent: React.FC = ({ ti [dispatch, timelineId, isFavorite] ); - return ( + return compact ? ( + + ) : ( = ({ selectedPatterns, } = useSourcererDataView(SourcererScopeName.timeline); - const { uiSettings } = useKibana().services; + const { uiSettings, data } = useKibana().services; const isEnterprisePlus = useLicense().isEnterprise(); const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5; @@ -371,7 +371,6 @@ export const QueryTabContentComponent: React.FC = ({ - + obj != null && typeof obj === 'object' && Object.hasOwn(obj, 'getName'); + const StatefulSearchOrFilterComponent = React.memo( ({ dataProviders, @@ -50,6 +65,40 @@ const StatefulSearchOrFilterComponent = React.memo( updateKqlMode, updateReduxTime, }) => { + const [dataView, setDataView] = useState(); + const { + services: { data }, + } = useKibana(); + + const { indexPattern } = useSourcererDataView(SourcererScopeName.timeline); + + useEffect(() => { + let dv: DataView; + if (isDataView(indexPattern)) { + setDataView(indexPattern); + } else if (!filterQuery) { + const createDataView = async () => { + dv = await data.dataViews.create({ title: indexPattern.title }); + setDataView(dv); + }; + createDataView(); + } + return () => { + if (dv?.id) { + data.dataViews.clearInstanceCache(dv?.id); + } + }; + }, [data.dataViews, indexPattern, filterQuery]); + + const arrDataView = useMemo(() => (dataView != null ? [dataView] : []), [dataView]); + + const onFiltersUpdated = useCallback( + (newFilters: Filter[]) => { + filterManager.setFilters(newFilters); + }, + [filterManager] + ); + const setFiltersInTimeline = useCallback( (newFilters: Filter[]) => setFilters({ @@ -69,25 +118,35 @@ const StatefulSearchOrFilterComponent = React.memo( ); return ( - + <> + + + + + + ); }, (prevProps, nextProps) => { From ef13b22a8ef403733fcf028b2d62f61b34be78bd Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Mon, 30 Oct 2023 10:54:17 +0100 Subject: [PATCH 06/43] timeline ui all changes --- .../drag_drop_context_wrapper.tsx | 8 +- .../drag_and_drop/draggable_wrapper.tsx | 49 +++-- .../common/components/query_bar/index.tsx | 86 +++++--- .../common/components/sourcerer/helpers.tsx | 9 +- .../common/components/sourcerer/index.tsx | 6 +- .../common/components/sourcerer/trigger.tsx | 16 +- .../components/toggle_container/index.tsx | 118 ----------- .../toggle_container/translations.ts | 17 -- .../flyout/header/active_timelines.tsx | 14 +- .../components/flyout/header/index.tsx | 52 +++-- .../components/flyout/header/kpis_new.tsx | 4 +- .../timeline/data_providers/index.tsx | 28 +-- .../timeline/data_providers/providers.tsx | 2 + .../timeline/date_picker_lock/index.tsx | 2 + .../timeline/eql_tab_content/index.tsx | 6 +- .../components/timeline/header/index.tsx | 111 ++++++++--- .../timelines/components/timeline/kpi/kpi.tsx | 186 +----------------- .../timeline/properties/helpers.tsx | 7 +- .../components/timeline/query_bar/index.tsx | 65 ++++-- .../timeline/query_tab_content/index.tsx | 55 +++--- .../timeline/search_or_filter/index.tsx | 111 +++++++---- .../search_or_filter/search_or_filter.tsx | 83 +++++--- .../timeline/search_or_filter/translations.ts | 21 ++ .../timelines/store/timeline/actions.ts | 5 + .../timelines/store/timeline/defaults.ts | 1 + .../public/timelines/store/timeline/model.ts | 2 + .../timelines/store/timeline/reducer.ts | 13 ++ .../timelines/store/timeline/selectors.ts | 3 + 28 files changed, 517 insertions(+), 563 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/toggle_container/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/toggle_container/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index 041ef14721b34..b94705f36d570 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -152,7 +152,12 @@ export const DragDropContextWrapperComponent: React.FC = ({ browserFields [activeTimelineDataProviders, browserFields, dataProviders, dispatch, onAddedToTimeline] ); return ( - + {children} ); @@ -169,6 +174,7 @@ export const DragDropContextWrapper = React.memo( DragDropContextWrapper.displayName = 'DragDropContextWrapper'; const onBeforeCapture = (before: BeforeCapture) => { + document.body.classList.add(IS_DRAGGING_CLASS_NAME); if (!draggableIsField(before)) { document.body.classList.add(IS_DRAGGING_CLASS_NAME); } diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 790a93b99e545..06d263f74508c 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -19,6 +19,7 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { TableId } from '@kbn/securitysolution-data-table'; +import { createPortal } from 'react-dom'; import { dragAndDropActions } from '../../store/drag_and_drop'; import type { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/components/row_renderers_browser/constants'; @@ -37,6 +38,11 @@ import { useDraggableKeyboardWrapper } from './draggable_keyboard_wrapper_hook'; export const DragEffects = styled.div``; DragEffects.displayName = 'DragEffects'; +const portal: HTMLElement = document.createElement('div'); +portal.classList.add('my-super-cool-portal'); +portal.style.position = 'absolute'; +portal.style.top = 'auto'; +portal.style.left = 'auto'; /** * Wraps the `@hello-pangea/dnd` error boundary. See also: @@ -180,6 +186,10 @@ const DraggableOnWrapperComponent: React.FC = ({ [providerRegistered, dispatch, dataProvider.id] ); + useEffect(() => { + document.body.append(portal); + }, []); + useEffect( () => () => { unRegisterProvider(); @@ -188,24 +198,29 @@ const DraggableOnWrapperComponent: React.FC = ({ ); const RenderClone = useCallback( - (provided, snapshot) => ( - -
- { + const output = ( + +
- {render(dataProvider, provided, snapshot)} - -
-
- ), + + {render(dataProvider, provided, snapshot)} + +
+
+ ); + + if (snapshot.isDragging) return createPortal(output, portal); + return output; + }, [dataProvider, registerProvider, render] ); diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index 900bc7a94f2aa..9fd16856dfff7 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -13,8 +13,13 @@ import type { FilterManager, SavedQuery, SavedQueryTimeFilter } from '@kbn/data- import { TimeHistory } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { SearchBarProps } from '@kbn/unified-search-plugin/public'; -import { SearchBar } from '@kbn/unified-search-plugin/public'; +import { FilterItems, SearchBar } from '@kbn/unified-search-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; +import type { HtmlPortalNode } from 'react-reverse-portal'; +import { InPortal } from 'react-reverse-portal'; +import styled from '@emotion/styled'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { EuiFlexGroup } from '@elastic/eui'; import { useKibana } from '../../lib/kibana'; import { convertToQueryType } from './convert_to_query_type'; @@ -36,8 +41,18 @@ export interface QueryBarComponentProps { onSavedQuery: (savedQuery: SavedQuery | undefined) => void; displayStyle?: SearchBarProps['displayStyle']; isDisabled?: boolean; + /* + * If filtersPortalNode provided, filters will rendered in that portal instead + * of in place. + * + * */ + filtersPortalNode?: HtmlPortalNode; } +const FilterItemsContainer = styled(EuiFlexGroup)` + margin-top: ${euiThemeVars.euiSizeXS}; +`; + export const isDataView = (obj: unknown): obj is DataView => obj != null && typeof obj === 'object' && Object.hasOwn(obj, 'getName'); @@ -60,6 +75,7 @@ export const QueryBar = memo( dataTestSubj, displayStyle, isDisabled, + filtersPortalNode, }) => { const { data } = useKibana().services; const [dataView, setDataView] = useState(); @@ -141,33 +157,47 @@ export const QueryBar = memo( const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []); const arrDataView = useMemo(() => (dataView != null ? [dataView] : []), [dataView]); return ( - + <> + + + {filtersPortalNode ? ( + + + + + + ) : null} + ); } ); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx index 0f6c37431c41c..3556e32196573 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx @@ -7,9 +7,10 @@ import React from 'react'; import type { EuiSuperSelectOption, EuiFormRowProps } from '@elastic/eui'; -import { EuiIcon, EuiBadge, EuiButtonEmpty, EuiFormRow, EuiButton } from '@elastic/eui'; +import { EuiIcon, EuiBadge, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; import styled, { css } from 'styled-components'; +import { euiThemeVars } from '@kbn/ui-theme'; import type { sourcererModel } from '../../store/sourcerer'; import * as i18n from './translations'; @@ -23,10 +24,10 @@ export const StyledFormRow = styled(EuiFormRow)` max-width: none; `; -export const StyledButton = styled(EuiButton)` +export const StyledButtonEmpty = styled(EuiButtonEmpty)` &:enabled:focus, &:focus { - /* background-color: transparent; */ + background-color: transparent; } `; @@ -43,7 +44,7 @@ export const PopoverContent = styled.div` `; export const StyledBadge = styled(EuiBadge)` - margin-left: 8px; + margin-left: ${euiThemeVars.euiSizeXS}; &, .euiBadge__text { cursor: pointer; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index a0bb1f3f27ff4..5a2f3050c1590 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -25,7 +25,7 @@ import { useDeepEqualSelector } from '../../hooks/use_selector'; import type { SourcererUrlState } from '../../store/sourcerer/model'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { usePickIndexPatterns } from './use_pick_index_patterns'; -import { FormRow, PopoverContent, StyledButton, StyledFormRow } from './helpers'; +import { FormRow, PopoverContent, StyledButtonEmpty, StyledFormRow } from './helpers'; import { TemporarySourcerer } from './temporary'; import { useSourcererDataView } from '../../containers/sourcerer'; import { useUpdateDataView } from './use_update_data_view'; @@ -338,14 +338,14 @@ export const Sourcerer = React.memo(({ scope: scopeId } )} - {i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE} - + {expandAdvancedOptions && } = ({ } }, [isModified]); + const Button = useMemo( + () => (isTimelineSourcerer ? EuiButton : StyledButtonEmpty), + [isTimelineSourcerer] + ); + const trigger = useMemo( () => ( - = ({ > {i18n.DATA_VIEW} {!disabled && badge} - + ), - [disabled, badge, isTimelineSourcerer, loading, onClick] + [disabled, badge, isTimelineSourcerer, loading, onClick, Button] ); const tooltipContent = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/toggle_container/index.tsx b/x-pack/plugins/security_solution/public/common/components/toggle_container/index.tsx deleted file mode 100644 index 3fddc0f346cdd..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/toggle_container/index.tsx +++ /dev/null @@ -1,118 +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 { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiTitle, EuiPanel } from '@elastic/eui'; -import React, { useState, useEffect, useCallback } from 'react'; -import type { PropsWithChildren } from 'react'; -import styled from 'styled-components'; -import { TOGGLE_CONTAINER_TITLE } from './translations'; - -interface ToggleContainerProps { - title: React.ReactElement | string; - onToggle?: (status: boolean) => void; - toggleStatus?: boolean; - append?: React.ReactElement; - height?: number; -} - -const PANEL_HEIGHT = 300; -const MOBILE_PANEL_HEIGHT = 500; -const COLLAPSED_HEIGHT = 64; // px - -const StyledPanel = styled(EuiPanel)<{ - height?: number; - $toggleStatus: boolean; -}>` - display: flex; - flex-direction: column; - position: relative; - overflow-x: hidden; - overflow-y: ${({ $toggleStatus }) => ($toggleStatus ? 'auto' : 'hidden')}; - @media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.m}) { - ${({ height, $toggleStatus }) => { - const result = $toggleStatus - ? `height: ${height != null ? height : PANEL_HEIGHT}px;` - : `height: ${COLLAPSED_HEIGHT}px`; - return result; - }} - } - ${({ $toggleStatus }) => $toggleStatus && `height: ${MOBILE_PANEL_HEIGHT}px;`} -`; - -export const ToggleContainer = React.forwardRef< - HTMLDivElement, - PropsWithChildren ->(({ title, onToggle, toggleStatus, append, children, height }, ref) => { - const [localToggleStatus, setLocalToggleStatus] = useState(toggleStatus ?? false); - - useEffect(() => { - if (!toggleStatus) return; - setLocalToggleStatus(toggleStatus); - }, [toggleStatus]); - - const toggle = useCallback(() => { - setLocalToggleStatus((prev: boolean) => { - if (onToggle) onToggle(!prev); - return !prev; - }); - }, [onToggle]); - - return ( -
- -
- - - - - - - - -

- {title} -

-
-
-
-
- - - - {append} - - - -
- {localToggleStatus ? {children} : null} -
-
-
- ); -}); - -ToggleContainer.displayName = 'ToggleContainer'; diff --git a/x-pack/plugins/security_solution/public/common/components/toggle_container/translations.ts b/x-pack/plugins/security_solution/public/common/components/toggle_container/translations.ts deleted file mode 100644 index a4cb2936c9c08..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/toggle_container/translations.ts +++ /dev/null @@ -1,17 +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 { i18n } from '@kbn/i18n'; - -export const TOGGLE_CONTAINER_TITLE = (toggleStatus: boolean) => - toggleStatus - ? i18n.translate('xpack.securitySolution.components.toggleContainer.open', { - defaultMessage: 'Open', - }) - : i18n.translate('xpack.securitySolution.components.toggleContainer.closed', { - defaultMessage: 'Closed', - }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx index 439855d246be6..6f9e1b61171b2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx @@ -100,19 +100,19 @@ const ActiveTimelinesComponent: React.FC = ({ justifyContent="flexStart" responsive={false} > - - - - - {title} {!isOpen && ( )} + {timelineStatus === TimelineStatus.draft ? ( + + + + + + ) : null} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index dc34a41455959..269173772e116 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -162,13 +162,6 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline [dataProviders, filters, kqlQueryTest, combinedQueries] ); - const [loading, kpis] = useTimelineKpis({ - defaultIndex: selectedPatterns, - timerange, - isBlankTimeline, - filterQuery: combinedQueries?.filterQuery ?? '', - }); - const handleClose = useCallback(() => { dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); focusActiveTimelineButton(); @@ -176,19 +169,21 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline const { euiTheme } = useEuiTheme(); - const isUnsaved = useMemo(() => timelineStatus === TimelineStatus.draft, [timelineStatus]); - return ( <> - + = ({ timeline updated={updated} /> - {/* */} - {/* */} + + + = ({ timeline responsive={false} alignItems="center" > - - - + {show ? ( + + + + ) : null} {show && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx index d8d69741a2cd5..8700a29e969bf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx @@ -65,13 +65,11 @@ export const TimelineKPIs2 = React.memo( const getColor = useCallback((count) => { if (count === 0) { - return 'danger'; + return 'hollow'; } return 'hollow'; }, []); - if (!kpis) return null; - return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index ee9bca3031d64..9aaea8837f696 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -26,6 +26,8 @@ import { timelineDefaults } from '../../../store/timeline/defaults'; import * as i18n from './translations'; import { options } from '../search_or_filter/helpers'; +import type { KqlMode } from '../../../store/timeline/model'; +import { updateKqlMode } from '../../../store/timeline/actions'; interface Props { timelineId: string; @@ -33,7 +35,7 @@ interface Props { const DropTargetDataProvidersContainer = styled.div` position: relative; - padding: 2px 0 4px 0; + padding: 16px 0 0px 0; .${IS_DRAGGING_CLASS_NAME} & .drop-target-data-providers { background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.1)}; @@ -53,13 +55,13 @@ const DropTargetDataProviders = styled.div` display: flex; flex-direction: column; justify-content: flex-start; - padding-bottom: 2px; - padding-top: 20px; + padding-bottom: 8px; + padding-top: 28px; position: relative; border: 0.2rem dashed ${({ theme }) => theme.eui.euiColorMediumShade}; border-radius: 5px; /* padding: ${({ theme }) => theme.eui.euiSizeXS} 0; */ - margin: 2px 0 2px 0; + margin: 0px 0 0px 0; max-height: 33vh; min-height: 100px; overflow: auto; @@ -128,10 +130,12 @@ export const DataProviders = React.memo(({ timelineId }) => { ); const droppableId = useMemo(() => getDroppableId(timelineId), [timelineId]); + const kqlMode = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).kqlMode + ); + const handleChange = useCallback( - () => - // (mode: KqlMode) => updateKqlMode({ id: timelineId, kqlMode: mode }), - console.log(timelineId), + () => (mode: KqlMode) => updateKqlMode({ id: timelineId, kqlMode: mode }), [timelineId] ); @@ -142,8 +146,8 @@ export const DataProviders = React.memo(({ timelineId }) => { aria-label={i18n.QUERY_AREA_ARIA_LABEL} className="drop-target-data-providers-container" > - - + + (({ timelineId }) => { onChange={handleChange} options={options} popoverProps={{ className: searchOrFilterPopoverClassName }} - valueOfSelected={'filter'} + valueOfSelected={kqlMode} /> - - + + rgba(theme.eui.euiColorSuccess, 0.2)} !important; } `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/index.tsx index c3d3db91430ee..4335e0b806a1b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/index.tsx @@ -39,8 +39,10 @@ const TimelineDatePickerLockComponent = () => { - `padding: 0 ${theme.eui.euiSizeM} ${theme.eui.euiSizeS} ${theme.eui.euiSizeS};`} + ${({ theme }) => `padding: ${theme.eui.euiSizeS} 0 0 0;`} } `; @@ -110,6 +109,7 @@ const FullWidthFlexGroup = styled(EuiFlexGroup)` `; const ScrollableFlexItem = styled(EuiFlexItem)` + ${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`} overflow: hidden; `; @@ -274,7 +274,7 @@ export const EqlTabContentComponent: React.FC = ({ - + {activeTab === TimelineTabs.eql && ( )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx index ceffb91731d81..ffcecc6bf8e7c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx @@ -5,16 +5,22 @@ * 2.0. */ -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; import type { FilterManager } from '@kbn/data-plugin/public'; +import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; +import styled from '@emotion/styled'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { DataProviders } from '../data_providers'; import { StatefulSearchOrFilter } from '../search_or_filter'; import * as i18n from './translations'; import type { TimelineStatusLiteralWithNull } from '../../../../../common/api/timeline'; -import { TimelineStatus } from '../../../../../common/api/timeline'; +import { TimelineType, TimelineStatus } from '../../../../../common/api/timeline'; +import { timelineSelectors } from '../../../store/timeline'; +import { timelineDefaults } from '../../../store/timeline/defaults'; interface Props { filterManager: FilterManager; @@ -24,36 +30,85 @@ interface Props { timelineId: string; } +const DataProvidersContainer = styled.div<{ $shouldShowQueryBuilder: boolean }>` + position: relative; + width: 100%; + transition: 0.5s ease-in-out; + overflow: hidden; + + ${(props) => + props.$shouldShowQueryBuilder + ? `display: block; max-height: 300px; visibility: visible; margin-block-start: 0px;` + : `display: block; max-height: 0px; visibility: hidden; margin-block-start:-${euiThemeVars.euiSizeS};`} + + .${IS_DRAGGING_CLASS_NAME} & { + display: block; + max-height: 300px; + visibility: visible; + margin-block-start: 0px; + } +`; + const TimelineHeaderComponent: React.FC = ({ filterManager, show, showCallOutUnauthorizedMsg, status, timelineId, -}) => ( - <> - - - {showCallOutUnauthorizedMsg && ( - - )} - {status === TimelineStatus.immutable && ( - - )} - {show && } - -); +}) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const getIsDataProviderVisible = useMemo( + () => timelineSelectors.dataProviderVisibilitySelector(), + [] + ); + + const timelineType = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).timelineType + ); + + const isDataProviderVisible = useDeepEqualSelector((state) => + getIsDataProviderVisible(state, timelineId) + ); + + const shouldShowQueryBuilder = isDataProviderVisible || timelineType === TimelineType.template; + + return ( + + + + + {showCallOutUnauthorizedMsg && ( + + + + )} + {status === TimelineStatus.immutable && ( + + + + )} + {show ? ( + + + + ) : null} + + ); +}; export const TimelineHeader = React.memo(TimelineHeaderComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx index d5a3c61c277d5..f48f6bc99b563 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx @@ -5,23 +5,13 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; -import { isEmpty, get, pick } from 'lodash/fp'; +import React, { useMemo } from 'react'; +import { isEmpty, pick } from 'lodash/fp'; import { useSelector } from 'react-redux'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { TimerangeInput } from '@kbn/timelines-plugin/common'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import type { EuiButtonGroupOptionProps } from '@elastic/eui'; -import { EuiButtonGroup, EuiPanel } from '@elastic/eui'; -import { useLocalStorage } from 'react-use'; -import type { TimeRange, Filter } from '@kbn/es-query'; -import { getAlertsHistogramLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram'; -import { InputsModelId } from '../../../../common/store/inputs/constants'; -import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; -import { FieldSelection } from '../../../../common/components/field_selection'; -import { useEuiComboBoxReset } from '../../../../common/components/use_combo_box_reset'; -import { ToggleContainer } from '../../../../common/components/toggle_container'; -import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { EuiPanel } from '@elastic/eui'; import { TimelineId } from '../../../../../common/types'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import type { State } from '../../../../common/store'; @@ -58,18 +48,7 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { const { uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { - activeTab, - dataProviders, - kqlQuery, - title: timelineTitle, - timelineType, - status: timelineStatus, - updated, - show, - filters, - kqlMode, - } = useDeepEqualSelector((state) => + const { dataProviders, kqlQuery, filters, kqlMode } = useDeepEqualSelector((state) => pick( [ 'activeTab', @@ -86,10 +65,6 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { getTimeline(state, timelineId) ?? timelineDefaults ) ); - const isDataInTimeline = useMemo( - () => !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), - [dataProviders, kqlQuery] - ); const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterQuerySelector(), []); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -98,8 +73,6 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { const kqlQueryExpression = isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) ? ' ' : kqlQueryTimeline; - console.log('@@', { kqlQueryTimeline, kqlQueryExpression }); - const kqlQueryTest = useMemo( () => ({ query: kqlQueryExpression, language: 'kuery' }), [kqlQueryExpression] @@ -139,16 +112,6 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryTest] ); - console.log('@@', { - dataProviders, - esQueryConfig, - indexPattern, - browserFields, - filters, - kqlQueryTest, - kqlMode, - }); - const isBlankTimeline: boolean = useMemo( () => (isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQueryTest.query)) || @@ -162,145 +125,10 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { isBlankTimeline, filterQuery: combinedQueries?.filterQuery ?? '', }); - const queryId = `TIMELINE_STATS`; - const { toggleStatus, setToggleStatus } = useQueryToggle(queryId); - const timelineStatsTypeOptions: string[] = ['trend', 'table', 'treemap']; - - const [statGroupSelection, setStatGroupSelection] = - useState('trend'); - - const statGroupOptions = timelineStatsTypeOptions.map((item) => getOptionProperties(item)); - - const title = useMemo(() => { - if (toggleStatus) { - return ( - setStatGroupSelection(id)} - buttonSize="compressed" - color="primary" - data-test-subj="chart-select-tabs" - /> - ); - } - return ; - }, [kpis, loading, statGroupOptions, statGroupSelection, toggleStatus]); - - const { - comboboxRef: stackByField0ComboboxRef, - onReset: onResetStackByField0, - setComboboxInputRef: setStackByField0ComboboxRef, - } = useEuiComboBoxReset(); - - const { - comboboxRef: stackByField1ComboboxRef, - onReset: onResetStackByField1, - setComboboxInputRef: setStackByField1ComboboxRef, - } = useEuiComboBoxReset(); - - const [stackByField0, setStackByField0] = useLocalStorage( - 'timeline-stats-stack-by-0', - 'user.name' - ); - - const [stackByField1, setStackByField1] = useLocalStorage( - 'timeline-stats-stack-by-1', - 'host.name' - ); - - const Append = useMemo(() => { - if (toggleStatus) - return ( - - ); - }, [ - toggleStatus, - queryId, - stackByField0, - stackByField1, - setStackByField1, - setStackByField0, - stackByField0ComboboxRef, - stackByField1ComboboxRef, - setStackByField0ComboboxRef, - setStackByField1ComboboxRef, - ]); - - // return ( - // - // <> - // - // - // - // ); - return ( - - {statGroupSelection === 'trend' ? ( - - ) : null} - - ); -}; - -function getOptionProperties(optionId: string): EuiButtonGroupOptionProps { - const timelineStatsOption = { - id: optionId, - 'data-test-subj': `timeline-stat-group-select-${optionId}`, - label: optionId, - value: optionId, - }; - - return timelineStatsOption; -} - -interface TimelineStatsProps { - stackByField: string; - timerange: Pick; - filters: Filter[]; -} - -const TimelineStatsTrend = (props: TimelineStatsProps) => { - const { stackByField, timerange, filters } = props; return ( - + + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index eaca275714929..430de7d283eb9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -77,12 +77,16 @@ const AddToFavoritesButtonComponent: React.FC = ({ [dispatch, timelineId, isFavorite] ); + const label = isFavorite ? i18n.REMOVE_FROM_FAVORITES : i18n.ADD_TO_FAVORITES; + return compact ? ( ) : ( = ({ onClick={handleClick} data-test-subj={`timeline-favorite-${isFavorite ? 'filled' : 'empty'}-star`} disabled={disableFavoriteButton} + aria-label={label} > - {isFavorite ? i18n.REMOVE_FROM_FAVORITES : i18n.ADD_TO_FAVORITES} + {label} ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index dfc58d1a2310d..56577a8c17b6e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -14,6 +14,7 @@ import deepEqual from 'fast-deep-equal'; import type { Filter, Query } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import type { FilterManager, SavedQuery, SavedQueryTimeFilter } from '@kbn/data-plugin/public'; +import styled from '@emotion/styled'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; @@ -47,6 +48,36 @@ export interface QueryBarTimelineComponentProps { updateReduxTime: DispatchUpdateReduxTime; } +const SearchBarContainer = styled.div` + /* + * + * hide search bar default filters as they are distrubing the layout as shown below + * + * Filters are displayed with QueryBar so below is how is the layout with default filters. + * + * + * ------------------------------ + * -----------------| |------------ + * | DataViewPicker | QueryBar | Date | + * ------------------------------------------------------------- + * | Filters | + * -------------------------------- + * + * This component makes sure that default filters are not rendered and we can saperate display + * them outside query component so that layout is as below: + * + * ----------------------------------------------------------- + * | DataViewPicker | QueryBar | Date | + * ----------------------------------------------------------- + * | Filters | + * ----------------------------------------------------------- + * + * */ + [data-test-subj='globalQueryBar'] [class$='filter_bar-styles--group'] { + display: none; + } +`; + export const TIMELINE_FILTER_DROP_AREA = 'timeline-filter-drop-area'; const getNonDropAreaFilters = (filters: Filter[] = []) => @@ -264,22 +295,24 @@ export const QueryBarTimeline = memo( ); return ( - + + + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index d25caeab355d2..ef649f60e0e5d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -77,8 +77,7 @@ const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` flex-direction: column; &.euiFlyoutHeader { - ${({ theme }) => - `padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS};`} + ${({ theme }) => `padding: ${theme.eui.euiSizeS} 0 0 0;`} } `; @@ -316,10 +315,6 @@ export const QueryTabContentComponent: React.FC = ({ ); }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]); - const isDatePickerDisabled = useMemo(() => { - return (combinedQueries && combinedQueries.kqlError != null) || false; - }, [combinedQueries]); - const leadingControlColumns = useMemo( () => getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({ @@ -342,35 +337,43 @@ export const QueryTabContentComponent: React.FC = ({ refetch={refetch} skip={!canQueryTimeline} /> - + - + {timelineFullScreen && setTimelineFullScreen != null && ( - + + + + + )} + + + + + + + + - - - - ( updateKqlMode, updateReduxTime, }) => { + const dispatch = useDispatch(); + const [dataView, setDataView] = useState(); const { services: { data }, @@ -72,6 +73,15 @@ const StatefulSearchOrFilterComponent = React.memo( const { indexPattern } = useSourcererDataView(SourcererScopeName.timeline); + const getIsDataProviderVisible = useMemo( + () => timelineSelectors.dataProviderVisibilitySelector(), + [] + ); + + const isDataProviderVisible = useDeepEqualSelector((state) => + getIsDataProviderVisible(state, timelineId) + ); + useEffect(() => { let dv: DataView; if (isDataView(indexPattern)) { @@ -117,36 +127,66 @@ const StatefulSearchOrFilterComponent = React.memo( [timelineId, setSavedQueryId] ); + const toggleDataProviderVisibility = useCallback(() => { + dispatch( + setDataProviderVisibility({ id: timelineId, isDataProviderVisible: !isDataProviderVisible }) + ); + }, [isDataProviderVisible, timelineId, dispatch]); + + useEffect(() => { + /* + * If there is a change in data providers and data provider was hidden, + * it must be made visible + * + * */ + if (dataProviders?.length > 0) { + dispatch(setDataProviderVisibility({ id: timelineId, isDataProviderVisible: true })); + } else if (dataProviders?.length === 0) { + dispatch(setDataProviderVisibility({ id: timelineId, isDataProviderVisible: false })); + } + }, [dataProviders, dispatch, timelineId]); + return ( - <> - - - - - - + + + + + + + + + {filters && filters.length > 0 ? ( + + + + + + ) : null} + ); }, (prevProps, nextProps) => { @@ -197,6 +237,7 @@ const makeMapStateToProps = () => { toStr: input.timerange.toStr!, }; }; + return mapStateToProps; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 16a4e1956b566..5a91037ea8213 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; import type { Filter } from '@kbn/es-query'; @@ -22,6 +22,11 @@ import { QueryBarTimeline } from '../query_bar'; import { TimelineDatePickerLock } from '../date_picker_lock'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { Sourcerer } from '../../../../common/components/sourcerer'; +import { + DATA_PROVIDER_HIDDEN_EMPTY, + DATA_PROVIDER_HIDDEN_POPULATED, + DATA_PROVIDER_VISIBLE, +} from './translations'; const timelineSelectModeItemsClassName = 'timelineSelectModeItemsClassName'; const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; @@ -42,10 +47,6 @@ const SearchOrFilterGlobalStyle = createGlobalStyle` } `; -const SourcererFlex = styled(EuiFlexItem)` - align-items: flex-end; -`; - interface Props { dataProviders: DataProvider[]; filterManager: FilterManager; @@ -64,12 +65,11 @@ interface Props { to: string; toStr: string; updateReduxTime: DispatchUpdateReduxTime; + isDataProviderVisible: boolean; + toggleDataProviderVisibility: () => void; } -const SearchOrFilterContainer = styled.div` - ${({ theme }) => `margin-top: ${theme.eui.euiSizeXS};`} - user-select: none; // This should not be here, it makes the entire page inaccessible -`; +const SearchOrFilterContainer = styled.div``; SearchOrFilterContainer.displayName = 'SearchOrFilterContainer'; @@ -98,12 +98,9 @@ export const SearchOrFilter = React.memo( toStr, updateKqlMode, updateReduxTime, + isDataProviderVisible, + toggleDataProviderVisibility, }) => { - const handleChange = useCallback( - (mode: KqlMode) => updateKqlMode({ id: timelineId, kqlMode: mode }), - [timelineId, updateKqlMode] - ); - return ( <> @@ -112,20 +109,6 @@ export const SearchOrFilter = React.memo( gutterSize="xs" alignItems="center" > - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} @@ -149,6 +132,45 @@ export const SearchOrFilter = React.memo( updateReduxTime={updateReduxTime} /> + + + 0 && !isDataProviderVisible + ? DATA_PROVIDER_HIDDEN_POPULATED + : dataProviders?.length === 0 && !isDataProviderVisible + ? DATA_PROVIDER_HIDDEN_EMPTY + : DATA_PROVIDER_VISIBLE + } + > + 0 && !isDataProviderVisible ? 'warning' : 'primary' + } + isSelected={isDataProviderVisible} + iconType={'timeline'} + size="m" + display="base" + aria-label={ + dataProviders?.length > 0 && !isDataProviderVisible + ? DATA_PROVIDER_HIDDEN_POPULATED + : dataProviders?.length === 0 && !isDataProviderVisible + ? DATA_PROVIDER_HIDDEN_EMPTY + : DATA_PROVIDER_VISIBLE + } + onClick={toggleDataProviderVisibility} + /> + + + + + + ( disabled={false} /> - - - diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts index 560a82a329684..791d861ae2590 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts @@ -70,3 +70,24 @@ export const FILTER_OR_SEARCH_WITH_KQL = i18n.translate( defaultMessage: 'Filter or Search with KQL', } ); + +export const DATA_PROVIDER_HIDDEN_POPULATED = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.dataProviderToggle.hiddenAndPopulated', + { + defaultMessage: 'Query Builder is hidden. Click here to see the existing Queries', + } +); + +export const DATA_PROVIDER_VISIBLE = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.dataProviderToggle.visible', + { + defaultMessage: 'Click here to hide Query builder', + } +); + +export const DATA_PROVIDER_HIDDEN_EMPTY = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.dataProviderToggle.hiddenAndEmpty', + { + defaultMessage: 'Click here to show the empty Query builder', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index dee16b6088c49..2ec0f5c43936c 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -281,3 +281,8 @@ export const setIsDiscoverSavedSearchLoaded = actionCreator<{ id: string; isDiscoverSavedSearchLoaded: boolean; }>('SET_IS_DISCOVER_SAVED_SEARCH_LOADED'); + +export const setDataProviderVisibility = actionCreator<{ + id: string; + isDataProviderVisible: boolean; +}>('SET_DATA_PROVIDER_VISIBLITY'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 449a2aa2b13f4..e1c01f226ca78 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -80,6 +80,7 @@ export const timelineDefaults: SubsetTimelineModel & filters: [], savedSearchId: null, isDiscoverSavedSearchLoaded: false, + isDataProviderVisible: false, }; export const getTimelineManageDefaults = (id: string) => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 98e50a11aa734..482c19da24bd8 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -136,6 +136,7 @@ export interface TimelineModel { /* discover saved search Id */ savedSearchId: string | null; isDiscoverSavedSearchLoaded?: boolean; + isDataProviderVisible: boolean; } export type SubsetTimelineModel = Readonly< @@ -191,6 +192,7 @@ export type SubsetTimelineModel = Readonly< | 'filterManager' | 'savedSearchId' | 'isDiscoverSavedSearchLoaded' + | 'isDataProviderVisible' > >; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 704017a10a7ca..69c5316f2e378 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -61,6 +61,7 @@ import { clearEventsLoading, updateSavedSearchId, setIsDiscoverSavedSearchLoaded, + setDataProviderVisibility, } from './actions'; import { @@ -552,4 +553,16 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) }, }, })) + .case(setDataProviderVisibility, (state, { id, isDataProviderVisible }) => { + return { + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + isDataProviderVisible, + }, + }, + }; + }) .build(); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts index ed0d3c0898a72..e42510e7bf061 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts @@ -58,3 +58,6 @@ export const getKqlFilterKuerySelector = () => ? timeline.kqlQuery.filterQuery.kuery : null ); + +export const dataProviderVisibilitySelector = () => + createSelector(selectTimeline, (timeline) => timeline.isDataProviderVisible); From 133756f5be3a5a188386576d0556857b13b4ae25 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:03:29 +0000 Subject: [PATCH 07/43] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/security_solution/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index de11312c2f60e..6a0f90001f8f6 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -177,6 +177,7 @@ "@kbn/core-application-common", "@kbn/openapi-generator", "@kbn/es", - "@kbn/react-kibana-mount" + "@kbn/react-kibana-mount", + "@kbn/react-kibana-context-styled" ] } From 70002c6ea42c54f563f94d6ebd591d381e07658c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:37:46 +0000 Subject: [PATCH 08/43] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../public/timelines/components/timeline/kpi/collapsed.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx index e69de29bb2d1d..1fec1c76430eb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx @@ -0,0 +1,6 @@ +/* + * Copyright 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. + */ From ba4bcc07bd7cb56ba407c4be698507e5b2120354 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Mon, 30 Oct 2023 13:19:55 +0100 Subject: [PATCH 09/43] fix: add missing translations --- .../drag_drop_context_wrapper.tsx | 11 +- .../drag_and_drop/draggable_wrapper.tsx | 49 +- .../common/components/inspect/index.tsx | 3 - .../common/components/query_bar/index.tsx | 30 +- .../visualization_embeddable.tsx | 2 - .../flyout/action_menu/new_timeline.tsx | 7 +- .../flyout/action_menu/open_timeline.tsx | 13 +- .../flyout/action_menu/save_timeline.tsx | 11 +- .../flyout/action_menu/translations.ts | 57 ++ .../flyout/add_to_case_button/index.tsx | 132 +---- .../components/flyout/header/index.test.tsx | 171 ------ .../components/flyout/header/index.tsx | 506 ------------------ .../components/flyout/header/kpis.tsx | 185 ++++--- .../components/flyout/header/kpis_new.tsx | 139 ----- .../flyout/header/timeline_context_menu.tsx | 138 ----- .../timeline/data_providers/index.tsx | 26 +- .../timeline/data_providers/translations.ts | 7 + .../timelines/components/timeline/kpi/kpi.tsx | 6 +- .../timeline/kpi/timeline_stats_treemap.tsx | 10 - .../timeline/properties/helpers.tsx | 25 - .../components/timeline/query_bar/index.tsx | 6 +- .../timeline/search_or_filter/index.tsx | 8 +- .../timeline/search_or_filter/translations.ts | 7 - 23 files changed, 248 insertions(+), 1301 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/timeline_stats_treemap.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index b94705f36d570..d209a99a2f3ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -151,13 +151,9 @@ export const DragDropContextWrapperComponent: React.FC = ({ browserFields }, [activeTimelineDataProviders, browserFields, dataProviders, dispatch, onAddedToTimeline] ); + return ( - + {children} ); @@ -173,8 +169,7 @@ export const DragDropContextWrapper = React.memo( DragDropContextWrapper.displayName = 'DragDropContextWrapper'; -const onBeforeCapture = (before: BeforeCapture) => { - document.body.classList.add(IS_DRAGGING_CLASS_NAME); +const onBeforeDragStart = (before: BeforeCapture) => { if (!draggableIsField(before)) { document.body.classList.add(IS_DRAGGING_CLASS_NAME); } diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 06d263f74508c..790a93b99e545 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -19,7 +19,6 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { TableId } from '@kbn/securitysolution-data-table'; -import { createPortal } from 'react-dom'; import { dragAndDropActions } from '../../store/drag_and_drop'; import type { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/components/row_renderers_browser/constants'; @@ -38,11 +37,6 @@ import { useDraggableKeyboardWrapper } from './draggable_keyboard_wrapper_hook'; export const DragEffects = styled.div``; DragEffects.displayName = 'DragEffects'; -const portal: HTMLElement = document.createElement('div'); -portal.classList.add('my-super-cool-portal'); -portal.style.position = 'absolute'; -portal.style.top = 'auto'; -portal.style.left = 'auto'; /** * Wraps the `@hello-pangea/dnd` error boundary. See also: @@ -186,10 +180,6 @@ const DraggableOnWrapperComponent: React.FC = ({ [providerRegistered, dispatch, dataProvider.id] ); - useEffect(() => { - document.body.append(portal); - }, []); - useEffect( () => () => { unRegisterProvider(); @@ -198,29 +188,24 @@ const DraggableOnWrapperComponent: React.FC = ({ ); const RenderClone = useCallback( - (provided, snapshot) => { - const output = ( - -
( + +
+ - - {render(dataProvider, provided, snapshot)} - -
-
- ); - - if (snapshot.isDragging) return createPortal(output, portal); - return output; - }, + {render(dataProvider, provided, snapshot)} + +
+
+ ), [dataProvider, registerProvider, render] ); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index 8d2e09e8b6a13..295fbdf84659b 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -80,9 +80,6 @@ const InspectButtonComponent: React.FC = ({ className={BUTTON_CLASS} aria-label={i18n.INSPECT} data-test-subj="inspect-empty-button" - // color="text" - // iconSide="left" - // iconType="inspect" isDisabled={isButtonDisabled} isLoading={loading} onClick={handleClick} diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index 9fd16856dfff7..8cdd48e024e30 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -13,13 +13,8 @@ import type { FilterManager, SavedQuery, SavedQueryTimeFilter } from '@kbn/data- import { TimeHistory } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { SearchBarProps } from '@kbn/unified-search-plugin/public'; -import { FilterItems, SearchBar } from '@kbn/unified-search-plugin/public'; +import { SearchBar } from '@kbn/unified-search-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import type { HtmlPortalNode } from 'react-reverse-portal'; -import { InPortal } from 'react-reverse-portal'; -import styled from '@emotion/styled'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { EuiFlexGroup } from '@elastic/eui'; import { useKibana } from '../../lib/kibana'; import { convertToQueryType } from './convert_to_query_type'; @@ -41,18 +36,8 @@ export interface QueryBarComponentProps { onSavedQuery: (savedQuery: SavedQuery | undefined) => void; displayStyle?: SearchBarProps['displayStyle']; isDisabled?: boolean; - /* - * If filtersPortalNode provided, filters will rendered in that portal instead - * of in place. - * - * */ - filtersPortalNode?: HtmlPortalNode; } -const FilterItemsContainer = styled(EuiFlexGroup)` - margin-top: ${euiThemeVars.euiSizeXS}; -`; - export const isDataView = (obj: unknown): obj is DataView => obj != null && typeof obj === 'object' && Object.hasOwn(obj, 'getName'); @@ -75,7 +60,6 @@ export const QueryBar = memo( dataTestSubj, displayStyle, isDisabled, - filtersPortalNode, }) => { const { data } = useKibana().services; const [dataView, setDataView] = useState(); @@ -185,18 +169,6 @@ export const QueryBar = memo( displayStyle={displayStyle} isDisabled={isDisabled} /> - - {filtersPortalNode ? ( - - - - - - ) : null} ); } diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx index 47ed5bfe05f1a..580aad868d5c7 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx @@ -36,7 +36,6 @@ const VisualizationEmbeddableComponent: React.FC = queryId: id, }); const { indicesExist } = useSourcererDataView(lensProps.scopeId); - console.log('@@', { indicesExist }); const memorizedTimerange = useRef(lensProps.timerange); const getGlobalQuery = inputsSelectors.globalQueryByIdSelector(); @@ -54,7 +53,6 @@ const VisualizationEmbeddableComponent: React.FC = `; const onEmbeddableLoad = useCallback( ({ requests, responses, isLoading }: EmbeddableData) => { - console.log(`@@ embeddables loaded`, { request, response }); dispatch( inputsActions.setQuery({ inputId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx index bb34228bfdfa1..3ca5fd09a32bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx @@ -9,6 +9,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/ import React, { useMemo, useState, useCallback } from 'react'; import { NewTimeline } from '../../timeline/properties/helpers'; import { NewTemplateTimeline } from '../../timeline/properties/new_template_timeline'; +import * as i18n from './translations'; interface NewTimelineActionProps { timelineId: string; @@ -30,7 +31,7 @@ export const NewTimelineAction = ({ timelineId }: NewTimelineActionProps) => { const newTimelineActionbtn = useMemo(() => { return ( - {`New`} + {i18n.NEW_TIMELINE_BTN} ); }, [onActionBtnClick]); @@ -46,10 +47,10 @@ export const NewTimelineAction = ({ timelineId }: NewTimelineActionProps) => { > - + - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/open_timeline.tsx index b26cc376f6add..59a7d6e393312 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/open_timeline.tsx @@ -9,12 +9,11 @@ import { EuiButtonEmpty } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal'; import type { ActionTimelineToShow } from '../../open_timeline/types'; - -export interface OpenTimelineModalButtonProps {} +import * as i18n from './translations'; const actionTimelineToHide: ActionTimelineToShow[] = ['createFrom']; -export const OpenTimelineAction = React.memo(() => { +export const OpenTimelineAction = React.memo<{}>(() => { const [showTimelineModal, setShowTimelineModal] = useState(false); const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); const onOpenTimelineModal = useCallback(() => { @@ -23,8 +22,12 @@ export const OpenTimelineAction = React.memo(() => return ( <> - - {'Open'} + + {i18n.OPEN_TIMELINE_BTN} {showTimelineModal ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx index 547540a2595aa..53fc614ed28a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx @@ -8,6 +8,7 @@ import { EuiButton } from '@elastic/eui'; import React from 'react'; import { useEditTimelineOperation } from '../../timeline/header/edit_timeline_button'; +import * as i18n from './translations'; interface SaveTimelineActionProps { timelineId: string; @@ -20,8 +21,14 @@ export const SaveTimelineAction = ({ timelineId }: SaveTimelineActionProps) => { return ( <> {editTimelineModal} - - {'Save'} + + {i18n.SAVE_TIMELINE_BTN} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts new file mode 100644 index 0000000000000..5dc33fd6cc606 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.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 { i18n } from '@kbn/i18n'; + +export const NEW_TIMELINE_BTN = i18n.translate( + 'xpack.securitySolution.flyout.timeline.actionMenu.newTimelineBtn', + { + defaultMessage: 'New', + } +); + +export const NEW_TIMELINE = i18n.translate( + 'xpack.securitySolution.flyout.timeline.actionMenu.newTimeline', + { + defaultMessage: 'New Timeline', + } +); + +export const OPEN_TIMELINE_BTN = i18n.translate( + 'xpack.securitySolution.flyout.timeline.actionMenu.openTimelineBtn', + { + defaultMessage: 'Open', + } +); + +export const OPEN_TIMELINE_BTN_LABEL = i18n.translate( + 'xpack.securitySolution.flyout.timeline.actionMenu.openTimelineBtnLabel', + { + defaultMessage: 'Open Existing Timeline', + } +); + +export const SAVE_TIMELINE_BTN = i18n.translate( + 'xpack.securitySolution.flyout.timeline.actionMenu.saveTimelineBtn', + { + defaultMessage: 'Save', + } +); + +export const SAVE_TIMELINE_BTN_LABEL = i18n.translate( + 'xpack.securitySolution.flyout.timeline.actionMenu.saveTimelineBtnLabel', + { + defaultMessage: 'Save currently opened Timeline', + } +); + +export const NEW_TEMPLATE_TIMELINE = i18n.translate( + 'xpack.securitySolution.flyout.timeline.actionMenu.newTimelineTemplate', + { + defaultMessage: 'New Timeline template', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index cfee1618297bc..56b7bafe58ea2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -6,13 +6,7 @@ */ import { pick } from 'lodash/fp'; -import { - EuiButton, - EuiContextMenuPanel, - EuiContextMenuItem, - EuiPopover, - EuiButtonEmpty, -} from '@elastic/eui'; +import { EuiContextMenuPanel, EuiContextMenuItem, EuiPopover, EuiButtonEmpty } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -33,130 +27,6 @@ interface Props { timelineId: string; } -export const useTimelineAddToCaseAction = ({ timelineId }: { timelineId: string }) => { - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { - cases, - application: { navigateToApp }, - } = useKibana().services; - const dispatch = useDispatch(); - const { - graphEventId, - savedObjectId, - status: timelineStatus, - title: timelineTitle, - timelineType, - } = useDeepEqualSelector((state) => - pick( - ['graphEventId', 'savedObjectId', 'status', 'title', 'timelineType'], - getTimeline(state, timelineId) ?? timelineDefaults - ) - ); - const [isPopoverOpen, setPopover] = useState(false); - const [isCaseModalOpen, openCaseModal] = useState(false); - - const onRowClick = useCallback( - async (theCase?: CaseUI) => { - openCaseModal(false); - await navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.case, - path: theCase != null ? getCaseDetailsUrl({ id: theCase.id }) : getCreateCaseUrl(), - }); - dispatch( - setInsertTimeline({ - graphEventId, - timelineId, - timelineSavedObjectId: savedObjectId, - timelineTitle, - }) - ); - }, - [dispatch, graphEventId, navigateToApp, savedObjectId, timelineId, timelineTitle] - ); - - const userCasesPermissions = useGetUserCasesPermissions(); - - const handleButtonClick = useCallback(() => { - setPopover((currentIsOpen) => !currentIsOpen); - }, []); - - const handlePopoverClose = useCallback(() => setPopover(false), []); - - const handleNewCaseClick = useCallback(() => { - handlePopoverClose(); - - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.case, - path: getCreateCaseUrl(), - }).then(() => { - dispatch( - setInsertTimeline({ - graphEventId, - timelineId, - timelineSavedObjectId: savedObjectId, - timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, - }) - ); - dispatch(showTimeline({ id: TimelineId.active, show: false })); - }); - }, [ - dispatch, - graphEventId, - navigateToApp, - handlePopoverClose, - savedObjectId, - timelineId, - timelineTitle, - ]); - - const handleExistingCaseClick = useCallback(() => { - handlePopoverClose(); - openCaseModal(true); - }, [openCaseModal, handlePopoverClose]); - - const onCaseModalClose = useCallback(() => { - openCaseModal(false); - }, [openCaseModal]); - - const closePopover = useCallback(() => { - setPopover(false); - }, []); - - const button = useMemo( - () => ( - - {i18n.ATTACH_TO_CASE} - - ), - [handleButtonClick, timelineStatus, timelineType] - ); - - const casesModal = useMemo(() => { - return isCaseModalOpen - ? cases.ui.getAllCasesSelectorModal({ - onRowClick, - onClose: onCaseModalClose, - owner: [APP_ID], - permissions: userCasesPermissions, - }) - : null; - }, [onRowClick, isCaseModalOpen, userCasesPermissions, onCaseModalClose, cases.ui]); - - return { - handleNewCaseClick, - handleExistingCaseClick, - casesModal, - }; -}; - const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx deleted file mode 100644 index 79ef41a070574..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana'; -import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock'; -import { TimelineId } from '../../../../../common/types/timeline'; -import { useTimelineKpis } from '../../../containers/kpis'; -import { FlyoutHeader } from '.'; -import { useSourcererDataView } from '../../../../common/containers/sourcerer'; -import { mockBrowserFields } from '../../../../common/containers/source/mock'; -import { getEmptyValue } from '../../../../common/components/empty_value'; -import { allCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; - -const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock; -jest.mock('../../../../common/containers/sourcerer'); - -const mockUseTimelineKpis: jest.Mock = useTimelineKpis as jest.Mock; -jest.mock('../../../containers/kpis', () => ({ - useTimelineKpis: jest.fn(), -})); -const useKibanaMock = useKibana as jest.Mocked; -jest.mock('../../../../common/lib/kibana'); -jest.mock('@kbn/i18n-react', () => { - const originalModule = jest.requireActual('@kbn/i18n-react'); - const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); - - return { - ...originalModule, - FormattedRelative, - }; -}); -const mockUseTimelineKpiResponse = { - processCount: 1, - userCount: 1, - sourceIpCount: 1, - hostCount: 1, - destinationIpCount: 1, -}; - -const mockUseTimelineLargeKpiResponse = { - processCount: 1000, - userCount: 1000000, - sourceIpCount: 1000000000, - hostCount: 999, - destinationIpCount: 1, -}; -const defaultMocks = { - browserFields: mockBrowserFields, - indexPattern: mockIndexPattern, - loading: false, - selectedPatterns: mockIndexNames, -}; -describe('header', () => { - beforeEach(() => { - // Mocking these services is required for the header component to render. - mockUseSourcererDataView.mockImplementation(() => defaultMocks); - useKibanaMock().services.application.capabilities = { - navLinks: {}, - management: {}, - catalogue: {}, - actions: { show: true, crud: true }, - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('AddToCaseButton', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); - }); - - it('renders the button when the user has create and read permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); - - render( - - - - ); - - expect(screen.getByTestId('attach-timeline-case-button')).toBeInTheDocument(); - }); - - it('does not render the button when the user does not have create permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); - - render( - - - - ); - - expect(screen.queryByTestId('attach-timeline-case-button')).not.toBeInTheDocument(); - }); - }); - - describe('Timeline KPIs', () => { - describe('when the data is not loading and the response contains data', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); - }); - it('renders the component, labels and values successfully', () => { - render( - - - - ); - expect(screen.getByTestId('siem-timeline-kpis')).toBeInTheDocument(); - // label - expect(screen.getByText('Processes')).toBeInTheDocument(); - // value - expect(screen.getByTestId('siem-timeline-process-kpi').textContent).toContain('1'); - }); - }); - - describe('when the data is loading', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]); - }); - it('renders a loading indicator for values', async () => { - render( - - - - ); - expect(screen.getAllByText('--')).not.toHaveLength(0); - }); - }); - - describe('when the response is null and timeline is blank', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, null]); - }); - it('renders labels and the default empty string', () => { - render( - - - - ); - expect(screen.getByText('Processes')).toBeInTheDocument(); - expect(screen.getAllByText(getEmptyValue())).not.toHaveLength(0); - }); - }); - - describe('when the response contains numbers larger than one thousand', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]); - }); - it('formats the numbers correctly', () => { - render( - - - - ); - expect(screen.getByText('1k', { selector: '.euiTitle' })).toBeInTheDocument(); - expect(screen.getByText('1m', { selector: '.euiTitle' })).toBeInTheDocument(); - expect(screen.getByText('1b', { selector: '.euiTitle' })).toBeInTheDocument(); - expect(screen.getByText('999', { selector: '.euiTitle' })).toBeInTheDocument(); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx deleted file mode 100644 index 269173772e116..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ /dev/null @@ -1,506 +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 { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiToolTip, - EuiButtonIcon, - EuiText, - EuiButtonEmpty, - EuiTextColor, - useEuiTheme, -} from '@elastic/eui'; -import type { MouseEventHandler, MouseEvent } from 'react'; -import React, { useCallback, useMemo } from 'react'; -import { isEmpty, get, pick } from 'lodash/fp'; -import { useDispatch, useSelector } from 'react-redux'; -import styled from 'styled-components'; -import { FormattedRelative } from '@kbn/i18n-react'; - -import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { TimelineTabs, TimelineId } from '../../../../../common/types/timeline'; -import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline'; -import type { State } from '../../../../common/store'; -import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { timelineDefaults } from '../../../store/timeline/defaults'; -import { AddToFavoritesButton } from '../../timeline/properties/helpers'; -import type { TimerangeInput } from '../../../../../common/search_strategy'; -import { AddToCaseButton } from '../add_to_case_button'; -import { EditTimelineButton } from '../../timeline/header/edit_timeline_button'; -import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; -import { useTimelineKpis } from '../../../containers/kpis'; -import { useSourcererDataView } from '../../../../common/containers/sourcerer'; -import type { TimelineModel } from '../../../store/timeline/model'; -import { - startSelector, - endSelector, -} from '../../../../common/components/super_date_picker/selectors'; -import { focusActiveTimelineButton } from '../../timeline/helpers'; -import { combineQueries } from '../../../../common/lib/kuery'; -import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import { ActiveTimelines } from './active_timelines'; -import * as i18n from './translations'; -import * as commonI18n from '../../timeline/properties/translations'; -import { getTimelineStatusByIdSelector } from './selectors'; -import { TimelineKPIs } from './kpis'; - -import { setActiveTabTimeline } from '../../../store/timeline/actions'; -import { useIsOverflow } from '../../../../common/hooks/use_is_overflow'; -import { TimelineActionMenu } from '../action_menu'; - -interface FlyoutHeaderProps { - timelineId: string; -} - -interface FlyoutHeaderPanelProps { - timelineId: string; -} - -const ActiveTimelinesContainer = styled(EuiFlexItem)` - overflow: hidden; -`; - -const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { - const dispatch = useDispatch(); - const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView( - SourcererScopeName.timeline - ); - const { uiSettings } = useKibana().services; - const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { - activeTab, - dataProviders, - kqlQuery, - title, - timelineType, - status: timelineStatus, - updated, - show, - filters, - kqlMode, - } = useDeepEqualSelector((state) => - pick( - [ - 'activeTab', - 'dataProviders', - 'kqlQuery', - 'status', - 'title', - 'timelineType', - 'updated', - 'show', - 'filters', - 'kqlMode', - ], - getTimeline(state, timelineId) ?? timelineDefaults - ) - ); - const isDataInTimeline = useMemo( - () => !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), - [dataProviders, kqlQuery] - ); - - const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterQuerySelector(), []); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)!); - - const kqlQueryExpression = - isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template' - ? ' ' - : kqlQueryTimeline; - const kqlQueryTest = useMemo( - () => ({ query: kqlQueryExpression, language: 'kuery' }), - [kqlQueryExpression] - ); - - const combinedQueries = useMemo( - () => - combineQueries({ - config: esQueryConfig, - dataProviders, - indexPattern, - browserFields, - filters: filters ? filters : [], - kqlQuery: kqlQueryTest, - kqlMode, - }), - [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryTest] - ); - - const isActive = useMemo(() => timelineId === TimelineId.active, [timelineId]); - const getStartSelector = useMemo(() => startSelector(), []); - const getEndSelector = useMemo(() => endSelector(), []); - - const timerange: TimerangeInput = useDeepEqualSelector((state) => { - if (isActive) { - return { - from: getStartSelector(state.inputs.timeline), - to: getEndSelector(state.inputs.timeline), - interval: '', - }; - } else { - return { - from: getStartSelector(state.inputs.global), - to: getEndSelector(state.inputs.global), - interval: '', - }; - } - }); - - const isBlankTimeline: boolean = useMemo( - () => - (isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQueryTest.query)) || - combinedQueries?.filterQuery === undefined, - [dataProviders, filters, kqlQueryTest, combinedQueries] - ); - - const handleClose = useCallback(() => { - dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); - focusActiveTimelineButton(); - }, [dispatch, timelineId]); - - const { euiTheme } = useEuiTheme(); - - return ( - <> - - - - - - - - - - - - {show ? ( - - - - ) : null} - {show && ( - - - - - - )} - - - - - - ); -}; - -export const FlyoutHeaderPanel = React.memo(FlyoutHeaderPanelComponent); - -const StyledDiv = styled.div` - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; -`; - -const ReadMoreButton = ({ - description, - onclick, -}: { - description: string; - onclick: MouseEventHandler; -}) => { - const [isOverflow, ref] = useIsOverflow(description); - return ( - <> - {description} - {isOverflow && ( - - {i18n.READ_MORE} - - )} - - ); -}; - -const StyledTimelineHeader = styled(EuiFlexGroup)` - ${({ theme }) => `margin: ${theme.eui.euiSizeXS} ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS};`} - flex: 0; -`; - -const TimelineStatusInfoContainer = styled.span` - ${({ theme }) => `margin-left: ${theme.eui.euiSizeS};`} - white-space: nowrap; -`; - -const KpisContainer = styled.div` - ${({ theme }) => `margin-right: ${theme.eui.euiSizeM};`} -`; - -const RowFlexItem = styled(EuiFlexItem)` - flex-direction: row; - align-items: center; -`; - -const TimelineTitleContainer = styled.h3` - display: -webkit-box; - overflow: hidden; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - word-break: break-word; -`; - -const TimelineNameComponent: React.FC = ({ timelineId }) => { - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { title, timelineType } = useDeepEqualSelector((state) => - pick(['title', 'timelineType'], getTimeline(state, timelineId) ?? timelineDefaults) - ); - const placeholder = useMemo( - () => - timelineType === TimelineType.template - ? commonI18n.UNTITLED_TEMPLATE - : commonI18n.UNTITLED_TIMELINE, - [timelineType] - ); - - const content = useMemo(() => title || placeholder, [title, placeholder]); - - return ( - - - {content} - - - ); -}; - -const TimelineName = React.memo(TimelineNameComponent); - -const TimelineDescriptionComponent: React.FC = ({ timelineId }) => { - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const description = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).description - ); - const dispatch = useDispatch(); - - const onReadMore: MouseEventHandler = useCallback( - (event: MouseEvent) => { - dispatch( - setActiveTabTimeline({ - id: timelineId, - activeTab: TimelineTabs.notes, - scrollToTop: true, - }) - ); - }, - [dispatch, timelineId] - ); - - return ( - - - - ); -}; - -const TimelineDescription = React.memo(TimelineDescriptionComponent); - -const TimelineStatusInfoComponent: React.FC = ({ timelineId }) => { - const getTimelineStatus = useMemo(() => getTimelineStatusByIdSelector(), []); - const { status: timelineStatus, updated } = useDeepEqualSelector((state) => - getTimelineStatus(state, timelineId) - ); - - const isUnsaved = useMemo(() => timelineStatus === TimelineStatus.draft, [timelineStatus]); - - if (isUnsaved) { - return ( - - - {i18n.UNSAVED} - - - ); - } - - return ( - - - {i18n.AUTOSAVED}{' '} - - - - ); -}; - -const TimelineStatusInfo = React.memo(TimelineStatusInfoComponent); - -const FlyoutHeaderComponent: React.FC = ({ timelineId }) => { - const { selectedPatterns, indexPattern, browserFields } = useSourcererDataView( - SourcererScopeName.timeline - ); - const getStartSelector = useMemo(() => startSelector(), []); - const getEndSelector = useMemo(() => endSelector(), []); - const isActive = useMemo(() => timelineId === TimelineId.active, [timelineId]); - const timerange: TimerangeInput = useDeepEqualSelector((state) => { - if (isActive) { - return { - from: getStartSelector(state.inputs.timeline), - to: getEndSelector(state.inputs.timeline), - interval: '', - }; - } else { - return { - from: getStartSelector(state.inputs.global), - to: getEndSelector(state.inputs.global), - interval: '', - }; - } - }); - const { uiSettings } = useKibana().services; - const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const timeline: TimelineModel = useSelector( - (state: State) => getTimeline(state, timelineId) ?? timelineDefaults - ); - const { dataProviders, filters, timelineType, kqlMode, activeTab } = timeline; - const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterQuerySelector(), []); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)!); - - const kqlQueryExpression = - isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template' - ? ' ' - : kqlQueryTimeline; - const kqlQuery = useMemo( - () => ({ query: kqlQueryExpression, language: 'kuery' }), - [kqlQueryExpression] - ); - - const combinedQueries = useMemo( - () => - combineQueries({ - config: esQueryConfig, - dataProviders, - indexPattern, - browserFields, - filters: filters ? filters : [], - kqlQuery, - kqlMode, - }), - [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQuery] - ); - - const isBlankTimeline: boolean = useMemo( - () => - (isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQuery.query)) || - combinedQueries?.filterQuery === undefined, - [dataProviders, filters, kqlQuery, combinedQueries] - ); - - const [loading, kpis] = useTimelineKpis({ - defaultIndex: selectedPatterns, - timerange, - isBlankTimeline, - filterQuery: combinedQueries?.filterQuery ?? '', - }); - - const userCasesPermissions = useGetUserCasesPermissions(); - - return ( - - - - - - - - - - - - - - - - - - - - {activeTab === TimelineTabs.query ? ( - - ) : null} - - - - - - - - - {userCasesPermissions.create && userCasesPermissions.read && ( - - - - )} - - - - ); -}; - -FlyoutHeaderComponent.displayName = 'FlyoutHeaderComponent'; - -export const FlyoutHeader = React.memo(FlyoutHeaderComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx index 4cb622ec801b4..d81d7dd871ecf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { EuiStat, EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; +import { EuiStat, EuiFlexItem, EuiFlexGroup, EuiToolTip, EuiBadge } from '@elastic/eui'; import numeral from '@elastic/numeral'; +import { euiThemeVars } from '@kbn/ui-theme'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; import { useUiSetting$ } from '../../../../common/lib/kibana'; import type { TimelineKpiStrategyResponse } from '../../../../../common/search_strategy'; @@ -22,89 +23,121 @@ const NoWrapEuiStat = styled(EuiStat)` } `; -export const TimelineKPIs = React.memo( - ({ kpis, isLoading }: { kpis: TimelineKpiStrategyResponse | null; isLoading: boolean }) => { - const kpiFormat = '0,0.[000]a'; - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const formattedKpis = useMemo(() => { - return { - process: kpis === null ? getEmptyValue() : numeral(kpis.processCount).format(kpiFormat), - user: kpis === null ? getEmptyValue() : numeral(kpis.userCount).format(kpiFormat), - host: kpis === null ? getEmptyValue() : numeral(kpis.hostCount).format(kpiFormat), - sourceIp: kpis === null ? getEmptyValue() : numeral(kpis.sourceIpCount).format(kpiFormat), - destinationIp: - kpis === null ? getEmptyValue() : numeral(kpis.destinationIpCount).format(kpiFormat), - }; - }, [kpis]); - const formattedKpiToolTips = useMemo(() => { - return { - process: numeral(kpis?.processCount).format(defaultNumberFormat), - user: numeral(kpis?.userCount).format(defaultNumberFormat), - host: numeral(kpis?.hostCount).format(defaultNumberFormat), - sourceIp: numeral(kpis?.sourceIpCount).format(defaultNumberFormat), - destinationIp: numeral(kpis?.destinationIpCount).format(defaultNumberFormat), - }; - }, [kpis, defaultNumberFormat]); - return ( - - +export const StatsContainer = styled.span` + font-size: ${euiThemeVars.euiFontSizeXS}; + font-weight: ${euiThemeVars.euiFontWeightSemiBold}; + /* border-right: ${euiThemeVars.euiBorderThin}; */ + padding-right: 16px; + .smallDot { + width: 3px !important; + display: inline-block; + } + .euiBadge__text { + text-align: center; + width: 100%; + } +`; + +export const TimelineKPIs = React.memo(({ kpis }: { kpis: TimelineKpiStrategyResponse | null }) => { + const kpiFormat = '0,0.[000]a'; + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const formattedKpis = useMemo(() => { + return { + process: kpis === null ? getEmptyValue() : numeral(kpis.processCount).format(kpiFormat), + user: kpis === null ? getEmptyValue() : numeral(kpis.userCount).format(kpiFormat), + host: kpis === null ? getEmptyValue() : numeral(kpis.hostCount).format(kpiFormat), + sourceIp: kpis === null ? getEmptyValue() : numeral(kpis.sourceIpCount).format(kpiFormat), + destinationIp: + kpis === null ? getEmptyValue() : numeral(kpis.destinationIpCount).format(kpiFormat), + }; + }, [kpis]); + + const formattedKpiToolTips = useMemo(() => { + return { + process: numeral(kpis?.processCount).format(defaultNumberFormat), + user: numeral(kpis?.userCount).format(defaultNumberFormat), + host: numeral(kpis?.hostCount).format(defaultNumberFormat), + sourceIp: numeral(kpis?.sourceIpCount).format(defaultNumberFormat), + destinationIp: numeral(kpis?.destinationIpCount).format(defaultNumberFormat), + }; + }, [kpis, defaultNumberFormat]); + + const getColor = useCallback((count) => { + if (count === 0) { + return 'hollow'; + } + return 'hollow'; + }, []); + + return ( + + + + {`${i18n.PROCESS_KPI_TITLE} : `} - + + {formattedKpis.process} + - - + + + + + {`${i18n.USER_KPI_TITLE} : `} - + + {formattedKpis.user} + - - + + + + + {`${i18n.HOST_KPI_TITLE} : `} - + + {formattedKpis.host} + - - + + + + + {`${i18n.SOURCE_IP_KPI_TITLE} : `} - + + {formattedKpis.sourceIp} + - - + + + + + {`${i18n.SOURCE_IP_KPI_TITLE} : `} - + + {formattedKpis.destinationIp} + - - - ); - } -); + + + + ); +}); TimelineKPIs.displayName = 'TimelineKPIs'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx deleted file mode 100644 index 8700a29e969bf..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis_new.tsx +++ /dev/null @@ -1,139 +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, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; - -import { EuiStat, EuiFlexItem, EuiFlexGroup, EuiToolTip, EuiBadge } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import { useUiSetting$ } from '../../../../common/lib/kibana'; -import type { TimelineKpiStrategyResponse } from '../../../../../common/search_strategy'; -import { getEmptyValue } from '../../../../common/components/empty_value'; -import * as i18n from './translations'; - -const NoWrapEuiStat = styled(EuiStat)` - & .euiStat__description { - white-space: nowrap; - } -`; - -export const StatsContainer = styled.span` - font-size: ${euiThemeVars.euiFontSizeXS}; - font-weight: ${euiThemeVars.euiFontWeightSemiBold}; - /* border-right: ${euiThemeVars.euiBorderThin}; */ - padding-right: 16px; - .smallDot { - width: 3px !important; - display: inline-block; - } - .euiBadge__text { - text-align: center; - width: 100%; - } -`; - -export const TimelineKPIs2 = React.memo( - ({ kpis, isLoading }: { kpis: TimelineKpiStrategyResponse | null; isLoading: boolean }) => { - const kpiFormat = '0,0.[000]a'; - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const formattedKpis = useMemo(() => { - return { - process: kpis === null ? getEmptyValue() : numeral(kpis.processCount).format(kpiFormat), - user: kpis === null ? getEmptyValue() : numeral(kpis.userCount).format(kpiFormat), - host: kpis === null ? getEmptyValue() : numeral(kpis.hostCount).format(kpiFormat), - sourceIp: kpis === null ? getEmptyValue() : numeral(kpis.sourceIpCount).format(kpiFormat), - destinationIp: - kpis === null ? getEmptyValue() : numeral(kpis.destinationIpCount).format(kpiFormat), - }; - }, [kpis]); - - const formattedKpiToolTips = useMemo(() => { - return { - process: numeral(kpis?.processCount).format(defaultNumberFormat), - user: numeral(kpis?.userCount).format(defaultNumberFormat), - host: numeral(kpis?.hostCount).format(defaultNumberFormat), - sourceIp: numeral(kpis?.sourceIpCount).format(defaultNumberFormat), - destinationIp: numeral(kpis?.destinationIpCount).format(defaultNumberFormat), - }; - }, [kpis, defaultNumberFormat]); - - const getColor = useCallback((count) => { - if (count === 0) { - return 'hollow'; - } - return 'hollow'; - }, []); - - return ( - - - - {`${i18n.PROCESS_KPI_TITLE} : `} - - - {formattedKpis.process} - - - - - - - {`${i18n.USER_KPI_TITLE} : `} - - - {formattedKpis.user} - - - - - - - {`${i18n.HOST_KPI_TITLE} : `} - - - {formattedKpis.host} - - - - - - - {`${i18n.SOURCE_IP_KPI_TITLE} : `} - - - {formattedKpis.sourceIp} - - - - - - - {`${i18n.SOURCE_IP_KPI_TITLE} : `} - - - {formattedKpis.destinationIp} - - - - - - ); - } -); - -TimelineKPIs2.displayName = 'TimelineKPIs'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx deleted file mode 100644 index 36743aa055a9b..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_context_menu.tsx +++ /dev/null @@ -1,138 +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 { - EuiButtonIcon, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiPopover, - EuiIcon, -} from '@elastic/eui'; -import React, { useState, useCallback, useMemo } from 'react'; -import { useEditTimelineOperation } from '../../timeline/header/edit_timeline_button'; -import { useTimelineAddToFavoriteAction } from '../../timeline/properties/helpers'; -import { useTimelineAddToCaseAction } from '../add_to_case_button'; - -interface Props { - timelineId: string; - showIcons: number; -} - -export const TimelineContextMenu = ({ timelineId, showIcons }: Props) => { - const [isContextMenuVisible, setIsContextMenuVisible] = useState(false); - - const toggleContextMenu = useCallback(() => { - setIsContextMenuVisible((prev) => !prev); - }, []); - - const withContextMenuAction = useCallback( - (fn: unknown) => { - return () => { - if (typeof fn === 'function') { - fn(); - } - toggleContextMenu(); - }; - }, - [toggleContextMenu] - ); - - const { openEditTimeline, editTimelineModal } = useEditTimelineOperation({ - timelineId, - }); - - const editTimelineNameDesc = useMemo(() => { - return ( - - {'Edit Timeline'} - - ); - }, [openEditTimeline, withContextMenuAction]); - - const { toggleFavorite, isFavorite } = useTimelineAddToFavoriteAction({ - timelineId, - }); - - const toggleTimelineFavorite = useMemo(() => { - return ( - - {isFavorite ? 'Remove From Favorites' : 'Add to Favorites'} - - ); - }, [withContextMenuAction, toggleFavorite, isFavorite]); - - const { handleNewCaseClick, handleExistingCaseClick, casesModal } = useTimelineAddToCaseAction({ - timelineId, - }); - - const addToNewCase = useMemo(() => { - return ( - - {'Attach to a new Case'} - - ); - }, [handleNewCaseClick, withContextMenuAction]); - - const addToExistingCase = useMemo(() => { - return ( - - {'Attach to a existing Case'} - - ); - }, [handleExistingCaseClick, withContextMenuAction]); - - const contextMenuItems = useMemo( - () => [editTimelineNameDesc, toggleTimelineFavorite, addToNewCase, addToExistingCase], - [editTimelineNameDesc, toggleTimelineFavorite, addToNewCase, addToExistingCase] - ); - - return ( - <> - {casesModal} - {editTimelineModal} - {isFavorite && } - - } - isOpen={isContextMenuVisible} - closePopover={toggleContextMenu} - panelPaddingSize="none" - anchorPosition="downLeft" - panelProps={{ - 'data-test-subj': 'timeline-context-menu', - }} - > - - - - ); -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index 9aaea8837f696..1ae59635c6295 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -6,6 +6,7 @@ */ import { rgba } from 'polished'; +import { useDispatch } from 'react-redux'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { v4 as uuidv4 } from 'uuid'; @@ -35,7 +36,7 @@ interface Props { const DropTargetDataProvidersContainer = styled.div` position: relative; - padding: 16px 0 0px 0; + padding: 20px 0 0px 0; .${IS_DRAGGING_CLASS_NAME} & .drop-target-data-providers { background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.1)}; @@ -109,16 +110,21 @@ const SearchOrFilterGlobalStyle = createGlobalStyle` width: ${searchOrFilterPopoverWidth} !important; } } + + [data-test-subj="timeline-select-search-or-filter"] { + height: auto + } `; const CustomTooltipDiv = styled.div` position: absolute; left: 20px; - transform: translateY(-35%); + transform: translateY(-50%); z-index: 9999; `; export const DataProviders = React.memo(({ timelineId }) => { + const dispatch = useDispatch(); const { browserFields } = useSourcererDataView(SourcererScopeName.timeline); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -135,8 +141,10 @@ export const DataProviders = React.memo(({ timelineId }) => { ); const handleChange = useCallback( - () => (mode: KqlMode) => updateKqlMode({ id: timelineId, kqlMode: mode }), - [timelineId] + (mode: KqlMode) => { + dispatch(updateKqlMode({ id: timelineId, kqlMode: mode })); + }, + [timelineId, dispatch] ); return ( @@ -147,7 +155,10 @@ export const DataProviders = React.memo(({ timelineId }) => { className="drop-target-data-providers-container" > - + (({ timelineId }) => { itemClassName={timelineSelectModeItemsClassName} onChange={handleChange} options={options} - popoverProps={{ className: searchOrFilterPopoverClassName }} + popoverProps={{ + className: searchOrFilterPopoverClassName, + panelClassName: searchOrFilterPopoverClassName, + }} valueOfSelected={kqlMode} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts index 18ca62a71c6d1..8733e42d24966 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts @@ -169,3 +169,10 @@ export const GROUP_AREA_ARIA_LABEL = (group: number) => values: { group }, defaultMessage: 'You are in group {group}', }); + +export const FILTER_OR_SEARCH_WITH_KQL = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.filterOrSearchWithKql', + { + defaultMessage: 'Filter or Search with KQL', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx index f48f6bc99b563..3914c6b8c197e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx @@ -17,7 +17,7 @@ import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import type { State } from '../../../../common/store'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import { TimelineKPIs2 } from '../../flyout/header/kpis_new'; +import { TimelineKPIs } from '../../flyout/header/kpis'; import { useTimelineKpis } from '../../../containers/kpis'; import { useKibana } from '../../../../common/lib/kibana'; import { timelineSelectors } from '../../../store/timeline'; @@ -119,7 +119,7 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { [dataProviders, filters, kqlQueryTest, combinedQueries] ); - const [loading, kpis] = useTimelineKpis({ + const [, kpis] = useTimelineKpis({ defaultIndex: selectedPatterns, timerange, isBlankTimeline, @@ -128,7 +128,7 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { return ( - + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/timeline_stats_treemap.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/timeline_stats_treemap.tsx deleted file mode 100644 index 99efd83b38858..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/timeline_stats_treemap.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' - -type Props = {} - -const TimelineStatsTreemap = (props: Props) => { - return ( - ) -} - -export default TimelineStatsTreemap diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 430de7d283eb9..348ae2bace468 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -30,31 +30,6 @@ interface AddToFavoritesButtonProps { compact?: boolean; } -export const useTimelineAddToFavoriteAction = ({ timelineId }: { timelineId: string }) => { - const dispatch = useDispatch(); - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - - const isFavorite = useShallowEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).isFavorite - ); - - const status = useShallowEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).status - ); - - const disableFavoriteButton = status === TimelineStatus.immutable; - const toggleFavorite = useCallback( - () => dispatch(timelineActions.updateIsFavorite({ id: timelineId, isFavorite: !isFavorite })), - [dispatch, timelineId, isFavorite] - ); - - return { - toggleFavorite, - isFavorite, - isFavouriteDisabled: disableFavoriteButton, - }; -}; - const AddToFavoritesButtonComponent: React.FC = ({ timelineId, compact, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 56577a8c17b6e..eadd479c44008 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -56,18 +56,18 @@ const SearchBarContainer = styled.div` * Filters are displayed with QueryBar so below is how is the layout with default filters. * * - * ------------------------------ + * -------------------------------- * -----------------| |------------ * | DataViewPicker | QueryBar | Date | * ------------------------------------------------------------- * | Filters | * -------------------------------- * - * This component makes sure that default filters are not rendered and we can saperate display + * The tree under this component makes sure that default filters are not rendered and we can saperately display * them outside query component so that layout is as below: * * ----------------------------------------------------------- - * | DataViewPicker | QueryBar | Date | + * | DataViewPicker | QueryBar | Date | * ----------------------------------------------------------- * | Filters | * ----------------------------------------------------------- diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index 995382753df3b..1770f0d82f5e7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -135,8 +135,12 @@ const StatefulSearchOrFilterComponent = React.memo( useEffect(() => { /* - * If there is a change in data providers and data provider was hidden, - * it must be made visible + * If there is a change in data providers + * - data provider has some data and it was hidden, + * * it must be made visible + * + * - data provider has no data and it was visible, + * * it must be hidden * * */ if (dataProviders?.length > 0) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts index 791d861ae2590..b0792604ff415 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts @@ -64,13 +64,6 @@ export const SEARCH_KQL_SELECTED_TEXT = i18n.translate( } ); -export const FILTER_OR_SEARCH_WITH_KQL = i18n.translate( - 'xpack.securitySolution.timeline.searchOrFilter.filterOrSearchWithKql', - { - defaultMessage: 'Filter or Search with KQL', - } -); - export const DATA_PROVIDER_HIDDEN_POPULATED = i18n.translate( 'xpack.securitySolution.timeline.searchOrFilter.dataProviderToggle.hiddenAndPopulated', { From eb76e7f78eb6edfec271b38799d793edbff29728 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Tue, 31 Oct 2023 07:45:31 +0100 Subject: [PATCH 10/43] fix: types + some tests --- .../public/common/mock/global_state.ts | 1 + .../public/common/mock/timeline_results.ts | 2 + .../components/alerts_table/actions.test.tsx | 1 + .../components/flyout/bottom_bar/index.tsx | 1 + .../components/flyout/header/index.test.tsx | 145 +++++++++++++++ .../components/flyout/header/index.tsx | 173 ++++++++++++++++++ .../components/flyout/header/translations.ts | 29 --- .../components/timeline/kpi/collapsed.tsx | 6 - .../components/timeline/kpi/index.tsx | 2 +- .../kpi/{kpi.tsx => kpi_container.tsx} | 15 +- .../{flyout/header => timeline/kpi}/kpis.tsx | 8 +- .../components/timeline/kpi/translations.ts | 37 ++++ .../timeline/query_tab_content/index.tsx | 2 +- .../timelines/store/timeline/epic.test.ts | 1 + .../timelines/store/timeline/reducer.test.ts | 1 + 15 files changed, 368 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx rename x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/{kpi.tsx => kpi_container.tsx} (90%) rename x-pack/plugins/security_solution/public/timelines/components/{flyout/header => timeline/kpi}/kpis.tsx (96%) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/translations.ts 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 3306f3224a432..8803f256bb439 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 @@ -376,6 +376,7 @@ export const mockGlobalState: State = { itemsPerPageOptions: [10, 25, 50, 100], savedSearchId: null, isDiscoverSavedSearchLoaded: false, + isDataProviderVisible: true, }, }, insertTimeline: null, 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 ef6b4d265a5a7..73763e84abd53 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 @@ -2027,6 +2027,7 @@ export const mockTimelineModel: TimelineModel = { templateTimelineVersion: null, version: '1', savedSearchId: null, + isDataProviderVisible: true, }; export const mockDataTableModel: DataTableModel = { @@ -2208,6 +2209,7 @@ export const defaultTimelineProps: CreateTimelineProps = { version: null, savedSearchId: null, isDiscoverSavedSearchLoaded: false, + isDataProviderVisible: true, }, to: '2018-11-05T19:03:25.937Z', notes: null, 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 23b8fe50be532..ea13b019e50ef 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 @@ -1117,6 +1117,7 @@ describe('alert actions', () => { filterQuery: null, }, resolveTimelineConfig: undefined, + isDataProviderVisible: false, }, from: expectedFrom, to: expectedTo, diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx index 0b5286f1fedb3..bc1617d3b9a53 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx @@ -10,6 +10,7 @@ import { FlyoutHeaderPanel } from '../header'; interface FlyoutBottomBarProps { showTimelineHeaderPanel: boolean; + timelineId: string; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx new file mode 100644 index 0000000000000..466f413e4df77 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana'; +import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock'; +import { useTimelineKpis } from '../../../containers/kpis'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { getEmptyValue } from '../../../../common/components/empty_value'; +import { allCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; + +const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock; +jest.mock('../../../../common/containers/sourcerer'); + +const mockUseTimelineKpis: jest.Mock = useTimelineKpis as jest.Mock; +jest.mock('../../../containers/kpis', () => ({ + useTimelineKpis: jest.fn(), +})); +const useKibanaMock = useKibana as jest.Mocked; +jest.mock('../../../../common/lib/kibana'); +jest.mock('@kbn/i18n-react', () => { + const originalModule = jest.requireActual('@kbn/i18n-react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + + return { + ...originalModule, + FormattedRelative, + }; +}); +const mockUseTimelineKpiResponse = { + processCount: 1, + userCount: 1, + sourceIpCount: 1, + hostCount: 1, + destinationIpCount: 1, +}; + +const mockUseTimelineLargeKpiResponse = { + processCount: 1000, + userCount: 1000000, + sourceIpCount: 1000000000, + hostCount: 999, + destinationIpCount: 1, +}; +const defaultMocks = { + browserFields: mockBrowserFields, + indexPattern: mockIndexPattern, + loading: false, + selectedPatterns: mockIndexNames, +}; +describe('header', () => { + beforeEach(() => { + // Mocking these services is required for the header component to render. + mockUseSourcererDataView.mockImplementation(() => defaultMocks); + useKibanaMock().services.application.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe.skip('AddToCaseButton', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); + }); + + it('renders the button when the user has create and read permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); + + render(); + + expect(screen.getByTestId('attach-timeline-case-button')).toBeInTheDocument(); + }); + + it('does not render the button when the user does not have create permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + + render(); + + expect(screen.queryByTestId('attach-timeline-case-button')).not.toBeInTheDocument(); + }); + }); + + describe.skip('Timeline KPIs', () => { + describe('when the data is not loading and the response contains data', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); + }); + it('renders the component, labels and values successfully', () => { + render(); + expect(screen.getByTestId('siem-timeline-kpis')).toBeInTheDocument(); + // label + expect(screen.getByText('Processes')).toBeInTheDocument(); + // value + expect(screen.getByTestId('siem-timeline-process-kpi').textContent).toContain('1'); + }); + }); + + describe('when the data is loading', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]); + }); + it('renders a loading indicator for values', async () => { + render(); + expect(screen.getAllByText('--')).not.toHaveLength(0); + }); + }); + + describe('when the response is null and timeline is blank', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, null]); + }); + it('renders labels and the default empty string', () => { + render(); + expect(screen.getByText('Processes')).toBeInTheDocument(); + expect(screen.getAllByText(getEmptyValue())).not.toHaveLength(0); + }); + }); + + describe('when the response contains numbers larger than one thousand', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]); + }); + it('formats the numbers correctly', () => { + render(); + expect(screen.getByText('1k', { selector: '.euiTitle' })).toBeInTheDocument(); + expect(screen.getByText('1m', { selector: '.euiTitle' })).toBeInTheDocument(); + expect(screen.getByText('1b', { selector: '.euiTitle' })).toBeInTheDocument(); + expect(screen.getByText('999', { selector: '.euiTitle' })).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx new file mode 100644 index 0000000000000..a54c2b2a8325a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiToolTip, + EuiButtonIcon, + useEuiTheme, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { isEmpty, get, pick } from 'lodash/fp'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import { InputsModelId } from '../../../../common/store/inputs/constants'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { TimelineTabs } from '../../../../../common/types/timeline'; +import type { State } from '../../../../common/store'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { timelineDefaults } from '../../../store/timeline/defaults'; +import { AddTimelineButton } from '../add_timeline_button'; +import { useKibana } from '../../../../common/lib/kibana'; +import { InspectButton } from '../../../../common/components/inspect'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { focusActiveTimelineButton } from '../../timeline/helpers'; +import { combineQueries } from '../../../../common/lib/kuery'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { ActiveTimelines } from './active_timelines'; +import * as i18n from './translations'; + +interface FlyoutHeaderPanelProps { + timelineId: string; +} + +const ActiveTimelinesContainer = styled(EuiFlexItem)` + overflow: hidden; +`; + +const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { + const dispatch = useDispatch(); + const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline); + const { uiSettings } = useKibana().services; + const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { + activeTab, + dataProviders, + kqlQuery, + title, + timelineType, + status: timelineStatus, + updated, + show, + filters, + kqlMode, + } = useDeepEqualSelector((state) => + pick( + [ + 'activeTab', + 'dataProviders', + 'kqlQuery', + 'status', + 'title', + 'timelineType', + 'updated', + 'show', + 'filters', + 'kqlMode', + ], + getTimeline(state, timelineId) ?? timelineDefaults + ) + ); + const isDataInTimeline = useMemo( + () => !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), + [dataProviders, kqlQuery] + ); + + const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterQuerySelector(), []); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)!); + + const kqlQueryExpression = + isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template' + ? ' ' + : kqlQueryTimeline; + const kqlQueryTest = useMemo( + () => ({ query: kqlQueryExpression, language: 'kuery' }), + [kqlQueryExpression] + ); + + const combinedQueries = useMemo( + () => + combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters: filters ? filters : [], + kqlQuery: kqlQueryTest, + kqlMode, + }), + [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryTest] + ); + + const handleClose = useCallback(() => { + dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); + focusActiveTimelineButton(); + }, [dispatch, timelineId]); + + const { euiTheme } = useEuiTheme(); + + return ( + + + + + + + {show && ( + + + {(activeTab === TimelineTabs.query || activeTab === TimelineTabs.eql) && ( + + + + )} + + + + + + + + )} + + + ); +}; + +export const FlyoutHeaderPanel = React.memo(FlyoutHeaderPanelComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts index c0efdd285b05c..10048b76147cf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts @@ -33,35 +33,6 @@ export const INSPECT_TIMELINE_TITLE = i18n.translate( } ); -export const PROCESS_KPI_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.kpis.processKpiTitle', - { - defaultMessage: 'Processes', - } -); - -export const HOST_KPI_TITLE = i18n.translate('xpack.securitySolution.timeline.kpis.hostKpiTitle', { - defaultMessage: 'Hosts', -}); - -export const SOURCE_IP_KPI_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.kpis.sourceIpKpiTitle', - { - defaultMessage: 'Source IPs', - } -); - -export const DESTINATION_IP_KPI_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.kpis.destinationKpiTitle', - { - defaultMessage: 'Destination IPs', - } -); - -export const USER_KPI_TITLE = i18n.translate('xpack.securitySolution.timeline.kpis.userKpiTitle', { - defaultMessage: 'Users', -}); - export const READ_MORE = i18n.translate('xpack.securitySolution.timeline.properties.readMore', { defaultMessage: 'Read More', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx deleted file mode 100644 index 1fec1c76430eb..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/collapsed.tsx +++ /dev/null @@ -1,6 +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. - */ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.tsx index 32a4f8f6e1935..a0ebf4d5408f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.tsx @@ -5,4 +5,4 @@ * 2.0. */ -export { TimelineKpi } from './kpi'; +export { TimelineKpisContainer as TimelineKpi } from './kpi_container'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx rename to x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx index 3914c6b8c197e..049801824073a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx @@ -10,14 +10,13 @@ import { isEmpty, pick } from 'lodash/fp'; import { useSelector } from 'react-redux'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { TimerangeInput } from '@kbn/timelines-plugin/common'; -import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { EuiPanel } from '@elastic/eui'; import { TimelineId } from '../../../../../common/types'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import type { State } from '../../../../common/store'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import { TimelineKPIs } from '../../flyout/header/kpis'; +import { TimelineKPIs } from './kpis'; import { useTimelineKpis } from '../../../containers/kpis'; import { useKibana } from '../../../../common/lib/kibana'; import { timelineSelectors } from '../../../store/timeline'; @@ -32,15 +31,7 @@ interface KpiExpandedProps { timelineId: string; } -const StyledEuiPanel = euiStyled(EuiPanel)` - display: flex; - flex-direction: column; - position: relative; - overflow: hidden; - max-height: 308px; -`; - -export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { +export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView( SourcererScopeName.timeline ); @@ -48,7 +39,7 @@ export const TimelineKpi = ({ timelineId }: KpiExpandedProps) => { const { uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { dataProviders, kqlQuery, filters, kqlMode } = useDeepEqualSelector((state) => + const { dataProviders, filters, kqlMode } = useDeepEqualSelector((state) => pick( [ 'activeTab', diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpis.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx rename to x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpis.tsx index d81d7dd871ecf..68b054624ec42 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpis.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { EuiStat, EuiFlexItem, EuiFlexGroup, EuiToolTip, EuiBadge } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiToolTip, EuiBadge } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { euiThemeVars } from '@kbn/ui-theme'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; @@ -17,12 +17,6 @@ import type { TimelineKpiStrategyResponse } from '../../../../../common/search_s import { getEmptyValue } from '../../../../common/components/empty_value'; import * as i18n from './translations'; -const NoWrapEuiStat = styled(EuiStat)` - & .euiStat__description { - white-space: nowrap; - } -`; - export const StatsContainer = styled.span` font-size: ${euiThemeVars.euiFontSizeXS}; font-weight: ${euiThemeVars.euiFontWeightSemiBold}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/translations.ts new file mode 100644 index 0000000000000..177516ea4a689 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/translations.ts @@ -0,0 +1,37 @@ +/* + * Copyright 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 PROCESS_KPI_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.kpis.processKpiTitle', + { + defaultMessage: 'Processes', + } +); + +export const HOST_KPI_TITLE = i18n.translate('xpack.securitySolution.timeline.kpis.hostKpiTitle', { + defaultMessage: 'Hosts', +}); + +export const SOURCE_IP_KPI_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.kpis.sourceIpKpiTitle', + { + defaultMessage: 'Source IPs', + } +); + +export const DESTINATION_IP_KPI_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.kpis.destinationKpiTitle', + { + defaultMessage: 'Destination IPs', + } +); + +export const USER_KPI_TITLE = i18n.translate('xpack.securitySolution.timeline.kpis.userKpiTitle', { + defaultMessage: 'Users', +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index ef649f60e0e5d..259156ea60832 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -194,7 +194,7 @@ export const QueryTabContentComponent: React.FC = ({ selectedPatterns, } = useSourcererDataView(SourcererScopeName.timeline); - const { uiSettings, data } = useKibana().services; + const { uiSettings } = useKibana().services; const isEnterprisePlus = useLicense().isEnterprise(); const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts index 21551dacebc66..13941635e5d34 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts @@ -176,6 +176,7 @@ describe('Epic Timeline', () => { id: '11169110-fc22-11e9-8ca9-072f15ce2685', savedQueryId: 'my endgame timeline query', savedSearchId: null, + isDataProviderVisible: true, }; expect( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 980573f0c73c3..f0d553ce75246 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -138,6 +138,7 @@ const basicTimeline: TimelineModel = { title: '', version: null, savedSearchId: null, + isDataProviderVisible: true, }; const timelineByIdMock: TimelineById = { foo: { ...basicTimeline }, From fee5b8ce12532afd6b0f19d17542394d0cbe1400 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Tue, 31 Oct 2023 08:38:21 +0100 Subject: [PATCH 11/43] fix: more tests --- .../public/common/mock/timeline_results.ts | 4 +- .../components/alerts_table/actions.test.tsx | 2 +- .../flyout/__snapshots__/index.test.tsx.snap | 26 +- .../header/__snapshots__/index.test.tsx.snap | 649 ++++++++++++++++-- .../components/timeline/header/index.test.tsx | 9 +- .../components/timeline/header/index.tsx | 5 +- 6 files changed, 629 insertions(+), 66 deletions(-) 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 73763e84abd53..ce52132282798 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 @@ -2027,7 +2027,7 @@ export const mockTimelineModel: TimelineModel = { templateTimelineVersion: null, version: '1', savedSearchId: null, - isDataProviderVisible: true, + isDataProviderVisible: false, }; export const mockDataTableModel: DataTableModel = { @@ -2209,7 +2209,7 @@ export const defaultTimelineProps: CreateTimelineProps = { version: null, savedSearchId: null, isDiscoverSavedSearchLoaded: false, - isDataProviderVisible: true, + isDataProviderVisible: false, }, to: '2018-11-05T19:03:25.937Z', notes: null, 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 ea13b019e50ef..b56889964f23e 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 @@ -453,6 +453,7 @@ describe('alert actions', () => { version: null, savedSearchId: null, isDiscoverSavedSearchLoaded: false, + isDataProviderVisible: false, }, to: '2018-11-05T19:03:25.937Z', resolveTimelineConfig: undefined, @@ -1117,7 +1118,6 @@ describe('alert actions', () => { filterQuery: null, }, resolveTimelineConfig: undefined, - isDataProviderVisible: false, }, from: expectedFrom, to: expectedTo, diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap index a20caa83debde..c68e193df3ab4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap @@ -9,7 +9,7 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` data-test-subj="flyout-pane" /> - .c2 { + .c3 { display: block; } @@ -17,7 +17,7 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` padding: 0; } -.c3 { +.c2 { overflow: hidden; display: inline-block; text-overflow: ellipsis; @@ -84,6 +84,16 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = `
+
+ Untitled timeline +
+
+
+
@@ -91,7 +101,7 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock" >
-
- Untitled timeline -
-
-
-
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap index c5494c676118a..6fba5a5c10a41 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -1,50 +1,607 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header rendering renders correctly against snapshot 1`] = ` - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx index 8ce39f9b535d5..905ed19278925 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx @@ -18,6 +18,7 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { TimelineHeader } from '.'; import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline'; import { waitFor } from '@testing-library/react'; +import { TimelineId } from '../../../../../common/types'; const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; @@ -44,13 +45,17 @@ describe('Header', () => { show: true, showCallOutUnauthorizedMsg: false, status: TimelineStatus.active, - timelineId: 'foo', + timelineId: TimelineId.test, timelineType: TimelineType.default, }; describe('rendering', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow( + + + + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx index ffcecc6bf8e7c..cae8739a7bdc8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx @@ -57,6 +57,7 @@ const TimelineHeaderComponent: React.FC = ({ timelineId, }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const getIsDataProviderVisible = useMemo( () => timelineSelectors.dataProviderVisibilitySelector(), [] @@ -66,8 +67,8 @@ const TimelineHeaderComponent: React.FC = ({ (state) => (getTimeline(state, timelineId) ?? timelineDefaults).timelineType ); - const isDataProviderVisible = useDeepEqualSelector((state) => - getIsDataProviderVisible(state, timelineId) + const isDataProviderVisible = useDeepEqualSelector( + (state) => getIsDataProviderVisible(state, timelineId) ?? timelineDefaults.isDataProviderVisible ); const shouldShowQueryBuilder = isDataProviderVisible || timelineType === TimelineType.template; From c3c11b94a8c5bc89b6131d45439d60814ebf8034 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Fri, 3 Nov 2023 15:39:06 +0100 Subject: [PATCH 12/43] fix: cypress tests --- .../__snapshots__/index.test.tsx.snap | 2 +- .../components/sourcerer/index.test.tsx | 209 +++++++++++------- .../authentications_host_table.test.tsx.snap | 2 +- .../components/flyout/action_menu/index.tsx | 4 +- .../flyout/action_menu/new_timeline.tsx | 8 +- .../flyout/action_menu/save_timeline.tsx | 34 ++- .../flyout/action_menu/translations.ts | 8 + .../flyout/header/active_timelines.tsx | 4 +- .../components/flyout/header/index.tsx | 65 +++--- .../header/__snapshots__/index.test.tsx.snap | 64 ++++-- .../timeline/properties/helpers.tsx | 6 +- .../properties/new_template_timeline.tsx | 6 +- .../properties/use_create_timeline.tsx | 14 +- .../timeline/search_or_filter/index.tsx | 8 +- .../search_or_filter/search_or_filter.tsx | 1 + .../cypress/e2e/explore/cases/creation.cy.ts | 3 +- .../alerts/investigate_in_timeline.cy.ts | 10 +- .../timeline_templates/creation.cy.ts | 1 - .../investigations/timelines/creation.cy.ts | 19 +- .../timelines/open_timeline.cy.ts | 9 +- .../timelines/search_or_filter.cy.ts | 2 + .../timelines/unsaved_timeline.cy.ts | 3 +- .../cypress/screens/timeline.ts | 13 +- .../cypress/tasks/api_calls/notes.ts | 6 +- .../cypress/tasks/api_calls/timelines.ts | 19 +- .../cypress/tasks/timeline.ts | 21 +- 26 files changed, 345 insertions(+), 196 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap index 00732ec7b82e8..46c33d5102feb 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap @@ -37,7 +37,7 @@ exports[`HeaderSection it renders 1`] = ` >

{ wrapper.find('[data-test-subj="comboBoxClearButton"]').first().simulate('click'); expect(wrapper.find('[data-test-subj="sourcerer-save"]').first().prop('disabled')).toBeTruthy(); }); - it('Does display signals index on timeline sourcerer', () => { + it('Does display signals index on timeline sourcerer', async () => { const state2 = { ...mockGlobalState, sourcerer: { @@ -559,16 +560,21 @@ describe('Sourcerer component', () => { }; store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const wrapper = mount( + const wrapper = render( ); - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="comboBoxToggleListButton"]`).first().simulate('click'); - expect(wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).at(0).text()).toEqual( - mockGlobalState.sourcerer.signalIndexName - ); + + fireEvent.click(wrapper.getByTestId('timeline-sourcerer-trigger')); + fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton')); + + screen.debug(undefined, 10000000); + await waitFor(() => { + expect(screen.queryAllByTestId('sourcerer-combo-option')[0].textContent).toBe( + mockGlobalState.sourcerer.signalIndexName + ); + }); }); it('Does not display signals index on default sourcerer', () => { const state2 = { @@ -1003,8 +1009,6 @@ describe('Update available', () => { }, }; - let wrapper: ReactWrapper; - beforeEach(() => { (useSourcererDataView as jest.Mock).mockReturnValue({ ...sourcererDataView, @@ -1012,7 +1016,7 @@ describe('Update available', () => { }); store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - wrapper = mount( + render( @@ -1023,67 +1027,108 @@ describe('Update available', () => { }); test('Show Update available label', () => { - expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-badge"]`).exists()).toBeTruthy(); + expect(screen.getByTestId('sourcerer-deprecated-badge')).toBeInTheDocument(); }); test('Show correct tooltip', () => { - expect(wrapper.find(`[data-test-subj="sourcerer-tooltip"]`).prop('content')).toEqual( - 'myFakebeat-*' - ); + expect(screen.getByTestId('sourcerer-tooltip').textContent).toBe('myFakebeat-*'); }); test('Show UpdateDefaultDataViewModal', () => { - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - expect(wrapper.find(`UpdateDefaultDataViewModal`).prop('isShowing')).toEqual(true); + expect(screen.getByTestId('sourcerer-update-data-view-modal')).toBeVisible(); + + // expect(wrapper.find(`UpdateDefaultDataViewModal`).prop('isShowing')).toEqual(true); }); test('Show UpdateDefaultDataViewModal Callout', () => { - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-callout"]`).first().text()).toEqual( + expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( 'This timeline uses a legacy data view selector' ); - expect( - wrapper.find(`[data-test-subj="sourcerer-current-patterns-message"]`).first().text() - ).toEqual('The active index patterns in this timeline are: myFakebeat-*'); + expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( + 'The active index patterns in this timeline are: myFakebeat-*' + ); - expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-message"]`).first().text()).toEqual( + expect(screen.queryAllByTestId('sourcerer-deprecated-message')[0].textContent).toBe( "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view here." ); + + // wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + // + // wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + // + // expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-callout"]`).first().text()).toEqual( + // 'This timeline uses a legacy data view selector' + // ); + // + // expect( + // wrapper.find(`[data-test-subj="sourcerer-current-patterns-message"]`).first().text() + // ).toEqual('The active index patterns in this timeline are: myFakebeat-*'); + // + // expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-message"]`).first().text()).toEqual( + // "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view here." + // ); }); test('Show Add index pattern in UpdateDefaultDataViewModal', () => { - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - expect(wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).text()).toEqual( + expect(screen.queryAllByTestId('sourcerer-update-data-view')[0].textContent).toBe( 'Add index pattern' ); + + // wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + // + // wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + // + // expect(wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).text()).toEqual( + // 'Add index pattern' + // ); }); test('Set all the index patterns from legacy timeline to sourcerer, after clicking on "Add index pattern"', async () => { - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).simulate('click'); + fireEvent.click(screen.queryAllByTestId('sourcerer-update-data-view')[0]); - await waitFor(() => wrapper.update()); - expect(mockDispatch).toHaveBeenCalledWith( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: 'security-solution', - selectedPatterns: ['myFakebeat-*'], - shouldValidateSelectedPatterns: false, - }) - ); + await waitFor(() => { + expect(mockDispatch).toHaveBeenCalledWith( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: 'security-solution', + selectedPatterns: ['myFakebeat-*'], + shouldValidateSelectedPatterns: false, + }) + ); + }); + + // wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + // + // wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + // + // wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).simulate('click'); + // + // await waitFor(() => wrapper.update()); + // expect(mockDispatch).toHaveBeenCalledWith( + // sourcererActions.setSelectedDataView({ + // id: SourcererScopeName.timeline, + // selectedDataViewId: 'security-solution', + // selectedPatterns: ['myFakebeat-*'], + // shouldValidateSelectedPatterns: false, + // }) + // ); }); }); @@ -1212,12 +1257,19 @@ describe('Missing index patterns', () => { }; let wrapper: ReactWrapper; + beforeEach(() => { + const pollForSignalIndexMock = jest.fn(); + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + }); afterEach(() => { jest.clearAllMocks(); }); - test('Show UpdateDefaultDataViewModal CallOut for timeline', () => { + test('Show UpdateDefaultDataViewModal CallOut for timeline', async () => { (useSourcererDataView as jest.Mock).mockReturnValue({ ...sourcererDataView, activePatterns: ['myFakebeat-*'], @@ -1226,36 +1278,33 @@ describe('Missing index patterns', () => { state3.timeline.timelineById[TimelineId.active].timelineType = TimelineType.default; store = createStore(state3, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - wrapper = mount( + render( ); - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); - - wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); - - expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-callout"]`).first().text()).toEqual( - 'This timeline is out of date with the Security Data View' - ); - - expect( - wrapper.find(`[data-test-subj="sourcerer-current-patterns-message"]`).first().text() - ).toEqual('The active index patterns in this timeline are: myFakebeat-*'); - - expect( - wrapper.find(`[data-test-subj="sourcerer-missing-patterns-callout"]`).first().text() - ).toEqual('Security Data View is missing the following index patterns: myFakebeat-*'); - - expect( - wrapper.find(`[data-test-subj="sourcerer-missing-patterns-message"]`).first().text() - ).toEqual( - "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." - ); + fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); + + fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); + + await waitFor(() => { + expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( + 'This timeline is out of date with the Security Data View' + ); + expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( + 'The active index patterns in this timeline are: myFakebeat-*' + ); + expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( + 'security data view is missing the following index patterns: myfakebeat-*' + ); + expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( + "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." + ); + }); }); - test('Show UpdateDefaultDataViewModal CallOut for timeline template', () => { + test('Show UpdateDefaultDataViewModal CallOut for timeline template', async () => { (useSourcererDataView as jest.Mock).mockReturnValue({ ...sourcererDataView, activePatterns: ['myFakebeat-*'], @@ -1268,26 +1317,26 @@ describe('Missing index patterns', () => { ); - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); - wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); - expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-callout"]`).first().text()).toEqual( - 'This timeline template is out of date with the Security Data View' - ); + await waitFor(() => { + expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( + 'This timeline template is out of date with the Security Data View' + ); - expect( - wrapper.find(`[data-test-subj="sourcerer-current-patterns-message"]`).first().text() - ).toEqual('The active index patterns in this timeline template are: myFakebeat-*'); + expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( + 'The active index patterns in this timeline template are: myFakebeat-*' + ); - expect( - wrapper.find(`[data-test-subj="sourcerer-missing-patterns-callout"]`).first().text() - ).toEqual('Security Data View is missing the following index patterns: myFakebeat-*'); + expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( + 'Security Data View is missing the following index patterns: myFakebeat-*' + ); - expect( - wrapper.find(`[data-test-subj="sourcerer-missing-patterns-message"]`).first().text() - ).toEqual( - "We have preserved your timeline template by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." - ); + expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( + "We have preserved your timeline template by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap b/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap index b9fd26239a829..cebd8a483aafb 100644 --- a/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap @@ -105,7 +105,7 @@ exports[`Authentication Host Table Component rendering it renders the host authe class="euiFlexItem emotion-euiFlexItem-grow-1" >

{ diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx index 3ca5fd09a32bc..192227348fce8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx @@ -47,10 +47,14 @@ export const NewTimelineAction = ({ timelineId }: NewTimelineActionProps) => { > - + - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx index 53fc614ed28a4..0e7436ab59d15 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { EuiButton } from '@elastic/eui'; -import React from 'react'; +import { EuiButton, EuiToolTip } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { useEditTimelineOperation } from '../../timeline/header/edit_timeline_button'; import * as i18n from './translations'; @@ -15,21 +16,44 @@ interface SaveTimelineActionProps { } export const SaveTimelineAction = ({ timelineId }: SaveTimelineActionProps) => { + const { + kibanaSecuritySolutionsPrivileges: { crud: hasKibanaCrud }, + } = useUserPrivileges(); + const { openEditTimeline, editTimelineModal } = useEditTimelineOperation({ timelineId, }); - return ( - <> - {editTimelineModal} + + const button = useMemo( + () => ( {i18n.SAVE_TIMELINE_BTN} + ), + [hasKibanaCrud, openEditTimeline] + ); + + return ( + <> + {editTimelineModal} + {hasKibanaCrud ? ( + button + ) : ( + + {button} + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts index 5dc33fd6cc606..6f9f64f0c705e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts @@ -55,3 +55,11 @@ export const NEW_TEMPLATE_TIMELINE = i18n.translate( defaultMessage: 'New Timeline template', } ); + +export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate( + 'xpack.securitySolution.timeline.callOut.unauthorized.message.description', + { + defaultMessage: + 'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx index 6f9e1b61171b2..7bd1b10afc9c4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx @@ -100,7 +100,9 @@ const ActiveTimelinesComponent: React.FC = ({ justifyContent="flexStart" responsive={false} > - {title} + + {title} + {!isOpen && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index a54c2b2a8325a..b7979f599e075 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -19,21 +19,19 @@ import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import { InputsModelId } from '../../../../common/store/inputs/constants'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { TimelineTabs } from '../../../../../common/types/timeline'; import type { State } from '../../../../common/store'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { timelineDefaults } from '../../../store/timeline/defaults'; -import { AddTimelineButton } from '../add_timeline_button'; import { useKibana } from '../../../../common/lib/kibana'; -import { InspectButton } from '../../../../common/components/inspect'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { focusActiveTimelineButton } from '../../timeline/helpers'; import { combineQueries } from '../../../../common/lib/kuery'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { ActiveTimelines } from './active_timelines'; import * as i18n from './translations'; +import { TimelineActionMenu } from '../action_menu'; +import { AddToFavoritesButton } from '../../timeline/properties/helpers'; interface FlyoutHeaderPanelProps { timelineId: string; @@ -123,35 +121,44 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline paddingSize="s" hasShadow={false} data-test-subj="timeline-flyout-header-panel" + data-show={show} style={{ backgroundColor: euiTheme.colors.emptyShade, color: euiTheme.colors.text }} > - - - - - + + + + + + + + + + + + + {show && ( - + - {(activeTab === TimelineTabs.query || activeTab === TimelineTabs.eql) && ( - - - - )} + void; + onClick?: () => void; outline?: boolean; timelineId: string; title?: string; } export const NewTimeline = React.memo( - ({ closeGearMenu, outline = false, timelineId, title = i18n.NEW_TIMELINE }) => { + ({ onClick, outline = false, timelineId, title = i18n.NEW_TIMELINE }) => { const { getButton } = useCreateTimelineButton({ timelineId, timelineType: TimelineType.default, - closeGearMenu, + onClick, }); const button = getButton({ outline, title }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx index 4cdef0f843dfe..3d79fdbf031bd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx @@ -13,14 +13,14 @@ import { TimelineType } from '../../../../../common/api/timeline'; import { useCreateTimelineButton } from './use_create_timeline'; interface OwnProps { - closeGearMenu?: () => void; + onClick?: () => void; outline?: boolean; title?: string; timelineId?: string; } export const NewTemplateTimelineComponent: React.FC = ({ - closeGearMenu, + onClick, outline, title, timelineId = TimelineId.active, @@ -28,7 +28,7 @@ export const NewTemplateTimelineComponent: React.FC = ({ const { getButton } = useCreateTimelineButton({ timelineId, timelineType: TimelineType.template, - closeGearMenu, + onClick, }); const button = getButton({ outline, title }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index 4742bb995fba3..8467f45eabe6d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -27,11 +27,11 @@ import { useDiscoverInTimelineContext } from '../../../../common/components/disc interface Props { timelineId?: string; timelineType: TimelineTypeLiteral; - closeGearMenu?: () => void; + onClick?: () => void; timeRange?: TimeRange; } -export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: Props) => { +export const useCreateTimeline = ({ timelineId, timelineType, onClick }: Props) => { const dispatch = useDispatch(); const defaultDataViewSelector = useMemo(() => sourcererSelectors.defaultDataViewSelector(), []); const { id: dataViewId, patternList: selectedPatterns } = @@ -110,12 +110,12 @@ export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: P const handleCreateNewTimeline = useCallback( (options?: CreateNewTimelineOptions) => { createTimeline({ id: timelineId, show: true, timelineType, timeRange: options?.timeRange }); - if (typeof closeGearMenu === 'function') { - closeGearMenu(); + if (typeof onClick === 'function') { + onClick(); } resetDiscoverAppState(); }, - [createTimeline, timelineId, timelineType, closeGearMenu, resetDiscoverAppState] + [createTimeline, timelineId, timelineType, onClick, resetDiscoverAppState] ); return handleCreateNewTimeline; @@ -125,11 +125,11 @@ interface CreateNewTimelineOptions { timeRange?: TimeRange; } -export const useCreateTimelineButton = ({ timelineId, timelineType, closeGearMenu }: Props) => { +export const useCreateTimelineButton = ({ timelineId, timelineType, onClick }: Props) => { const handleCreateNewTimeline = useCreateTimeline({ timelineId, timelineType, - closeGearMenu, + onClick, }); const getButton = useCallback( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index 1770f0d82f5e7..72e1ecc2f8e8a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -181,7 +181,13 @@ const StatefulSearchOrFilterComponent = React.memo( {filters && filters.length > 0 ? ( - + ( } isSelected={isDataProviderVisible} iconType={'timeline'} + data-test-subj="toggle-data-provider" size="m" display="base" aria-label={ diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts index 79730b3c45854..193553a0514ef 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts @@ -34,7 +34,7 @@ import { CASES_METRIC, UNEXPECTED_METRICS, } from '../../../screens/case_details'; -import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../../../screens/timeline'; +import { TIMELINE_QUERY, TIMELINE_TITLE } from '../../../screens/timeline'; import { OVERVIEW_CASE_DESCRIPTION, OVERVIEW_CASE_NAME } from '../../../screens/overview'; @@ -124,7 +124,6 @@ describe('Cases', { tags: ['@ess', '@serverless'] }, () => { openCaseTimeline(); cy.get(TIMELINE_TITLE).contains(this.mycase.timeline.title); - cy.get(TIMELINE_DESCRIPTION).contains(this.mycase.timeline.description); cy.get(TIMELINE_QUERY).should('have.text', this.mycase.timeline.query); visitWithTimeRange(OVERVIEW_URL); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts index 89bae99047759..cde25abd3968b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts @@ -7,8 +7,12 @@ import { disableExpandableFlyout } from '../../../tasks/api_calls/kibana_advanced_settings'; import { getNewRule } from '../../../objects/rule'; -import { PROVIDER_BADGE, QUERY_TAB_BUTTON, TIMELINE_TITLE } from '../../../screens/timeline'; -import { FILTER_BADGE } from '../../../screens/alerts'; +import { + PROVIDER_BADGE, + QUERY_TAB_BUTTON, + TIMELINE_FILTER_BADGE, + TIMELINE_TITLE, +} from '../../../screens/timeline'; import { expandFirstAlert, investigateFirstAlertInTimeline } from '../../../tasks/alerts'; import { createRule } from '../../../tasks/api_calls/rules'; @@ -80,7 +84,7 @@ describe('Investigate in timeline', { tags: ['@ess', '@serverless'] }, () => { cy.get(QUERY_TAB_BUTTON).should('contain.text', alertCount); // The correct filter is applied to the timeline query - cy.get(FILTER_BADGE).should( + cy.get(TIMELINE_FILTER_BADGE).should( 'have.text', ' {"bool":{"must":[{"term":{"process.args":"-zsh"}},{"term":{"process.args":"unique"}}]}}' ); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts index e040d7ed6c6f3..2968cd262d300 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts @@ -113,7 +113,6 @@ describe('Timeline Templates', { tags: ['@ess', '@serverless'] }, () => { cy.wait('@timeline', { timeout: 100000 }); cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); - cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/creation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/creation.cy.ts index b7236d7ea0d80..4d33b1ecea314 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/creation.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/creation.cy.ts @@ -12,14 +12,13 @@ import { LOCKED_ICON, NOTES_TEXT, PIN_EVENT, - TIMELINE_DESCRIPTION, TIMELINE_FILTER, TIMELINE_FLYOUT_WRAPPER, TIMELINE_QUERY, TIMELINE_PANEL, TIMELINE_TAB_CONTENT_GRAPHS_NOTES, - EDIT_TIMELINE_BTN, - EDIT_TIMELINE_TOOLTIP, + SAVE_TIMELINE_ACTION_BTN, + SAVE_TIMELINE_TOOLTIP, } from '../../../screens/timeline'; import { createTimelineTemplate } from '../../../tasks/api_calls/timelines'; @@ -59,9 +58,7 @@ describe('Create a timeline from a template', { tags: ['@ess', '@serverless'] }, selectCustomTemplates(); expandEventAction(); clickingOnCreateTimelineFormTemplateBtn(); - cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); - cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query); closeTimeline(); }); @@ -72,7 +69,7 @@ describe('Timelines', (): void => { cleanKibana(); }); - describe('Toggle create timeline from plus icon', () => { + describe('Toggle create timeline from "New" btn', () => { context('Privileges: CRUD', { tags: '@ess' }, () => { beforeEach(() => { login(); @@ -80,6 +77,7 @@ describe('Timelines', (): void => { }); it('toggle create timeline ', () => { + openTimelineUsingToggle(); createNewTimeline(); addNameAndDescriptionToTimeline(getTimeline()); cy.get(TIMELINE_PANEL).should('be.visible'); @@ -93,12 +91,13 @@ describe('Timelines', (): void => { }); it('should not be able to create/update timeline ', () => { + openTimelineUsingToggle(); createNewTimeline(); cy.get(TIMELINE_PANEL).should('be.visible'); - cy.get(EDIT_TIMELINE_BTN).should('be.disabled'); - cy.get(EDIT_TIMELINE_BTN).first().realHover(); - cy.get(EDIT_TIMELINE_TOOLTIP).should('be.visible'); - cy.get(EDIT_TIMELINE_TOOLTIP).should( + cy.get(SAVE_TIMELINE_ACTION_BTN).should('be.disabled'); + cy.get(SAVE_TIMELINE_ACTION_BTN).first().realHover(); + cy.get(SAVE_TIMELINE_TOOLTIP).should('be.visible'); + cy.get(SAVE_TIMELINE_TOOLTIP).should( 'have.text', 'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.' ); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/open_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/open_timeline.cy.ts index 7f9083acb0b9f..5a7e32d06e340 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/open_timeline.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/open_timeline.cy.ts @@ -7,11 +7,7 @@ import { getTimeline } from '../../../objects/timeline'; -import { - TIMELINE_DESCRIPTION, - TIMELINE_TITLE, - OPEN_TIMELINE_MODAL, -} from '../../../screens/timeline'; +import { TIMELINE_TITLE, OPEN_TIMELINE_MODAL } from '../../../screens/timeline'; import { TIMELINES_DESCRIPTION, TIMELINES_PINNED_EVENT_COUNT, @@ -26,6 +22,7 @@ import { cleanKibana } from '../../../tasks/common'; import { login } from '../../../tasks/login'; import { visit } from '../../../tasks/navigation'; +import { openTimelineUsingToggle } from '../../../tasks/security_main'; import { markAsFavorite, openTimelineById, @@ -66,6 +63,7 @@ describe('Open timeline', { tags: ['@serverless', '@ess'] }, () => { beforeEach(function () { login(); visit(TIMELINES_URL); + openTimelineUsingToggle(); openTimelineFromSettings(); openTimelineById(this.timelineId); }); @@ -78,7 +76,6 @@ describe('Open timeline', { tags: ['@serverless', '@ess'] }, () => { cy.get(TIMELINES_NOTES_COUNT).last().should('have.text', '1'); cy.get(TIMELINES_FAVORITE).last().should('exist'); cy.get(TIMELINE_TITLE).should('have.text', getTimeline().title); - cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/search_or_filter.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/search_or_filter.cy.ts index a6f5ae49f5d8a..2e8e7631122f2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/search_or_filter.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/search_or_filter.cy.ts @@ -22,6 +22,7 @@ import { changeTimelineQueryLanguage, executeTimelineKQL, executeTimelineSearch, + showDataProviderQueryBuilder, } from '../../../tasks/timeline'; import { waitForTimelinesPanelToBeLoaded } from '../../../tasks/timelines'; @@ -64,6 +65,7 @@ describe('Timeline search and filters', { tags: ['@ess', '@serverless'] }, () => openTimelineUsingToggle(); cy.intercept('PATCH', '/api/timeline').as('update'); cy.get(LOADING_INDICATOR).should('not.exist'); + showDataProviderQueryBuilder(); cy.get(TIMELINE_SEARCH_OR_FILTER).click(); cy.get(TIMELINE_SEARCH_OR_FILTER).should('exist'); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts index 9f368ca7b1220..57768d538a8f4 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/unsaved_timeline.cy.ts @@ -25,7 +25,7 @@ import { } from '../../../tasks/kibana_navigation'; import { login } from '../../../tasks/login'; import { visitWithTimeRange } from '../../../tasks/navigation'; -import { closeTimelineUsingToggle } from '../../../tasks/security_main'; +import { closeTimelineUsingToggle, openTimelineUsingToggle } from '../../../tasks/security_main'; import { addNameAndDescriptionToTimeline, createNewTimeline, @@ -54,6 +54,7 @@ describe('Save Timeline Prompts', { tags: ['@ess', '@serverless', '@brokenInServ beforeEach(() => { login(); visitWithTimeRange(hostsUrl('allHosts')); + openTimelineUsingToggle(); createNewTimeline(); }); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts index 2c89a0a5877bd..b425c7d6eedbd 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts @@ -29,7 +29,7 @@ export const CORRELATION_EVENT_TABLE_CELL = export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; -export const COMBO_BOX = '.euiComboBoxOption__content'; +export const COMBO_BOX = 'button.euiFilterSelectItem[role="option"]'; export const COMBO_BOX_INPUT = '[data-test-subj="comboBoxInput"]'; @@ -354,3 +354,14 @@ export const OPEN_TIMELINE_MODAL_SEARCH_BAR = `${OPEN_TIMELINE_MODAL} ${getData export const OPEN_TIMELINE_MODAL_TIMELINE_NAMES = `${OPEN_TIMELINE_MODAL} ${getDataTestSubjectSelectorStartWith( 'timeline-title-' )}`; + +export const TIMELINE_FILTER_BADGE = `[data-test-subj^='timeline-filters-container'] [data-test-subj^="filter-badge"]`; + +export const NEW_TIMELINE_ACTION = getDataTestSubjectSelector('new-timeline-action'); + +export const SAVE_TIMELINE_ACTION = getDataTestSubjectSelector('save-timeline-action'); +export const SAVE_TIMELINE_ACTION_BTN = getDataTestSubjectSelector('save-timeline-action-btn'); + +export const SAVE_TIMELINE_TOOLTIP = getDataTestSubjectSelector('save-timeline-btn-tooltip'); + +export const TOGGLE_DATA_PROVIDER_BTN = getDataTestSubjectSelector('toggle-data-provider'); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/notes.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/notes.ts index d1addc0407900..21f718dc355d1 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/notes.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/notes.ts @@ -17,5 +17,9 @@ export const addNoteToTimeline = ( version: null, note: { note, timelineId }, }, - headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, + headers: { + 'kbn-xsrf': 'cypress-creds', + 'x-elastic-internal-origin': 'security-solution', + 'elastic-api-version': '2023-10-31', + }, }); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/timelines.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/timelines.ts index 2f555e8a9ed9a..cf974583c3107 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/timelines.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/timelines.ts @@ -53,7 +53,11 @@ export const createTimeline = (timeline: CompleteTimeline) => : {}), }, }, - headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, + headers: { + 'kbn-xsrf': 'cypress-creds', + 'x-elastic-internal-origin': 'security-solution', + 'elastic-api-version': '2023-10-31', + }, }); export const createTimelineTemplate = (timeline: CompleteTimeline) => @@ -99,14 +103,23 @@ export const createTimelineTemplate = (timeline: CompleteTimeline) => savedQueryId: null, }, }, - headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, + headers: { + 'kbn-xsrf': 'cypress-creds', + 'x-elastic-internal-origin': 'security-solution', + 'elastic-api-version': '2023-10-31', + }, }); export const loadPrepackagedTimelineTemplates = () => rootRequest({ method: 'POST', url: 'api/timeline/_prepackaged', - headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, + headers: { + 'kbn-xsrf': 'cypress-creds', + 'x-elastic-internal-origin': 'security-solution', + + 'elastic-api-version': '2023-10-31', + }, }); export const favoriteTimeline = ({ diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts b/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts index 2dc4447e75ffd..81aa4f796965c 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts @@ -87,6 +87,9 @@ import { OPEN_TIMELINE_MODAL_TIMELINE_NAMES, OPEN_TIMELINE_MODAL_SEARCH_BAR, OPEN_TIMELINE_MODAL, + NEW_TIMELINE_ACTION, + SAVE_TIMELINE_ACTION, + TOGGLE_DATA_PROVIDER_BTN, } from '../screens/timeline'; import { REFRESH_BUTTON, TIMELINE } from '../screens/timelines'; import { drag, drop } from './common'; @@ -123,7 +126,7 @@ export const addNameAndDescriptionToTimeline = ( modalAlreadyOpen: boolean = false ) => { if (!modalAlreadyOpen) { - cy.get(TIMELINE_EDIT_MODAL_OPEN_BUTTON).first().click(); + cy.get(SAVE_TIMELINE_ACTION).click(); } cy.get(TIMELINE_TITLE_INPUT).type(`${timeline.title}{enter}`); cy.get(TIMELINE_TITLE_INPUT).should('have.attr', 'value', timeline.title); @@ -195,7 +198,7 @@ export const addFilter = (filter: TimelineFilter): Cypress.Chainable { }; export const createNewTimeline = () => { - cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click(); - cy.get(TIMELINE_SETTINGS_ICON).should('be.visible'); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(1000); + cy.get(NEW_TIMELINE_ACTION).should('be.visible').trigger('click'); cy.get(CREATE_NEW_TIMELINE).eq(0).should('be.visible').click({ force: true }); }; export const openCreateTimelineOptionsPopover = () => { - cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').should('be.visible').click(); + cy.get(NEW_TIMELINE_ACTION).filter(':visible').should('be.visible').click(); }; export const closeCreateTimelineOptionsPopover = () => { @@ -366,7 +366,6 @@ export const openTimelineInspectButton = () => { }; export const openTimelineFromSettings = () => { - openCreateTimelineOptionsPopover(); cy.get(OPEN_TIMELINE_ICON).should('be.visible'); cy.get(OPEN_TIMELINE_ICON).click(); }; @@ -505,3 +504,9 @@ export const openTimelineFromOpenTimelineModal = (timelineName: string) => { cy.get(OPEN_TIMELINE_MODAL).should('contain.text', timelineName); cy.get(OPEN_TIMELINE_MODAL_TIMELINE_NAMES).first().click(); }; + +export const showDataProviderQueryBuilder = () => { + cy.get(TOGGLE_DATA_PROVIDER_BTN).should('have.attr', 'aria-pressed', 'false'); + cy.get(TOGGLE_DATA_PROVIDER_BTN).trigger('click'); + cy.get(TOGGLE_DATA_PROVIDER_BTN).should('have.attr', 'aria-pressed', 'true'); +}; From f244f03957c7b53ea722fb177564c6e0c173ed3b Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Tue, 7 Nov 2023 11:23:11 +0100 Subject: [PATCH 13/43] fix: adapt UI according to new timeline saving strategy --- .../components/exit_full_screen/index.tsx | 10 +- .../components/sourcerer/index.test.tsx | 1 - .../rules/eql_query_bar/eql_query_bar.tsx | 32 +++++- .../components/rules/eql_query_bar/footer.tsx | 8 +- .../flyout/action_menu/save_timeline.tsx | 7 +- .../save_timeline_button.test.tsx | 4 +- .../action_menu}/save_timeline_button.tsx | 2 +- .../action_menu}/save_timeline_modal.test.tsx | 2 +- .../action_menu}/save_timeline_modal.tsx | 8 +- .../flyout/action_menu/translations.ts | 106 ++++++++++++++++++ .../flyout/add_timeline_button/index.tsx | 4 +- .../timeline/eql_tab_content/index.tsx | 23 ++-- .../timeline/header/timeline_save_prompt.tsx | 2 +- .../timeline/header/translations.ts | 98 ---------------- .../timeline/properties/helpers.test.tsx | 2 +- .../properties/new_template_timeline.test.tsx | 4 +- .../components/timeline/query_bar/index.tsx | 15 ++- 17 files changed, 183 insertions(+), 145 deletions(-) rename x-pack/plugins/security_solution/public/timelines/components/{timeline/header => flyout/action_menu}/save_timeline_button.test.tsx (96%) rename x-pack/plugins/security_solution/public/timelines/components/{timeline/header => flyout/action_menu}/save_timeline_button.tsx (98%) rename x-pack/plugins/security_solution/public/timelines/components/{timeline/header => flyout/action_menu}/save_timeline_modal.test.tsx (99%) rename x-pack/plugins/security_solution/public/timelines/components/{timeline/header => flyout/action_menu}/save_timeline_modal.tsx (96%) diff --git a/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx index ee47fca53543b..d4652c4f5b629 100644 --- a/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx @@ -7,16 +7,11 @@ import { EuiButton, EuiWindowEvent } from '@elastic/eui'; import React, { useCallback } from 'react'; -import styled from 'styled-components'; import * as i18n from './translations'; export const EXIT_FULL_SCREEN_CLASS_NAME = 'exit-full-screen'; -const StyledEuiButton = styled(EuiButton)` - margin: ${({ theme }) => theme.eui.euiSizeS}; -`; - interface Props { fullScreen: boolean; setFullScreen: (fullScreen: boolean) => void; @@ -45,16 +40,17 @@ const ExitFullScreenComponent: React.FC = ({ fullScreen, setFullScreen }) return ( <> - {i18n.EXIT_FULL_SCREEN} - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index d5a42601c8ad4..7862057e0a5db 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -1256,7 +1256,6 @@ describe('Missing index patterns', () => { }, }; - let wrapper: ReactWrapper; beforeEach(() => { const pollForSignalIndexMock = jest.fn(); (useSignalHelpers as jest.Mock).mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx index a60c24378902f..56b3eaa2bec3f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx @@ -14,6 +14,7 @@ import { EuiFormRow, EuiSpacer, EuiTextArea } from '@elastic/eui'; import type { DataViewBase, Filter, Query } from '@kbn/es-query'; import { FilterManager } from '@kbn/data-plugin/public'; +import { euiThemeVars } from '@kbn/ui-theme'; import type { FieldHook } from '../../../../shared_imports'; import { FilterBar } from '../../../../common/components/filter_bar'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; @@ -30,10 +31,33 @@ import { useKibana } from '../../../../common/lib/kibana'; const TextArea = styled(EuiTextArea)` display: block; - border: ${({ theme }) => theme.eui.euiBorderThin}; - border-bottom: 0; + border: 0; box-shadow: none; + border-radius: 0px; min-height: ${({ theme }) => theme.eui.euiFormControlHeight}; + &:focus { + box-shadow: none; + } +`; + +const StyledFormRow = styled(EuiFormRow)` + border: ${({ theme }) => theme.eui.euiBorderThin}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; + .euiFormRow__labelWrapper { + background: ${({ theme }) => theme.eui.euiColorLightestShade}; + border-top-left-radius: ${({ theme }) => theme.eui.euiBorderRadius}; + border-top-right-radius: ${({ theme }) => theme.eui.euiBorderRadius}; + padding: 8px 10px; + margin-bottom: 0px; + label { + color: ${euiThemeVars.euiTextSubduedColor}; + &.euiFormLabel-isInvalid { + color: ${euiThemeVars.euiColorDangerText}; + } + } + } + .euiFormRow__fieldWrapper { + } `; export interface FieldValueQueryBar { @@ -157,7 +181,7 @@ export const EqlQueryBar: FC = ({ ); return ( - = ({ )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx index c9e74b6a9acf5..a0647a2c953c2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx @@ -14,7 +14,6 @@ import { EuiFlexItem, EuiFormRow, EuiLoadingSpinner, - EuiPanel, EuiPopover, EuiPopoverTitle, } from '@elastic/eui'; @@ -44,9 +43,11 @@ export interface Props { type SizeVoidFunc = (newSize: string) => void; -const Container = styled(EuiPanel)` +const Container = styled(EuiFlexGroup)` border-radius: 0; - background: ${({ theme }) => theme.eui.euiPageBackgroundColor}; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + background: ${({ theme }) => theme.eui.euiColorLightestShade}; padding: ${({ theme }) => theme.eui.euiSizeXS} ${({ theme }) => theme.eui.euiSizeS}; `; @@ -173,6 +174,7 @@ export const EqlQueryBarFooter: FC = ({ )} + {onOptionsChange && ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx index c3eff892c36ea..29a02e83da892 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline.tsx @@ -6,17 +6,12 @@ */ import React from 'react'; -import { useUserPrivileges } from '../../../../common/components/user_privileges'; -import { SaveTimelineButton } from '../../timeline/header/save_timeline_button'; +import { SaveTimelineButton } from './save_timeline_button'; interface SaveTimelineActionProps { timelineId: string; } export const SaveTimelineAction = ({ timelineId }: SaveTimelineActionProps) => { - const { - kibanaSecuritySolutionsPrivileges: { crud: hasKibanaCrud }, - } = useUserPrivileges(); - return ; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx rename to x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.test.tsx index 7c05526594501..a7259e256bd7e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.test.tsx @@ -11,7 +11,7 @@ import type { SaveTimelineButtonProps } from './save_timeline_button'; import { SaveTimelineButton } from './save_timeline_button'; import { TestProviders } from '../../../../common/mock'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; -import { getTimelineStatusByIdSelector } from '../../flyout/header/selectors'; +import { getTimelineStatusByIdSelector } from '../header/selectors'; import { TimelineStatus } from '../../../../../common/api/timeline'; const TEST_ID = { @@ -28,7 +28,7 @@ jest.mock('react-redux', () => { jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/components/user_privileges'); -jest.mock('../../flyout/header/selectors', () => { +jest.mock('../header/selectors', () => { return { getTimelineStatusByIdSelector: jest.fn().mockReturnValue(() => ({ status: 'draft', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx rename to x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx index 7a025259a6574..c3d3c5c7adfd6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx @@ -8,7 +8,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiButton, EuiToolTip, EuiTourStep, EuiCode, EuiText, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { getTimelineStatusByIdSelector } from '../../flyout/header/selectors'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { TimelineStatus } from '../../../../../common/api/timeline'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; @@ -18,6 +17,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_ex import { SaveTimelineModal } from './save_timeline_modal'; import * as timelineTranslations from './translations'; +import { getTimelineStatusByIdSelector } from '../header/selectors'; export interface SaveTimelineButtonProps { timelineId: string; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_modal.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.test.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_modal.test.tsx rename to x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.test.tsx index 8ee6bf807913a..33fa8f17880f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.test.tsx @@ -18,7 +18,7 @@ jest.mock('../../../../common/hooks/use_selector', () => ({ useDeepEqualSelector: jest.fn(), })); -jest.mock('../properties/use_create_timeline', () => ({ +jest.mock('../../timeline/properties/use_create_timeline', () => ({ useCreateTimeline: jest.fn(), })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_modal.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_modal.tsx rename to x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.tsx index f38cb4784ec17..bfbbdb4c4a905 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_modal.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.tsx @@ -26,13 +26,13 @@ import { TimelineId } from '../../../../../common/types/timeline'; import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { NOTES_PANEL_WIDTH } from '../properties/notes_size'; -import { useCreateTimeline } from '../properties/use_create_timeline'; -import * as commonI18n from '../properties/translations'; +import * as commonI18n from '../../timeline/properties/translations'; import * as i18n from './translations'; -import { formSchema } from './schema'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { TIMELINE_ACTIONS } from '../../../../common/lib/apm/user_actions'; +import { useCreateTimeline } from '../../timeline/properties/use_create_timeline'; +import { formSchema } from '../../timeline/header/schema'; +import { NOTES_PANEL_WIDTH } from '../../timeline/properties/notes_size'; const CommonUseField = getUseField({ component: Field }); interface SaveTimelineModalProps { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts index 6f9f64f0c705e..6c19410ef3140 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts @@ -6,6 +6,8 @@ */ import { i18n } from '@kbn/i18n'; +import type { TimelineTypeLiteral } from '../../../../../common/api/timeline'; +import { TimelineType } from '../../../../../common/api/timeline'; export const NEW_TIMELINE_BTN = i18n.translate( 'xpack.securitySolution.flyout.timeline.actionMenu.newTimelineBtn', @@ -63,3 +65,107 @@ export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate( 'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.', } ); + +export const CALL_OUT_IMMUTABLE = i18n.translate( + 'xpack.securitySolution.timeline.callOut.immutable.message.description', + { + defaultMessage: + 'This prebuilt timeline template cannot be modified. To make changes, please duplicate this template and make modifications to the duplicate template.', + } +); + +export const SAVE_TIMELINE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.header', + { + defaultMessage: 'Save Timeline', + } +); + +export const SAVE_TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.header', + { + defaultMessage: 'Save Timeline Template', + } +); + +export const SAVE = i18n.translate('xpack.securitySolution.timeline.nameTimeline.save.title', { + defaultMessage: 'Save', +}); + +export const NAME_TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.nameTimelineTemplate.modal.header', + { + defaultMessage: 'Name Timeline Template', + } +); + +export const DISCARD_TIMELINE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.discard.title', + { + defaultMessage: 'Discard Timeline', + } +); + +export const DISCARD_TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.discard.title', + { + defaultMessage: 'Discard Timeline Template', + } +); + +export const CLOSE_MODAL = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.close.title', + { + defaultMessage: 'Close', + } +); + +export const UNSAVED_TIMELINE_WARNING = (timelineType: TimelineTypeLiteral) => + i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.warning.title', { + values: { + timeline: timelineType === TimelineType.template ? 'timeline template' : 'timeline', + }, + defaultMessage: 'You have an unsaved {timeline}. Do you wish to save it?', + }); + +export const TIMELINE_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel', + { + defaultMessage: 'Title', + } +); + +export const TIMELINE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.descriptionLabel', + { + defaultMessage: 'Description', + } +); + +export const OPTIONAL = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.optionalLabel', + { + defaultMessage: 'Optional', + } +); + +export const SAVE_TOUR_CLOSE = i18n.translate( + 'xpack.securitySolution.timeline.flyout.saveTour.closeButton', + { + defaultMessage: 'Close', + } +); + +export const TITLE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.titleTitle', + { + defaultMessage: 'Title', + } +); + +export const SAVE_TOUR_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.flyout.saveTour.title', + { + defaultMessage: 'Timeline changes now require manual saves', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.tsx index 74662e7563201..69b71adb9fb6e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.tsx @@ -70,13 +70,13 @@ const AddTimelineButtonComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index d769127308743..e5d5659940782 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -22,6 +22,7 @@ import { connect, useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; +import { euiThemeVars } from '@kbn/ui-theme'; import type { ControlColumnProps } from '../../../../../common/types'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; @@ -58,7 +59,7 @@ import { useLicense } from '../../../../common/hooks/use_license'; import { HeaderActions } from '../../../../common/components/header_actions/header_actions'; const TimelineHeaderContainer = styled.div` - margin-top: 6px; + margin-top: ${euiThemeVars.euiSizeS}; width: 100%; `; @@ -260,7 +261,7 @@ export const EqlTabContentComponent: React.FC = ({ hasBorder={false} > @@ -270,22 +271,22 @@ export const EqlTabContentComponent: React.FC = ({ setFullScreen={setTimelineFullScreen} /> )} - - - - - - {activeTab === TimelineTabs.eql && ( )} + + + + + + - - - + + + - i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.warning.title', { - values: { - timeline: timelineType === TimelineType.template ? 'timeline template' : 'timeline', - }, - defaultMessage: 'You have an unsaved {timeline}. Do you wish to save it?', - }); - -export const TITLE = i18n.translate( - 'xpack.securitySolution.timeline.saveTimeline.modal.titleTitle', - { - defaultMessage: 'Title', - } -); - -export const TIMELINE_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel', - { - defaultMessage: 'Title', - } -); - -export const TIMELINE_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.timeline.saveTimeline.modal.descriptionLabel', - { - defaultMessage: 'Description', - } -); - -export const OPTIONAL = i18n.translate( - 'xpack.securitySolution.timeline.saveTimeline.modal.optionalLabel', - { - defaultMessage: 'Optional', - } -); - -export const SAVE_TOUR_CLOSE = i18n.translate( - 'xpack.securitySolution.timeline.flyout.saveTour.closeButton', - { - defaultMessage: 'Close', - } -); - -export const SAVE_TOUR_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.flyout.saveTour.title', - { - defaultMessage: 'Timeline changes now require manual saves', - } -); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx index 6a86700c984d9..919bca8308d89 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx @@ -44,7 +44,7 @@ describe('NewTimeline', () => { const mockGetButton = jest.fn().mockReturnValue('<>'); const props: NewTimelineProps = { - closeGearMenu: jest.fn(), + onClick: jest.fn(), timelineId: 'mockTimelineId', title: 'mockTitle', }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx index ac69b86ec5803..10ad06c17d5bd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx @@ -55,7 +55,7 @@ describe('NewTemplateTimeline', () => { wrapper = mount( - + ); }); @@ -92,7 +92,7 @@ describe('NewTemplateTimeline', () => { wrapper = mount( - + ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index fe3bd43f14078..894abf5fcf75a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -15,6 +15,7 @@ import type { Filter, Query } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import type { FilterManager, SavedQuery, SavedQueryTimeFilter } from '@kbn/data-plugin/public'; import styled from '@emotion/styled'; +import { createGlobalStyle } from 'styled-components'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; @@ -78,6 +79,17 @@ const SearchBarContainer = styled.div` } `; +const SearchBarFullScreenVisibilityStyle = createGlobalStyle` +.euiDataGrid__restrictBody { + .search_bar_container { + .headerGlobalNav, + .kbnQueryBar { + display: flex; + } + } +} +`; + export const TIMELINE_FILTER_DROP_AREA = 'timeline-filter-drop-area'; const getNonDropAreaFilters = (filters: Filter[] = []) => @@ -296,7 +308,8 @@ export const QueryBarTimeline = memo( ); return ( - + + Date: Wed, 8 Nov 2023 09:08:09 +0100 Subject: [PATCH 14/43] fix: cypress+ftr tests --- .../common/components/query_bar/index.tsx | 56 +++++++++---------- .../action_menu/save_timeline_button.tsx | 3 +- .../flyout/header/active_timelines.tsx | 17 +++--- .../components/flyout/header/index.tsx | 18 +++++- .../flyout/header/timeline_status_info.tsx | 46 +++++++++++++++ .../timelines/store/timeline/defaults.ts | 1 + .../timelines/flyout_button.cy.ts | 18 +----- .../cypress/tasks/timeline.ts | 8 +-- .../page_objects/timeline/index.ts | 13 ++--- 9 files changed, 113 insertions(+), 67 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_status_info.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index 8cdd48e024e30..08818172bca5a 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -141,35 +141,33 @@ export const QueryBar = memo( const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []); const arrDataView = useMemo(() => (dataView != null ? [dataView] : []), [dataView]); return ( - <> - - + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx index c3d3c5c7adfd6..450c00351cf2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx @@ -92,10 +92,11 @@ export const SaveTimelineButton = React.memo(({ timelin fill color="primary" onClick={openEditTimeline} + size="s" iconType="save" isLoading={isSaving} disabled={!canEditTimeline} - data-test-subj="save-timeline-btn" + data-test-subj="save-timeline-action-btn" id={SAVE_BUTTON_ELEMENT_ID} > {timelineTranslations.SAVE} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx index 7a3bf2f9f5cab..52d255b5ad7a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx @@ -33,6 +33,7 @@ interface ActiveTimelinesProps { timelineType: TimelineType; isOpen: boolean; updated?: number; + changed: boolean; } const StyledEuiButtonEmpty = styled(EuiButtonEmpty)` @@ -54,8 +55,10 @@ const ActiveTimelinesComponent: React.FC = ({ timelineTitle, updated, isOpen, + changed, }) => { const dispatch = useDispatch(); + const handleToggleOpen = useCallback(() => { dispatch(timelineActions.showTimeline({ id: timelineId, show: !isOpen })); focusActiveTimelineButton(); @@ -67,6 +70,8 @@ const ActiveTimelinesComponent: React.FC = ({ ? UNTITLED_TEMPLATE : UNTITLED_TIMELINE; + const timelineChangeStatus = useMemo(() => {}, []); + const tooltipContent = useMemo(() => { if (timelineStatus === TimelineStatus.draft) { return <>{i18n.UNSAVED}; @@ -108,13 +113,11 @@ const ActiveTimelinesComponent: React.FC = ({ )} - {timelineStatus === TimelineStatus.draft ? ( - - - - - - ) : null} + + + + + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index b7979f599e075..837326cdaa66f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -32,6 +32,7 @@ import { ActiveTimelines } from './active_timelines'; import * as i18n from './translations'; import { TimelineActionMenu } from '../action_menu'; import { AddToFavoritesButton } from '../../timeline/properties/helpers'; +import { TimelineStatusInfoComponent } from './timeline_status_info'; interface FlyoutHeaderPanelProps { timelineId: string; @@ -58,6 +59,7 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline show, filters, kqlMode, + changed, } = useDeepEqualSelector((state) => pick( [ @@ -71,6 +73,7 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline 'show', 'filters', 'kqlMode', + 'changed', ], getTimeline(state, timelineId) ?? timelineDefaults ) @@ -141,9 +144,17 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline timelineStatus={timelineStatus} isOpen={show} updated={updated} + changed={changed} /> + + + @@ -151,7 +162,12 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline {show && ( - + (({ status, updated, changed }) => { + const isUnsaved = status === TimelineStatus.draft; + + let statusContent: React.ReactNode = null; + if (isUnsaved) { + statusContent = {i18n.UNSAVED}; + } else if (changed) { + statusContent = {i18n.UNSAVED_CHANGES}; + } else { + statusContent = ( + <> + {i18n.SAVED}{' '} + + + ); + } + return ( + + {statusContent} + + ); +}); +TimelineStatusInfoComponent.displayName = 'TimelineStatusInfoComponent'; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index e1c01f226ca78..af4045aec077b 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -81,6 +81,7 @@ export const timelineDefaults: SubsetTimelineModel & savedSearchId: null, isDiscoverSavedSearchLoaded: false, isDataProviderVisible: false, + changed: false, }; export const getTimelineManageDefaults = (id: string) => ({ diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/flyout_button.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/flyout_button.cy.ts index a6896de020a5a..af1d7d8247481 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/flyout_button.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/flyout_button.cy.ts @@ -6,7 +6,7 @@ */ import { TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON } from '../../../screens/security_main'; -import { CREATE_NEW_TIMELINE, TIMELINE_FLYOUT_HEADER } from '../../../screens/timeline'; +import { TIMELINE_FLYOUT_HEADER } from '../../../screens/timeline'; import { cleanKibana } from '../../../tasks/common'; import { waitForAllHostsToBeLoaded } from '../../../tasks/hosts/all_hosts'; @@ -17,10 +17,6 @@ import { closeTimelineUsingToggle, openTimelineUsingToggle, } from '../../../tasks/security_main'; -import { - closeCreateTimelineOptionsPopover, - openCreateTimelineOptionsPopover, -} from '../../../tasks/timeline'; import { hostsUrl } from '../../../urls/navigation'; @@ -74,18 +70,6 @@ describe('timeline flyout button', () => { } ); - it( - 'the `(+)` button popover menu owns focus when open', - { tags: ['@ess', '@serverless'] }, - () => { - openCreateTimelineOptionsPopover(); - cy.get(CREATE_NEW_TIMELINE).focus(); - cy.get(CREATE_NEW_TIMELINE).should('have.focus'); - closeCreateTimelineOptionsPopover(); - cy.get(CREATE_NEW_TIMELINE).should('not.exist'); - } - ); - it( 'should render the global search dropdown when the input is focused', { tags: ['@ess'] }, diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts b/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts index b85cca1b1c945..09cdb6158073f 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts @@ -54,7 +54,6 @@ import { CREATE_NEW_TIMELINE_TEMPLATE, OPEN_TIMELINE_TEMPLATE_ICON, TIMELINE_SAVE_MODAL, - TIMELINE_SAVE_MODAL_OPEN_BUTTON, TIMELINE_EDIT_MODAL_SAVE_BUTTON, TIMELINE_PROGRESS_BAR, QUERY_TAB_BUTTON, @@ -92,6 +91,7 @@ import { NEW_TIMELINE_ACTION, SAVE_TIMELINE_ACTION, TOGGLE_DATA_PROVIDER_BTN, + SAVE_TIMELINE_ACTION_BTN, } from '../screens/timeline'; import { REFRESH_BUTTON, TIMELINE } from '../screens/timelines'; import { drag, drop } from './common'; @@ -105,7 +105,7 @@ export const addDescriptionToTimeline = ( modalAlreadyOpen: boolean = false ) => { if (!modalAlreadyOpen) { - cy.get(TIMELINE_SAVE_MODAL_OPEN_BUTTON).first().click(); + cy.get(SAVE_TIMELINE_ACTION_BTN).first().click(); } cy.get(TIMELINE_DESCRIPTION_INPUT).should('not.be.disabled').type(description); cy.get(TIMELINE_DESCRIPTION_INPUT).invoke('val').should('equal', description); @@ -114,7 +114,7 @@ export const addDescriptionToTimeline = ( }; export const addNameToTimelineAndSave = (name: string) => { - cy.get(TIMELINE_SAVE_MODAL_OPEN_BUTTON).first().click(); + cy.get(SAVE_TIMELINE_ACTION_BTN).first().click(); cy.get(TIMELINE_TITLE_INPUT).should('not.be.disabled').clear(); cy.get(TIMELINE_TITLE_INPUT).type(`${name}{enter}`); cy.get(TIMELINE_TITLE_INPUT).should('have.attr', 'value', name); @@ -351,7 +351,7 @@ export const expandFirstTimelineEventDetails = () => { * before you're using this task. Otherwise it will fail to save. */ export const saveTimeline = () => { - cy.get(TIMELINE_SAVE_MODAL_OPEN_BUTTON).first().click(); + cy.get(SAVE_TIMELINE_ACTION_BTN).first().click(); cy.get(TIMELINE_SAVE_MODAL).within(() => { cy.get(TIMELINE_PROGRESS_BAR).should('not.exist'); diff --git a/x-pack/test/security_solution_ftr/page_objects/timeline/index.ts b/x-pack/test/security_solution_ftr/page_objects/timeline/index.ts index b6c41a813b07e..e7f35e31702cc 100644 --- a/x-pack/test/security_solution_ftr/page_objects/timeline/index.ts +++ b/x-pack/test/security_solution_ftr/page_objects/timeline/index.ts @@ -15,11 +15,9 @@ const TIMELINE_MODAL_PAGE_TEST_SUBJ = 'timeline'; const TIMELINE_TAB_QUERY_TEST_SUBJ = 'timeline-tab-content-query'; const TIMELINE_CSS_SELECTOR = Object.freeze({ - /** The Plus icon to add a new timeline located in the bottom timeline sticky bar */ - buttonBarAddButton: `${testSubjSelector( + bottomBarTimelineTitle: `${testSubjSelector( TIMELINE_BOTTOM_BAR_CONTAINER_TEST_SUBJ - )} ${testSubjSelector('settings-plus-in-circle')}`, - + )} ${testSubjSelector('timeline-title')}`, /** The refresh button on the timeline view (top of view, next to the date selector) */ refreshButton: `${testSubjSelector(TIMELINE_TAB_QUERY_TEST_SUBJ)} ${testSubjSelector( 'superDatePickerApplyTimeButton' @@ -45,16 +43,15 @@ export class TimelinePageObject extends FtrService { await this.testSubjects.existOrFail(TIMELINE_BOTTOM_BAR_CONTAINER_TEST_SUBJ); } - async showOpenTimelinePopupFromBottomBar(): Promise { + async openTimelineFromBottomBar() { await this.ensureTimelineAccessible(); await this.testSubjects.findService.clickByCssSelector( - TIMELINE_CSS_SELECTOR.buttonBarAddButton + TIMELINE_CSS_SELECTOR.bottomBarTimelineTitle ); - await this.testSubjects.existOrFail('timeline-addPopupPanel'); } async openTimelineById(id: string): Promise { - await this.showOpenTimelinePopupFromBottomBar(); + await this.openTimelineFromBottomBar(); await this.testSubjects.click('open-timeline-button'); await this.testSubjects.findService.clickByCssSelector( `${testSubjSelector('open-timeline-modal')} ${testSubjSelector(`timeline-title-${id}`)}` From 9b20c0ebcb3b2b8656e31fe801d8c1090d70d407 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Wed, 8 Nov 2023 13:12:30 +0100 Subject: [PATCH 15/43] fix: some tests --- .../components/sourcerer/index.test.tsx | 204 ++---------------- .../sourcerer/sourcerer_integration.test.tsx | 155 +++++++++++++ .../common/components/sourcerer/temporary.tsx | 18 +- .../sourcerer/timeline_sourcerer.test.tsx | 184 ++++++++++++++++ .../common/components/sourcerer/utils.tsx | 95 ++++---- .../authentications_user_table.test.tsx.snap | 2 +- .../flyout/__snapshots__/index.test.tsx.snap | 154 +++++++------ .../header/__snapshots__/index.test.tsx.snap | 14 +- 8 files changed, 510 insertions(+), 316 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/sourcerer_integration.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index 7862057e0a5db..cb4e0331e444b 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -356,6 +356,9 @@ describe('Sourcerer component', () => { ); + + wrapper.update(); + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click'); expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({ @@ -753,179 +756,6 @@ describe('sourcerer on alerts page or rules details page', () => { }); }); -describe('timeline sourcerer', () => { - let wrapper: ReactWrapper; - const { storage } = createSecuritySolutionStorageMock(); - store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const testProps = { - scope: sourcererModel.SourcererScopeName.timeline, - }; - - beforeAll(() => { - (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); - wrapper = mount( - - - - ); - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); - wrapper - .find( - `[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-advanced-options-toggle"]` - ) - .first() - .simulate('click'); - }); - - it('renders "alerts only" checkbox, unchecked', () => { - wrapper - .find( - `[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-alert-only-checkbox"]` - ) - .first() - .simulate('click'); - expect(wrapper.find(`[data-test-subj="sourcerer-alert-only-checkbox"]`).first().text()).toEqual( - 'Show only detection alerts' - ); - expect( - wrapper.find(`[data-test-subj="sourcerer-alert-only-checkbox"] input`).first().prop('checked') - ).toEqual(false); - }); - - it('data view selector is enabled', () => { - expect( - wrapper - .find(`[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-select"]`) - .first() - .prop('disabled') - ).toBeFalsy(); - }); - - it('data view selector is default to Security Default Data View', () => { - expect( - wrapper - .find(`[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-select"]`) - .first() - .prop('valueOfSelected') - ).toEqual('security-solution'); - }); - - it('index pattern selector is enabled', () => { - expect( - wrapper - .find( - `[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-combo-box"]` - ) - .first() - .prop('disabled') - ).toBeFalsy(); - }); - - it('render reset button', () => { - expect(wrapper.find(`[data-test-subj="sourcerer-reset"]`).exists()).toBeTruthy(); - }); - - it('render save button', () => { - expect(wrapper.find(`[data-test-subj="sourcerer-save"]`).exists()).toBeTruthy(); - }); - - it('Checks box when only alerts index is selected in timeline', () => { - const state2 = { - ...mockGlobalState, - sourcerer: { - ...mockGlobalState.sourcerer, - sourcererScopes: { - ...mockGlobalState.sourcerer.sourcererScopes, - [SourcererScopeName.timeline]: { - ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - loading: false, - selectedDataViewId: id, - selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`], - }, - }, - }, - }; - - store = createStore( - state2, - SUB_PLUGINS_REDUCER, - - kibanaObservable, - storage - ); - - wrapper = mount( - - - - ); - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); - expect( - wrapper.find(`[data-test-subj="sourcerer-alert-only-checkbox"] input`).first().prop('checked') - ).toEqual(true); - }); -}); - -describe('Sourcerer integration tests', () => { - const state = { - ...mockGlobalState, - sourcerer: { - ...mockGlobalState.sourcerer, - kibanaDataViews: [ - mockGlobalState.sourcerer.defaultDataView, - { - ...mockGlobalState.sourcerer.defaultDataView, - id: '1234', - title: 'fakebeat-*,neatbeat-*', - patternList: ['fakebeat-*'], - }, - ], - sourcererScopes: { - ...mockGlobalState.sourcerer.sourcererScopes, - [SourcererScopeName.default]: { - ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], - loading: false, - selectedDataViewId: id, - selectedPatterns: patternListNoSignals.slice(0, 2), - }, - }, - }, - }; - - const { storage } = createSecuritySolutionStorageMock(); - - beforeEach(() => { - (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); - store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - jest.clearAllMocks(); - }); - - it('Selects a different index pattern', async () => { - const wrapper = mount( - - - - ); - wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - wrapper.find(`button[data-test-subj="sourcerer-select"]`).first().simulate('click'); - - wrapper.find(`[data-test-subj="dataView-option-super"]`).first().simulate('click'); - expect(checkOptionsAndSelections(wrapper, ['fakebeat-*'])).toEqual({ - availableOptionCount: 0, - optionsSelected: true, - }); - wrapper.find(`button[data-test-subj="sourcerer-save"]`).first().simulate('click'); - - expect(mockDispatch).toHaveBeenCalledWith( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.default, - selectedDataViewId: '1234', - selectedPatterns: ['fakebeat-*'], - }) - ); - }); -}); - describe('No data', () => { const mockNoIndicesState = { ...mockGlobalState, @@ -1287,20 +1117,18 @@ describe('Missing index patterns', () => { fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); - await waitFor(() => { - expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( - 'This timeline is out of date with the Security Data View' - ); - expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( - 'The active index patterns in this timeline are: myFakebeat-*' - ); - expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( - 'security data view is missing the following index patterns: myfakebeat-*' - ); - expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( - "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." - ); - }); + expect(screen.getByTestId('sourcerer-deprecated-callout').textContent).toBe( + 'This timeline is out of date with the Security Data View' + ); + expect(screen.getByTestId('sourcerer-current-patterns-message').textContent).toBe( + 'The active index patterns in this timeline are: myFakebeat-*' + ); + expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( + 'Security Data View is missing the following index patterns: myFakebeat-*' + ); + expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( + "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." + ); }); test('Show UpdateDefaultDataViewModal CallOut for timeline template', async () => { @@ -1310,7 +1138,7 @@ describe('Missing index patterns', () => { }); store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - wrapper = mount( + render( diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/sourcerer_integration.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/sourcerer_integration.test.tsx new file mode 100644 index 0000000000000..7332afe5380f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/sourcerer_integration.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright 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 type { ReactWrapper } from 'enzyme'; +import { mount } from 'enzyme'; + +import { SourcererScopeName } from '../../store/sourcerer/model'; +import { Sourcerer } from '.'; +import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; +import { sourcererActions, sourcererModel } from '../../store/sourcerer'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../mock'; +import { createStore } from '../../store'; +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer'; +import { useSourcererDataView } from '../../containers/sourcerer'; + +const mockDispatch = jest.fn(); + +jest.mock('../../containers/sourcerer'); +jest.mock('../../containers/sourcerer/use_signal_helpers'); +const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); +jest.mock('./use_update_data_view', () => ({ + useUpdateDataView: () => mockUseUpdateDataView, +})); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + + return { + ...original, + toMountPoint: jest.fn(), + }; +}); + +const mockUpdateUrlParam = jest.fn(); +jest.mock('../../utils/global_query_string', () => { + const original = jest.requireActual('../../utils/global_query_string'); + + return { + ...original, + useUpdateUrlParam: () => mockUpdateUrlParam, + }; +}); + +const mockOptions = DEFAULT_INDEX_PATTERN.map((index) => ({ label: index, value: index })); + +const defaultProps = { + scope: sourcererModel.SourcererScopeName.default, +}; + +const checkOptionsAndSelections = (wrapper: ReactWrapper, patterns: string[]) => ({ + availableOptionCount: + wrapper.find('List').length > 0 ? wrapper.find('List').prop('itemCount') : 0, + optionsSelected: patterns.every((pattern) => + wrapper.find(`[data-test-subj="sourcerer-combo-box"] span[title="${pattern}"]`).first().exists() + ), +}); + +const { id, patternList, title } = mockGlobalState.sourcerer.defaultDataView; +const patternListNoSignals = sortWithExcludesAtEnd( + patternList.filter((p) => p !== mockGlobalState.sourcerer.signalIndexName) +); +let store: ReturnType; +const sourcererDataView = { + indicesExist: true, + loading: false, +}; + +describe('Sourcerer integration tests', () => { + const state = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + mockGlobalState.sourcerer.defaultDataView, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '1234', + title: 'fakebeat-*,neatbeat-*', + patternList: ['fakebeat-*'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + loading: false, + selectedDataViewId: id, + selectedPatterns: patternListNoSignals.slice(0, 2), + }, + }, + }, + }; + + const { storage } = createSecuritySolutionStorageMock(); + + beforeEach(() => { + const pollForSignalIndexMock = jest.fn(); + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + activePatterns: ['myFakebeat-*'], + }); + store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + jest.clearAllMocks(); + }); + + it('Selects a different index pattern', async () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + wrapper.find(`button[data-test-subj="sourcerer-select"]`).first().simulate('click'); + + wrapper.find(`[data-test-subj="dataView-option-super"]`).first().simulate('click'); + expect(checkOptionsAndSelections(wrapper, ['fakebeat-*'])).toEqual({ + availableOptionCount: 0, + optionsSelected: true, + }); + wrapper.find(`button[data-test-subj="sourcerer-save"]`).first().simulate('click'); + + expect(mockDispatch).toHaveBeenCalledWith( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.default, + selectedDataViewId: '1234', + selectedPatterns: ['fakebeat-*'], + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx index 7653a0830b70e..1c2c73abcc042 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx @@ -107,6 +107,7 @@ export const TemporarySourcererComp = React.memo( const timelineType = useDeepEqualSelector( (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).timelineType ); + return ( <> ( )} {isModified === 'missingPatterns' && ( <> - {missingPatterns.join(', ')}, - }} - /> + + {missingPatterns.join(', ')}, + }} + /> + )} diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx new file mode 100644 index 0000000000000..3100e9a43de2e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx @@ -0,0 +1,184 @@ +/* + * Copyright 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 { SourcererScopeName } from '../../store/sourcerer/model'; +import { Sourcerer } from '.'; +import { sourcererModel } from '../../store/sourcerer'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../mock'; +import { createStore } from '../../store'; +import { fireEvent, screen, waitFor } from '@testing-library/dom'; +import { useSourcererDataView } from '../../containers/sourcerer'; +import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; +import { render, cleanup } from '@testing-library/react'; + +const mockDispatch = jest.fn(); + +jest.mock('../../containers/sourcerer'); +jest.mock('../../containers/sourcerer/use_signal_helpers'); +const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); +jest.mock('./use_update_data_view', () => ({ + useUpdateDataView: () => mockUseUpdateDataView, +})); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + + return { + ...original, + toMountPoint: jest.fn(), + }; +}); + +const mockUpdateUrlParam = jest.fn(); +jest.mock('../../utils/global_query_string', () => { + const original = jest.requireActual('../../utils/global_query_string'); + + return { + ...original, + useUpdateUrlParam: () => mockUpdateUrlParam, + }; +}); + +const { id } = mockGlobalState.sourcerer.defaultDataView; + +let store: ReturnType; +const sourcererDataView = { + indicesExist: true, + loading: false, +}; + +describe('timeline sourcerer', () => { + const { storage } = createSecuritySolutionStorageMock(); + store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const testProps = { + scope: sourcererModel.SourcererScopeName.timeline, + }; + + beforeEach(() => { + const pollForSignalIndexMock = jest.fn(); + (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + render( + + + + ); + + fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); + + fireEvent.click(screen.getByTestId(`sourcerer-advanced-options-toggle`)); + }); + + afterEach(() => { + cleanup(); + }); + + it('renders "alerts only" checkbox, unchecked', async () => { + await waitFor(() => { + expect(screen.getByTestId('sourcerer-alert-only-checkbox').parentElement).toHaveTextContent( + 'Show only detection alerts' + ); + expect(screen.getByTestId('sourcerer-alert-only-checkbox')).not.toBeChecked(); + }); + + fireEvent.click(screen.getByTestId('sourcerer-alert-only-checkbox')); + + await waitFor(() => { + expect(screen.getByTestId('sourcerer-alert-only-checkbox')).toBeChecked(); + }); + }); + + it('data view selector is enabled', async () => { + await waitFor(() => { + expect(screen.getByTestId('sourcerer-select')).toBeEnabled(); + }); + }); + + it('data view selector is default to Security Default Data View', async () => { + await waitFor(() => { + expect(screen.getByTestId('security-option-super')).toHaveTextContent( + 'Security Default Data View' + ); + }); + }); + + it('index pattern selector is enabled', async () => { + await waitFor(() => { + expect(screen.getByTestId('sourcerer-combo-box')).not.toBeDisabled(); + }); + }); + + it('render reset button', async () => { + await waitFor(() => { + expect(screen.getByTestId('sourcerer-reset')).toBeVisible(); + }); + }); + + it('render save button', async () => { + await waitFor(() => { + expect(screen.getByTestId('sourcerer-save')).toBeVisible(); + }); + }); + + it('Checks box when only alerts index is selected in timeline', async () => { + cleanup(); + const state2 = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + loading: false, + selectedDataViewId: id, + selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`], + }, + }, + }, + }; + + store = createStore( + state2, + SUB_PLUGINS_REDUCER, + + kibanaObservable, + storage + ); + + render( + + + + ); + + fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); + + await waitFor(() => { + expect(screen.getByTestId('sourcerer-alert-only-checkbox')).toBeChecked(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/utils.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/utils.tsx index 00c6219598729..27539f9664d5f 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/utils.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/utils.tsx @@ -43,28 +43,30 @@ export const CurrentPatternsMessage = ({ if (timelineType === TimelineType.template) { return ( + + {activePatterns.join(', ')}, + }} + /> + + ); + } + + return ( + {activePatterns.join(', ')}, }} /> - ); - } - - return ( - {activePatterns.join(', ')}, - }} - /> + ); }; @@ -147,25 +149,28 @@ export const DeprecatedMessage = ({ }) => { if (timelineType === TimelineType.template) { return ( + + {i18n.TOGGLE_TO_NEW_SOURCERER}, + }} + /> + + ); + } + return ( + {i18n.TOGGLE_TO_NEW_SOURCERER}, }} /> - ); - } - return ( - {i18n.TOGGLE_TO_NEW_SOURCERER}, - }} - /> + ); }; @@ -178,24 +183,26 @@ export const MissingPatternsMessage = ({ }) => { if (timelineType === TimelineType.template) { return ( + + {i18n.TOGGLE_TO_NEW_SOURCERER}, + }} + /> + + ); + } + return ( + {i18n.TOGGLE_TO_NEW_SOURCERER}, }} /> - ); - } - return ( - {i18n.TOGGLE_TO_NEW_SOURCERER}, - }} - /> + ); }; diff --git a/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_user_table.test.tsx.snap b/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_user_table.test.tsx.snap index e991234f0e33b..7b64962e9556e 100644 --- a/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_user_table.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_user_table.test.tsx.snap @@ -105,7 +105,7 @@ exports[`Authentication User Table Component rendering it renders the user authe class="euiFlexItem emotion-euiFlexItem-grow-1" >

- -
-
-
-
- + + +
+
+
+
+ + Unsaved + +
+
+
+ +
+

diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap index c57698cef7582..d322d35e86740 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -37,9 +37,8 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` "_destroy": [Function], "_events": Object { "end": [Function], - "error": [Function], }, - "_eventsCount": 2, + "_eventsCount": 1, "_hadError": false, "_host": null, "_isStdio": true, @@ -118,7 +117,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` "needDrain": false, "objectMode": false, "onwrite": [Function], - "pendingcb": 7, + "pendingcb": 11, "prefinished": false, "sync": false, "writecb": null, @@ -127,7 +126,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` Symbol(kOnFinished): Array [], }, "allowHalfOpen": false, - "columns": 106, + "columns": 283, "connecting": false, "destroySoon": [Function], "fd": 1, @@ -155,9 +154,8 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` "_destroy": [Function], "_events": Object { "end": [Function], - "error": [Function], }, - "_eventsCount": 2, + "_eventsCount": 1, "_hadError": false, "_host": null, "_isStdio": true, @@ -236,7 +234,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` "needDrain": false, "objectMode": false, "onwrite": [Function], - "pendingcb": 7, + "pendingcb": 11, "prefinished": false, "sync": false, "writecb": null, @@ -245,7 +243,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` Symbol(kOnFinished): Array [], }, "allowHalfOpen": false, - "columns": 106, + "columns": 283, "connecting": false, "destroySoon": [Function], "fd": 1, From 5266dcef85e7dfe54bfbdedf46574ad3e4d5b347 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Wed, 8 Nov 2023 13:19:42 +0100 Subject: [PATCH 16/43] fix: types --- .../components/sourcerer/sourcerer_integration.test.tsx | 5 +---- .../timelines/components/flyout/header/active_timelines.tsx | 2 -- .../public/timelines/components/flyout/header/index.tsx | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/sourcerer_integration.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/sourcerer_integration.test.tsx index 7332afe5380f3..33eba9dac6b95 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/sourcerer_integration.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/sourcerer_integration.test.tsx @@ -21,7 +21,6 @@ import { TestProviders, } from '../../mock'; import { createStore } from '../../store'; -import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer'; import { useSourcererDataView } from '../../containers/sourcerer'; @@ -61,8 +60,6 @@ jest.mock('../../utils/global_query_string', () => { }; }); -const mockOptions = DEFAULT_INDEX_PATTERN.map((index) => ({ label: index, value: index })); - const defaultProps = { scope: sourcererModel.SourcererScopeName.default, }; @@ -75,7 +72,7 @@ const checkOptionsAndSelections = (wrapper: ReactWrapper, patterns: string[]) => ), }); -const { id, patternList, title } = mockGlobalState.sourcerer.defaultDataView; +const { id, patternList } = mockGlobalState.sourcerer.defaultDataView; const patternListNoSignals = sortWithExcludesAtEnd( patternList.filter((p) => p !== mockGlobalState.sourcerer.signalIndexName) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx index 52d255b5ad7a0..ba7a06f7f1250 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx @@ -70,8 +70,6 @@ const ActiveTimelinesComponent: React.FC = ({ ? UNTITLED_TEMPLATE : UNTITLED_TIMELINE; - const timelineChangeStatus = useMemo(() => {}, []); - const tooltipContent = useMemo(() => { if (timelineStatus === TimelineStatus.draft) { return <>{i18n.UNSAVED}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index 837326cdaa66f..93de396fd16b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -59,7 +59,7 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline show, filters, kqlMode, - changed, + changed = false, } = useDeepEqualSelector((state) => pick( [ From 580b0f0bc420c2af0fdadfbbe5345036d5dbab9b Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Thu, 9 Nov 2023 16:29:52 +0100 Subject: [PATCH 17/43] fix: jest tests --- .../sourcerer/alerts_sourcerer.test.tsx | 139 +++++ .../components/sourcerer/index.test.tsx | 555 ++---------------- .../common/components/sourcerer/misc.test.tsx | 538 +++++++++++++++++ .../sourcerer/timeline_sourcerer.test.tsx | 14 +- .../components/alerts_table/actions.test.tsx | 1 - .../timelines/store/timeline/defaults.ts | 1 - .../cypress/screens/timeline.ts | 2 +- 7 files changed, 730 insertions(+), 520 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/alerts_sourcerer.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/misc.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/alerts_sourcerer.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/alerts_sourcerer.test.tsx new file mode 100644 index 0000000000000..da0bc5699882d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/alerts_sourcerer.test.tsx @@ -0,0 +1,139 @@ +/* + * Copyright 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 { Sourcerer } from '.'; +import { sourcererModel } from '../../store/sourcerer'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../mock'; +import { createStore } from '../../store'; +import { useSourcererDataView } from '../../containers/sourcerer'; +import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; + +const mockDispatch = jest.fn(); + +jest.mock('../../containers/sourcerer'); +jest.mock('../../containers/sourcerer/use_signal_helpers'); +const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); +jest.mock('./use_update_data_view', () => ({ + useUpdateDataView: () => mockUseUpdateDataView, +})); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + + return { + ...original, + toMountPoint: jest.fn(), + }; +}); + +const mockUpdateUrlParam = jest.fn(); +jest.mock('../../utils/global_query_string', () => { + const original = jest.requireActual('../../utils/global_query_string'); + + return { + ...original, + useUpdateUrlParam: () => mockUpdateUrlParam, + }; +}); + +let store: ReturnType; +const sourcererDataView = { + indicesExist: true, + loading: false, +}; +describe('sourcerer on alerts page or rules details page', () => { + const { storage } = createSecuritySolutionStorageMock(); + store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const testProps = { + scope: sourcererModel.SourcererScopeName.detections, + }; + + const pollForSignalIndexMock = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + indicesExist: true, + }); + + render( + + + + ); + + fireEvent.click(screen.getByTestId('sourcerer-trigger')); + await waitFor(() => { + expect(screen.getByTestId('sourcerer-advanced-options-toggle')).toBeVisible(); + }); + fireEvent.click(screen.getByTestId('sourcerer-advanced-options-toggle')); + }); + + it('renders an alerts badge in sourcerer button', () => { + expect(screen.getByTestId('sourcerer-advanced-options-toggle')).toHaveTextContent( + /Advanced options/ + ); + }); + + it('renders a callout', () => { + expect(screen.getByTestId('sourcerer-callout')).toHaveTextContent( + 'Data view cannot be modified on this page' + ); + }); + + it('disable data view selector', () => { + expect(screen.getByTestId('sourcerer-select')).toBeDisabled(); + }); + + it('data view selector is default to Security Data View', () => { + expect(screen.getByTestId('sourcerer-select')).toHaveTextContent(/security data view/i); + }); + + it('renders an alert badge in data view selector', () => { + expect(screen.getByTestId('security-alerts-option-badge')).toHaveTextContent('Alerts'); + }); + + it('disable index pattern selector', () => { + expect(screen.getByTestId('sourcerer-combo-box')).toHaveAttribute('disabled'); + }); + + it('shows signal index as index pattern option', () => { + expect(screen.getByTestId('euiComboBoxPill')).toHaveTextContent('.siem-signals-spacename'); + }); + + it('does not render reset button', () => { + expect(screen.queryByTestId('sourcerer-reset')).toBeFalsy(); + }); + + it('does not render save button', () => { + expect(screen.queryByTestId('sourcerer-save')).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index cb4e0331e444b..c921e4f346841 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -8,9 +8,8 @@ import React from 'react'; import type { ReactWrapper } from 'enzyme'; import { mount } from 'enzyme'; -import { cloneDeep } from 'lodash'; -import { initialSourcererState, SourcererScopeName } from '../../store/sourcerer/model'; +import { SourcererScopeName } from '../../store/sourcerer/model'; import { Sourcerer } from '.'; import { sourcererActions, sourcererModel } from '../../store/sourcerer'; import { @@ -22,11 +21,9 @@ import { } from '../../mock'; import { createStore } from '../../store'; import type { EuiSuperSelectOption } from '@elastic/eui/src/components/form/super_select/super_select_control'; -import { fireEvent, screen, waitFor } from '@testing-library/dom'; +import { fireEvent, waitFor } from '@testing-library/dom'; import { useSourcererDataView } from '../../containers/sourcerer'; import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; -import { TimelineId } from '../../../../common/types/timeline'; -import { TimelineType } from '../../../../common/api/timeline'; import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer'; import { render } from '@testing-library/react'; @@ -94,6 +91,7 @@ const sourcererDataView = { describe('Sourcerer component', () => { const { storage } = createSecuritySolutionStorageMock(); const pollForSignalIndexMock = jest.fn(); + let wrapper: ReactWrapper; beforeEach(() => { jest.clearAllMocks(); store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -101,8 +99,12 @@ describe('Sourcerer component', () => { (useSignalHelpers as jest.Mock).mockReturnValue({ signalIndexNeedsInit: false }); }); + afterEach(() => { + if (wrapper && wrapper.exists()) wrapper.unmount(); + }); + it('renders data view title', () => { - const wrapper = mount( + wrapper = mount( @@ -118,7 +120,7 @@ describe('Sourcerer component', () => { ...defaultProps, showAlertsOnlyCheckbox: true, }; - const wrapper = mount( + wrapper = mount( @@ -130,7 +132,7 @@ describe('Sourcerer component', () => { }); it('renders tooltip', () => { - const wrapper = mount( + wrapper = mount( @@ -141,7 +143,7 @@ describe('Sourcerer component', () => { }); it('renders popover button inside tooltip', () => { - const wrapper = mount( + wrapper = mount( @@ -157,7 +159,7 @@ describe('Sourcerer component', () => { // Using props callback instead of simulating clicks, // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects it('Mounts with all options selected', () => { - const wrapper = mount( + wrapper = mount( @@ -207,7 +209,7 @@ describe('Sourcerer component', () => { kibanaObservable, storage ); - const wrapper = mount( + wrapper = mount( @@ -257,7 +259,7 @@ describe('Sourcerer component', () => { kibanaObservable, storage ); - const wrapper = mount( + wrapper = mount( @@ -306,7 +308,7 @@ describe('Sourcerer component', () => { }; store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const wrapper = mount( + wrapper = mount( @@ -319,7 +321,7 @@ describe('Sourcerer component', () => { optionsSelected: true, }); }); - it('Mounts with multiple options selected - timeline', () => { + it('Mounts with multiple options selected - timeline', async () => { const state2 = { ...mockGlobalState, sourcerer: { @@ -351,20 +353,22 @@ describe('Sourcerer component', () => { }; store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const wrapper = mount( + const { getByTestId, queryByTitle, queryAllByTestId } = render( ); - wrapper.update(); + fireEvent.click(getByTestId('timeline-sourcerer-trigger')); + await waitFor(() => { + for (const pattern of patternList.slice(0, 2)) { + expect(queryByTitle(pattern)).toBeInTheDocument(); + } + }); - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click'); - expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({ - // should show every option except fakebeat-* - availableOptionCount: title.split(',').length - 2, - optionsSelected: true, + fireEvent.click(getByTestId('comboBoxInput')); + await waitFor(() => { + expect(queryAllByTestId('sourcerer-combo-option')).toHaveLength(title.split(',').length - 2); }); }); it('onSave dispatches setSelectedDataView', async () => { @@ -396,7 +400,7 @@ describe('Sourcerer component', () => { kibanaObservable, storage ); - const wrapper = mount( + wrapper = mount( @@ -454,7 +458,7 @@ describe('Sourcerer component', () => { storage ); - const wrapper = mount( + wrapper = mount( @@ -468,7 +472,7 @@ describe('Sourcerer component', () => { }); it('resets to default index pattern', async () => { - const wrapper = mount( + wrapper = mount( @@ -521,7 +525,7 @@ describe('Sourcerer component', () => { kibanaObservable, storage ); - const wrapper = mount( + wrapper = mount( @@ -531,6 +535,13 @@ describe('Sourcerer component', () => { expect(wrapper.find('[data-test-subj="sourcerer-save"]').first().prop('disabled')).toBeTruthy(); }); it('Does display signals index on timeline sourcerer', async () => { + /* + * Since both enzyme and RTL share JSDOM when running these tests, + * and enzyme does not clears jsdom after each test, because of this + * `screen` of RTL does not work as expect, please avoid using screen + * till all the tests have been converted to RTL + * + * */ const state2 = { ...mockGlobalState, sourcerer: { @@ -563,18 +574,17 @@ describe('Sourcerer component', () => { }; store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const wrapper = render( + const el = render( ); - fireEvent.click(wrapper.getByTestId('timeline-sourcerer-trigger')); - fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton')); + fireEvent.click(el.getByTestId('timeline-sourcerer-trigger')); + fireEvent.click(el.getByTestId('comboBoxToggleListButton')); - screen.debug(undefined, 10000000); await waitFor(() => { - expect(screen.queryAllByTestId('sourcerer-combo-option')[0].textContent).toBe( + expect(el.queryAllByTestId('sourcerer-combo-option')[0].textContent).toBe( mockGlobalState.sourcerer.signalIndexName ); }); @@ -612,7 +622,7 @@ describe('Sourcerer component', () => { }; store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const wrapper = mount( + wrapper = mount( @@ -686,484 +696,3 @@ describe('Sourcerer component', () => { expect(pollForSignalIndexMock).toHaveBeenCalledTimes(1); }); }); - -describe('sourcerer on alerts page or rules details page', () => { - let wrapper: ReactWrapper; - const { storage } = createSecuritySolutionStorageMock(); - store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const testProps = { - scope: sourcererModel.SourcererScopeName.detections, - }; - - beforeAll(() => { - wrapper = mount( - - - - ); - wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="sourcerer-advanced-options-toggle"]`).first().simulate('click'); - }); - - it('renders an alerts badge in sourcerer button', () => { - expect(wrapper.find(`[data-test-subj="sourcerer-alerts-badge"]`).first().text()).toEqual( - 'Alerts' - ); - }); - - it('renders a callout', () => { - expect(wrapper.find(`[data-test-subj="sourcerer-callout"]`).first().text()).toEqual( - 'Data view cannot be modified on this page' - ); - }); - - it('disable data view selector', () => { - expect( - wrapper.find(`[data-test-subj="sourcerer-select"]`).first().prop('disabled') - ).toBeTruthy(); - }); - - it('data view selector is default to Security Data View', () => { - expect( - wrapper.find(`[data-test-subj="sourcerer-select"]`).first().prop('valueOfSelected') - ).toEqual('security-solution'); - }); - - it('renders an alert badge in data view selector', () => { - expect(wrapper.find(`[data-test-subj="security-alerts-option-badge"]`).first().text()).toEqual( - 'Alerts' - ); - }); - - it('disable index pattern selector', () => { - expect( - wrapper.find(`[data-test-subj="sourcerer-combo-box"]`).first().prop('disabled') - ).toBeTruthy(); - }); - - it('shows signal index as index pattern option', () => { - expect(wrapper.find(`[data-test-subj="sourcerer-combo-box"]`).first().prop('options')).toEqual([ - { disabled: false, label: '.siem-signals-spacename', value: '.siem-signals-spacename' }, - ]); - }); - - it('does not render reset button', () => { - expect(wrapper.find(`[data-test-subj="sourcerer-reset"]`).exists()).toBeFalsy(); - }); - - it('does not render save button', () => { - expect(wrapper.find(`[data-test-subj="sourcerer-save"]`).exists()).toBeFalsy(); - }); -}); - -describe('No data', () => { - const mockNoIndicesState = { - ...mockGlobalState, - sourcerer: { - ...initialSourcererState, - }, - }; - - const { storage } = createSecuritySolutionStorageMock(); - - beforeEach(() => { - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - indicesExist: false, - }); - store = createStore(mockNoIndicesState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - jest.clearAllMocks(); - }); - - test('Hide sourcerer - default ', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); - }); - test('Hide sourcerer - detections ', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); - }); - test('Hide sourcerer - timeline ', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).exists()).toEqual(true); - }); -}); - -describe('Update available', () => { - const { storage } = createSecuritySolutionStorageMock(); - const state2 = { - ...mockGlobalState, - sourcerer: { - ...mockGlobalState.sourcerer, - kibanaDataViews: [ - mockGlobalState.sourcerer.defaultDataView, - { - ...mockGlobalState.sourcerer.defaultDataView, - id: '1234', - title: 'auditbeat-*', - patternList: ['auditbeat-*'], - }, - { - ...mockGlobalState.sourcerer.defaultDataView, - id: '12347', - title: 'packetbeat-*', - patternList: ['packetbeat-*'], - }, - ], - sourcererScopes: { - ...mockGlobalState.sourcerer.sourcererScopes, - [SourcererScopeName.timeline]: { - ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - loading: false, - patternList, - selectedDataViewId: null, - selectedPatterns: ['myFakebeat-*'], - missingPatterns: ['myFakebeat-*'], - }, - }, - }, - }; - - beforeEach(() => { - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - activePatterns: ['myFakebeat-*'], - }); - store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - render( - - - - ); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - - test('Show Update available label', () => { - expect(screen.getByTestId('sourcerer-deprecated-badge')).toBeInTheDocument(); - }); - - test('Show correct tooltip', () => { - expect(screen.getByTestId('sourcerer-tooltip').textContent).toBe('myFakebeat-*'); - }); - - test('Show UpdateDefaultDataViewModal', () => { - fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - - expect(screen.getByTestId('sourcerer-update-data-view-modal')).toBeVisible(); - - // expect(wrapper.find(`UpdateDefaultDataViewModal`).prop('isShowing')).toEqual(true); - }); - - test('Show UpdateDefaultDataViewModal Callout', () => { - fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - - expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( - 'This timeline uses a legacy data view selector' - ); - - expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( - 'The active index patterns in this timeline are: myFakebeat-*' - ); - - expect(screen.queryAllByTestId('sourcerer-deprecated-message')[0].textContent).toBe( - "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view here." - ); - - // wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); - // - // wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); - // - // expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-callout"]`).first().text()).toEqual( - // 'This timeline uses a legacy data view selector' - // ); - // - // expect( - // wrapper.find(`[data-test-subj="sourcerer-current-patterns-message"]`).first().text() - // ).toEqual('The active index patterns in this timeline are: myFakebeat-*'); - // - // expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-message"]`).first().text()).toEqual( - // "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view here." - // ); - }); - - test('Show Add index pattern in UpdateDefaultDataViewModal', () => { - fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - - expect(screen.queryAllByTestId('sourcerer-update-data-view')[0].textContent).toBe( - 'Add index pattern' - ); - - // wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); - // - // wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); - // - // expect(wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).text()).toEqual( - // 'Add index pattern' - // ); - }); - - test('Set all the index patterns from legacy timeline to sourcerer, after clicking on "Add index pattern"', async () => { - fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-update-data-view')[0]); - - await waitFor(() => { - expect(mockDispatch).toHaveBeenCalledWith( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: 'security-solution', - selectedPatterns: ['myFakebeat-*'], - shouldValidateSelectedPatterns: false, - }) - ); - }); - - // wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); - // - // wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); - // - // wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).simulate('click'); - // - // await waitFor(() => wrapper.update()); - // expect(mockDispatch).toHaveBeenCalledWith( - // sourcererActions.setSelectedDataView({ - // id: SourcererScopeName.timeline, - // selectedDataViewId: 'security-solution', - // selectedPatterns: ['myFakebeat-*'], - // shouldValidateSelectedPatterns: false, - // }) - // ); - }); -}); - -describe('Update available for timeline template', () => { - const { storage } = createSecuritySolutionStorageMock(); - const state2 = { - ...mockGlobalState, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - ...mockGlobalState.timeline.timelineById, - [TimelineId.active]: { - ...mockGlobalState.timeline.timelineById.test, - timelineType: TimelineType.template, - }, - }, - }, - sourcerer: { - ...mockGlobalState.sourcerer, - kibanaDataViews: [ - mockGlobalState.sourcerer.defaultDataView, - { - ...mockGlobalState.sourcerer.defaultDataView, - id: '1234', - title: 'auditbeat-*', - patternList: ['auditbeat-*'], - }, - { - ...mockGlobalState.sourcerer.defaultDataView, - id: '12347', - title: 'packetbeat-*', - patternList: ['packetbeat-*'], - }, - ], - sourcererScopes: { - ...mockGlobalState.sourcerer.sourcererScopes, - [SourcererScopeName.timeline]: { - ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - loading: false, - patternList, - selectedDataViewId: null, - selectedPatterns: ['myFakebeat-*'], - missingPatterns: ['myFakebeat-*'], - }, - }, - }, - }; - - let wrapper: ReactWrapper; - - beforeEach(() => { - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - activePatterns: ['myFakebeat-*'], - }); - store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - wrapper = mount( - - - - ); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - - test('Show UpdateDefaultDataViewModal CallOut', () => { - wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); - - wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); - - expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-callout"]`).first().text()).toEqual( - 'This timeline template uses a legacy data view selector' - ); - - expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-message"]`).first().text()).toEqual( - "We have preserved your timeline template by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view here." - ); - }); -}); - -describe('Missing index patterns', () => { - const { storage } = createSecuritySolutionStorageMock(); - const state2 = { - ...mockGlobalState, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - ...mockGlobalState.timeline.timelineById, - [TimelineId.active]: { - ...mockGlobalState.timeline.timelineById.test, - timelineType: TimelineType.template, - }, - }, - }, - sourcerer: { - ...mockGlobalState.sourcerer, - kibanaDataViews: [ - mockGlobalState.sourcerer.defaultDataView, - { - ...mockGlobalState.sourcerer.defaultDataView, - id: '1234', - title: 'auditbeat-*', - patternList: ['auditbeat-*'], - }, - { - ...mockGlobalState.sourcerer.defaultDataView, - id: '12347', - title: 'packetbeat-*', - patternList: ['packetbeat-*'], - }, - ], - sourcererScopes: { - ...mockGlobalState.sourcerer.sourcererScopes, - [SourcererScopeName.timeline]: { - ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - loading: false, - patternList, - selectedDataViewId: 'fake-data-view-id', - selectedPatterns: ['myFakebeat-*'], - missingPatterns: ['myFakebeat-*'], - }, - }, - }, - }; - - beforeEach(() => { - const pollForSignalIndexMock = jest.fn(); - (useSignalHelpers as jest.Mock).mockReturnValue({ - pollForSignalIndex: pollForSignalIndexMock, - signalIndexNeedsInit: false, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('Show UpdateDefaultDataViewModal CallOut for timeline', async () => { - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - activePatterns: ['myFakebeat-*'], - }); - const state3 = cloneDeep(state2); - state3.timeline.timelineById[TimelineId.active].timelineType = TimelineType.default; - store = createStore(state3, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - render( - - - - ); - - fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); - - fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); - - expect(screen.getByTestId('sourcerer-deprecated-callout').textContent).toBe( - 'This timeline is out of date with the Security Data View' - ); - expect(screen.getByTestId('sourcerer-current-patterns-message').textContent).toBe( - 'The active index patterns in this timeline are: myFakebeat-*' - ); - expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( - 'Security Data View is missing the following index patterns: myFakebeat-*' - ); - expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( - "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." - ); - }); - - test('Show UpdateDefaultDataViewModal CallOut for timeline template', async () => { - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - activePatterns: ['myFakebeat-*'], - }); - store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - render( - - - - ); - - fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); - - fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); - - await waitFor(() => { - expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( - 'This timeline template is out of date with the Security Data View' - ); - - expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( - 'The active index patterns in this timeline template are: myFakebeat-*' - ); - - expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( - 'Security Data View is missing the following index patterns: myFakebeat-*' - ); - - expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( - "We have preserved your timeline template by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/misc.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/misc.test.tsx new file mode 100644 index 0000000000000..f5d02b7654b0d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/misc.test.tsx @@ -0,0 +1,538 @@ +/* + * Copyright 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 type { ReactWrapper } from 'enzyme'; +import { mount } from 'enzyme'; +import { cloneDeep } from 'lodash'; + +import { initialSourcererState, SourcererScopeName } from '../../store/sourcerer/model'; +import { Sourcerer } from '.'; +import { sourcererActions, sourcererModel } from '../../store/sourcerer'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../mock'; +import { createStore } from '../../store'; +import { fireEvent, screen, waitFor } from '@testing-library/dom'; +import { useSourcererDataView } from '../../containers/sourcerer'; +import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; +import { TimelineId } from '../../../../common/types/timeline'; +import { TimelineType } from '../../../../common/api/timeline'; +import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer'; +import { render } from '@testing-library/react'; + +const mockDispatch = jest.fn(); + +jest.mock('../../containers/sourcerer'); +jest.mock('../../containers/sourcerer/use_signal_helpers'); +const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); +jest.mock('./use_update_data_view', () => ({ + useUpdateDataView: () => mockUseUpdateDataView, +})); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + + return { + ...original, + toMountPoint: jest.fn(), + }; +}); + +const mockUpdateUrlParam = jest.fn(); +jest.mock('../../utils/global_query_string', () => { + const original = jest.requireActual('../../utils/global_query_string'); + + return { + ...original, + useUpdateUrlParam: () => mockUpdateUrlParam, + }; +}); + +const defaultProps = { + scope: sourcererModel.SourcererScopeName.default, +}; + +const checkOptionsAndSelections = (wrapper: ReactWrapper, patterns: string[]) => ({ + availableOptionCount: + wrapper.find('List').length > 0 ? wrapper.find('List').prop('itemCount') : 0, + optionsSelected: patterns.every((pattern) => + wrapper.find(`[data-test-subj="sourcerer-combo-box"] span[title="${pattern}"]`).first().exists() + ), +}); + +const { id, patternList } = mockGlobalState.sourcerer.defaultDataView; + +const patternListNoSignals = sortWithExcludesAtEnd( + patternList.filter((p) => p !== mockGlobalState.sourcerer.signalIndexName) +); +let store: ReturnType; +const sourcererDataView = { + indicesExist: true, + loading: false, +}; + +describe('No data', () => { + const mockNoIndicesState = { + ...mockGlobalState, + sourcerer: { + ...initialSourcererState, + }, + }; + + const { storage } = createSecuritySolutionStorageMock(); + const pollForSignalIndexMock = jest.fn(); + + beforeEach(() => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + indicesExist: false, + }); + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + store = createStore(mockNoIndicesState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + jest.clearAllMocks(); + }); + + test('Hide sourcerer - default ', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); + }); + test('Hide sourcerer - detections ', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); + }); + test('Hide sourcerer - timeline ', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).exists()).toEqual(true); + }); +}); + +describe('Update available', () => { + const { storage } = createSecuritySolutionStorageMock(); + const state2 = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + mockGlobalState.sourcerer.defaultDataView, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '1234', + title: 'auditbeat-*', + patternList: ['auditbeat-*'], + }, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '12347', + title: 'packetbeat-*', + patternList: ['packetbeat-*'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + loading: false, + patternList, + selectedDataViewId: null, + selectedPatterns: ['myFakebeat-*'], + missingPatterns: ['myFakebeat-*'], + }, + }, + }, + }; + + const pollForSignalIndexMock = jest.fn(); + beforeEach(() => { + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + activePatterns: ['myFakebeat-*'], + }); + store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + render( + + + + ); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Show Update available label', () => { + expect(screen.getByTestId('sourcerer-deprecated-badge')).toBeInTheDocument(); + }); + + test('Show correct tooltip', async () => { + fireEvent.mouseOver(screen.getByTestId('timeline-sourcerer-trigger')); + await waitFor(() => { + expect(screen.getByTestId('sourcerer-tooltip').textContent).toBe('myFakebeat-*'); + }); + }); + + test('Show UpdateDefaultDataViewModal', () => { + fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); + + fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); + + expect(screen.getByTestId('sourcerer-update-data-view-modal')).toBeVisible(); + }); + + test('Show UpdateDefaultDataViewModal Callout', () => { + fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); + + fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); + + expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( + 'This timeline uses a legacy data view selector' + ); + + expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( + 'The active index patterns in this timeline are: myFakebeat-*' + ); + + expect(screen.queryAllByTestId('sourcerer-deprecated-message')[0].textContent).toBe( + "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view here." + ); + }); + + test('Show Add index pattern in UpdateDefaultDataViewModal', () => { + fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); + + fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); + + expect(screen.queryAllByTestId('sourcerer-update-data-view')[0].textContent).toBe( + 'Add index pattern' + ); + }); + + test('Set all the index patterns from legacy timeline to sourcerer, after clicking on "Add index pattern"', async () => { + fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); + + fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); + + fireEvent.click(screen.queryAllByTestId('sourcerer-update-data-view')[0]); + + await waitFor(() => { + expect(mockDispatch).toHaveBeenCalledWith( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: 'security-solution', + selectedPatterns: ['myFakebeat-*'], + shouldValidateSelectedPatterns: false, + }) + ); + }); + }); +}); + +describe('Update available for timeline template', () => { + const { storage } = createSecuritySolutionStorageMock(); + const state2 = { + ...mockGlobalState, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + ...mockGlobalState.timeline.timelineById, + [TimelineId.active]: { + ...mockGlobalState.timeline.timelineById.test, + timelineType: TimelineType.template, + }, + }, + }, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + mockGlobalState.sourcerer.defaultDataView, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '1234', + title: 'auditbeat-*', + patternList: ['auditbeat-*'], + }, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '12347', + title: 'packetbeat-*', + patternList: ['packetbeat-*'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + loading: false, + patternList, + selectedDataViewId: null, + selectedPatterns: ['myFakebeat-*'], + missingPatterns: ['myFakebeat-*'], + }, + }, + }, + }; + + beforeEach(() => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + activePatterns: ['myFakebeat-*'], + }); + store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + render( + + + + ); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Show UpdateDefaultDataViewModal CallOut', () => { + fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); + fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); + + expect(screen.getByTestId('sourcerer-deprecated-callout')).toHaveTextContent( + 'This timeline template uses a legacy data view selector' + ); + + expect(screen.getByTestId('sourcerer-deprecated-message')).toHaveTextContent( + "We have preserved your timeline template by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view here." + ); + }); +}); + +describe('Missing index patterns', () => { + const { storage } = createSecuritySolutionStorageMock(); + const state2 = { + ...mockGlobalState, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + ...mockGlobalState.timeline.timelineById, + [TimelineId.active]: { + ...mockGlobalState.timeline.timelineById.test, + timelineType: TimelineType.template, + }, + }, + }, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + mockGlobalState.sourcerer.defaultDataView, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '1234', + title: 'auditbeat-*', + patternList: ['auditbeat-*'], + }, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '12347', + title: 'packetbeat-*', + patternList: ['packetbeat-*'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + loading: false, + patternList, + selectedDataViewId: 'fake-data-view-id', + selectedPatterns: ['myFakebeat-*'], + missingPatterns: ['myFakebeat-*'], + }, + }, + }, + }; + + beforeEach(() => { + const pollForSignalIndexMock = jest.fn(); + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Show UpdateDefaultDataViewModal CallOut for timeline', async () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + activePatterns: ['myFakebeat-*'], + }); + const state3 = cloneDeep(state2); + state3.timeline.timelineById[TimelineId.active].timelineType = TimelineType.default; + store = createStore(state3, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + render( + + + + ); + + fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); + + fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); + + expect(screen.getByTestId('sourcerer-deprecated-callout').textContent).toBe( + 'This timeline is out of date with the Security Data View' + ); + expect(screen.getByTestId('sourcerer-current-patterns-message').textContent).toBe( + 'The active index patterns in this timeline are: myFakebeat-*' + ); + expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( + 'Security Data View is missing the following index patterns: myFakebeat-*' + ); + expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( + "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." + ); + }); + + test('Show UpdateDefaultDataViewModal CallOut for timeline template', async () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + activePatterns: ['myFakebeat-*'], + }); + store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + render( + + + + ); + + fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); + + fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); + + await waitFor(() => { + expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( + 'This timeline template is out of date with the Security Data View' + ); + + expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( + 'The active index patterns in this timeline template are: myFakebeat-*' + ); + + expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( + 'Security Data View is missing the following index patterns: myFakebeat-*' + ); + + expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( + "We have preserved your timeline template by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." + ); + }); + }); +}); + +describe('Sourcerer integration tests', () => { + const state = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + mockGlobalState.sourcerer.defaultDataView, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '1234', + title: 'fakebeat-*,neatbeat-*', + patternList: ['fakebeat-*'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + loading: false, + selectedDataViewId: id, + selectedPatterns: patternListNoSignals.slice(0, 2), + }, + }, + }, + }; + + const { storage } = createSecuritySolutionStorageMock(); + + beforeEach(() => { + const pollForSignalIndexMock = jest.fn(); + (useSignalHelpers as jest.Mock).mockReturnValue({ + pollForSignalIndex: pollForSignalIndexMock, + signalIndexNeedsInit: false, + }); + + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + activePatterns: ['myFakebeat-*'], + }); + store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + jest.clearAllMocks(); + }); + + it('Selects a different index pattern', async () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + wrapper.find(`button[data-test-subj="sourcerer-select"]`).first().simulate('click'); + + wrapper.find(`[data-test-subj="dataView-option-super"]`).first().simulate('click'); + expect(checkOptionsAndSelections(wrapper, ['fakebeat-*'])).toEqual({ + availableOptionCount: 0, + optionsSelected: true, + }); + wrapper.find(`button[data-test-subj="sourcerer-save"]`).first().simulate('click'); + + expect(mockDispatch).toHaveBeenCalledWith( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.default, + selectedDataViewId: '1234', + selectedPatterns: ['fakebeat-*'], + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx index 3100e9a43de2e..491490c07dad5 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx @@ -7,6 +7,8 @@ import React from 'react'; +import { fireEvent, screen, waitFor } from '@testing-library/dom'; +import { render, cleanup } from '@testing-library/react'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { Sourcerer } from '.'; import { sourcererModel } from '../../store/sourcerer'; @@ -18,10 +20,8 @@ import { TestProviders, } from '../../mock'; import { createStore } from '../../store'; -import { fireEvent, screen, waitFor } from '@testing-library/dom'; import { useSourcererDataView } from '../../containers/sourcerer'; import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; -import { render, cleanup } from '@testing-library/react'; const mockDispatch = jest.fn(); @@ -74,13 +74,16 @@ describe('timeline sourcerer', () => { scope: sourcererModel.SourcererScopeName.timeline, }; - beforeEach(() => { + beforeEach(async () => { const pollForSignalIndexMock = jest.fn(); + (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); + (useSignalHelpers as jest.Mock).mockReturnValue({ pollForSignalIndex: pollForSignalIndexMock, signalIndexNeedsInit: false, }); + render( @@ -89,7 +92,9 @@ describe('timeline sourcerer', () => { fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); - fireEvent.click(screen.getByTestId(`sourcerer-advanced-options-toggle`)); + await waitFor(() => { + fireEvent.click(screen.getByTestId(`sourcerer-advanced-options-toggle`)); + }); }); afterEach(() => { @@ -97,6 +102,7 @@ describe('timeline sourcerer', () => { }); it('renders "alerts only" checkbox, unchecked', async () => { + screen.debug(undefined, 10000000); await waitFor(() => { expect(screen.getByTestId('sourcerer-alert-only-checkbox').parentElement).toHaveTextContent( 'Show only detection alerts' 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 b56889964f23e..da975ca4a9564 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 @@ -456,7 +456,6 @@ describe('alert actions', () => { isDataProviderVisible: false, }, to: '2018-11-05T19:03:25.937Z', - resolveTimelineConfig: undefined, ruleNote: '# this is some markdown documentation', ruleAuthor: ['elastic'], }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index af4045aec077b..e1c01f226ca78 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -81,7 +81,6 @@ export const timelineDefaults: SubsetTimelineModel & savedSearchId: null, isDiscoverSavedSearchLoaded: false, isDataProviderVisible: false, - changed: false, }; export const getTimelineManageDefaults = (id: string) => ({ diff --git a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts index 44654e18537b9..e84d38c78fd8b 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts @@ -222,7 +222,7 @@ export const TIMELINE_FLYOUT_BODY = '[data-test-subj="query-tab-flyout-body"]'; export const TIMELINE_HEADER = '[data-test-subj="timeline-hide-show-container"]'; -export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-icon-button"]`; +export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-empty-button"]`; export const TIMELINE_PANEL = `[data-test-subj="timeline-flyout-header-panel"]`; From 8fa0e7449d6b52baa4fda81af3dd8ead33df4a0b Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Thu, 9 Nov 2023 16:56:43 +0100 Subject: [PATCH 18/43] fix: review feedback --- .../flyout/action_menu/new_timeline.tsx | 11 +++++-- .../timeline/data_providers/index.tsx | 13 +++++--- .../components/timeline/kpi/kpi_container.tsx | 10 +++--- .../search_or_filter/search_or_filter.tsx | 32 +++++++++---------- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx index 192227348fce8..37f6a441d81c2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/new_timeline.tsx @@ -36,14 +36,19 @@ export const NewTimelineAction = ({ timelineId }: NewTimelineActionProps) => { ); }, [onActionBtnClick]); + const panelStyle = useMemo( + () => ({ + padding: 0, + }), + [] + ); + return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index 1ae59635c6295..b33fb6583cbd6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -147,6 +147,14 @@ export const DataProviders = React.memo(({ timelineId }) => { [timelineId, dispatch] ); + const popoverProps = useMemo( + () => ({ + className: searchOrFilterPopoverClassName, + panelClassName: searchOrFilterPopoverClassName, + }), + [] + ); + return ( <> @@ -166,10 +174,7 @@ export const DataProviders = React.memo(({ timelineId }) => { itemClassName={timelineSelectModeItemsClassName} onChange={handleChange} options={options} - popoverProps={{ - className: searchOrFilterPopoverClassName, - panelClassName: searchOrFilterPopoverClassName, - }} + popoverProps={popoverProps} valueOfSelected={kqlMode} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx index 049801824073a..032f631fdc68c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx @@ -64,7 +64,7 @@ export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { const kqlQueryExpression = isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) ? ' ' : kqlQueryTimeline; - const kqlQueryTest = useMemo( + const kqlQuery = useMemo( () => ({ query: kqlQueryExpression, language: 'kuery' }), [kqlQueryExpression] ); @@ -97,17 +97,17 @@ export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { indexPattern, browserFields, filters: filters ? filters : [], - kqlQuery: kqlQueryTest, + kqlQuery, kqlMode, }), - [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryTest] + [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQuery] ); const isBlankTimeline: boolean = useMemo( () => - (isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQueryTest.query)) || + (isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQuery.query)) || combinedQueries?.filterQuery === undefined, - [dataProviders, filters, kqlQueryTest, combinedQueries] + [dataProviders, filters, kqlQuery, combinedQueries] ); const [, kpis] = useTimelineKpis({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 8230832853888..cad4df954d365 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -6,7 +6,7 @@ */ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled, { createGlobalStyle } from 'styled-components'; import type { Filter } from '@kbn/es-query'; @@ -101,6 +101,18 @@ export const SearchOrFilter = React.memo( isDataProviderVisible, toggleDataProviderVisibility, }) => { + const isDataProviderEmpty = useMemo(() => dataProviders?.length === 0, [dataProviders]); + + const dataProviderIconTooltipContent = useMemo(() => { + if (isDataProviderVisible) { + return DATA_PROVIDER_VISIBLE; + } + if (isDataProviderEmpty) { + return DATA_PROVIDER_HIDDEN_EMPTY; + } + return DATA_PROVIDER_HIDDEN_POPULATED; + }, [isDataProviderEmpty, isDataProviderVisible]); + return ( <> @@ -139,15 +151,7 @@ export const SearchOrFilter = React.memo( alignItems="center" justifyContent="center" > - 0 && !isDataProviderVisible - ? DATA_PROVIDER_HIDDEN_POPULATED - : dataProviders?.length === 0 && !isDataProviderVisible - ? DATA_PROVIDER_HIDDEN_EMPTY - : DATA_PROVIDER_VISIBLE - } - > + 0 && !isDataProviderVisible ? 'warning' : 'primary' @@ -157,13 +161,7 @@ export const SearchOrFilter = React.memo( data-test-subj="toggle-data-provider" size="m" display="base" - aria-label={ - dataProviders?.length > 0 && !isDataProviderVisible - ? DATA_PROVIDER_HIDDEN_POPULATED - : dataProviders?.length === 0 && !isDataProviderVisible - ? DATA_PROVIDER_HIDDEN_EMPTY - : DATA_PROVIDER_VISIBLE - } + aria-label={dataProviderIconTooltipContent} onClick={toggleDataProviderVisibility} /> From 2ae0cdc12ffdbbf777c82b7b3d69f2bf98e918e0 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Thu, 9 Nov 2023 17:06:36 +0100 Subject: [PATCH 19/43] fix: data provider redesign --- .../timeline/data_providers/index.tsx | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index b33fb6583cbd6..33ccaa4f04cfc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -11,7 +11,7 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { v4 as uuidv4 } from 'uuid'; import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; -import { EuiToolTip, EuiSuperSelect } from '@elastic/eui'; +import { EuiToolTip, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { createGlobalStyle } from '@kbn/react-kibana-context-styled'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; @@ -36,7 +36,6 @@ interface Props { const DropTargetDataProvidersContainer = styled.div` position: relative; - padding: 20px 0 0px 0; .${IS_DRAGGING_CLASS_NAME} & .drop-target-data-providers { background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.1)}; @@ -117,10 +116,7 @@ const SearchOrFilterGlobalStyle = createGlobalStyle` `; const CustomTooltipDiv = styled.div` - position: absolute; - left: 20px; - transform: translateY(-50%); - z-index: 9999; + position: relative; `; export const DataProviders = React.memo(({ timelineId }) => { @@ -162,39 +158,45 @@ export const DataProviders = React.memo(({ timelineId }) => { aria-label={i18n.QUERY_AREA_ARIA_LABEL} className="drop-target-data-providers-container" > - - - - - - - {dataProviders != null && dataProviders.length ? ( - - ) : ( - - - - )} - + + + + + + + + + + + {dataProviders != null && dataProviders.length ? ( + + ) : ( + + + + )} + + + ); From 92b5f91074982a4ff325637eb6698691e2967c52 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Fri, 10 Nov 2023 04:15:14 +0100 Subject: [PATCH 20/43] fix: tests + housekeeping --- .../common/components/sourcerer/misc.test.tsx | 3 +-- .../sourcerer/timeline_sourcerer.test.tsx | 3 +-- .../timeline/data_providers/index.tsx | 4 +--- .../header/__snapshots__/index.test.tsx.snap | 18 ++++++++++-------- .../timeline/query_tab_content/index.tsx | 8 ++------ .../search_or_filter/search_or_filter.tsx | 2 +- 6 files changed, 16 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/misc.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/misc.test.tsx index f5d02b7654b0d..5acd207856619 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/misc.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/misc.test.tsx @@ -21,13 +21,12 @@ import { TestProviders, } from '../../mock'; import { createStore } from '../../store'; -import { fireEvent, screen, waitFor } from '@testing-library/dom'; import { useSourcererDataView } from '../../containers/sourcerer'; import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; import { TimelineId } from '../../../../common/types/timeline'; import { TimelineType } from '../../../../common/api/timeline'; import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer'; -import { render } from '@testing-library/react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; const mockDispatch = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx index 491490c07dad5..3628f781cfa6c 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/timeline_sourcerer.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; -import { fireEvent, screen, waitFor } from '@testing-library/dom'; -import { render, cleanup } from '@testing-library/react'; +import { render, cleanup, fireEvent, screen, waitFor } from '@testing-library/react'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { Sourcerer } from '.'; import { sourcererModel } from '../../store/sourcerer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index 33ccaa4f04cfc..b2e860a6f52d3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -55,12 +55,10 @@ const DropTargetDataProviders = styled.div` display: flex; flex-direction: column; justify-content: flex-start; - padding-bottom: 8px; - padding-top: 28px; position: relative; border: 0.2rem dashed ${({ theme }) => theme.eui.euiColorMediumShade}; border-radius: 5px; - /* padding: ${({ theme }) => theme.eui.euiSizeXS} 0; */ + padding: ${({ theme }) => theme.eui.euiSizeS} 0; margin: 0px 0 0px 0; max-height: 33vh; min-height: 100px; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap index d322d35e86740..95a67c451e1ad 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -37,8 +37,9 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` "_destroy": [Function], "_events": Object { "end": [Function], + "error": [Function], }, - "_eventsCount": 1, + "_eventsCount": 2, "_hadError": false, "_host": null, "_isStdio": true, @@ -117,7 +118,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` "needDrain": false, "objectMode": false, "onwrite": [Function], - "pendingcb": 11, + "pendingcb": 7, "prefinished": false, "sync": false, "writecb": null, @@ -126,11 +127,11 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` Symbol(kOnFinished): Array [], }, "allowHalfOpen": false, - "columns": 283, + "columns": 60, "connecting": false, "destroySoon": [Function], "fd": 1, - "rows": 70, + "rows": 52, "server": null, "write": [Function], Symbol(async_id_symbol): 5, @@ -154,8 +155,9 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` "_destroy": [Function], "_events": Object { "end": [Function], + "error": [Function], }, - "_eventsCount": 1, + "_eventsCount": 2, "_hadError": false, "_host": null, "_isStdio": true, @@ -234,7 +236,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` "needDrain": false, "objectMode": false, "onwrite": [Function], - "pendingcb": 11, + "pendingcb": 7, "prefinished": false, "sync": false, "writecb": null, @@ -243,11 +245,11 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` Symbol(kOnFinished): Array [], }, "allowHalfOpen": false, - "columns": 283, + "columns": 60, "connecting": false, "destroySoon": [Function], "fd": 1, - "rows": 70, + "rows": 52, "server": null, "write": [Function], Symbol(async_id_symbol): 5, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index e0265bc7ac312..3ef6be66574b2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -348,11 +348,7 @@ export const QueryTabContentComponent: React.FC = ({ {timelineFullScreen && setTimelineFullScreen != null && ( - + = ({ )} - + ( - + Date: Fri, 10 Nov 2023 05:25:45 +0100 Subject: [PATCH 21/43] fix: organise tests + more fixes --- .../flyout/action_menu/index.test.tsx | 88 +++ .../components/flyout/action_menu/index.tsx | 10 +- .../components/flyout/header/index.test.tsx | 145 ---- .../header/__snapshots__/index.test.tsx.snap | 623 ------------------ .../components/timeline/header/index.test.tsx | 10 - .../components/timeline/kpi/index.test.tsx | 100 +++ .../components/timeline/kpi/kpi_container.tsx | 3 +- .../search_or_filter/search_or_filter.tsx | 9 +- 8 files changed, 202 insertions(+), 786 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx new file mode 100644 index 0000000000000..39dc27c540d56 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana'; +import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { allCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { TimelineActionMenu } from '.'; +import { TimelineId, TimelineTabs } from '../../../../../common/types'; + +const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock; +jest.mock('../../../../common/containers/sourcerer'); + +const useKibanaMock = useKibana as jest.Mocked; +jest.mock('../../../../common/lib/kibana'); +jest.mock('@kbn/i18n-react', () => { + const originalModule = jest.requireActual('@kbn/i18n-react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + + return { + ...originalModule, + FormattedRelative, + }; +}); + +const sourcererDefaultValue = { + sourcererDefaultValue: mockBrowserFields, + indexPattern: mockIndexPattern, + loading: false, + selectedPatterns: mockIndexNames, +}; + +describe('Action menu', () => { + beforeEach(() => { + // Mocking these services is required for the header component to render. + mockUseSourcererDataView.mockImplementation(() => sourcererDefaultValue); + useKibanaMock().services.application.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + describe('AddToCaseButton', () => { + it('renders the button when the user has create and read permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); + + render( + + + + ); + + expect(screen.getByTestId('attach-timeline-case-button')).toBeInTheDocument(); + }); + + it('does not render the button when the user does not have create permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + + render( + + + + ); + + expect(screen.queryByTestId('attach-timeline-case-button')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx index 914f6adbe04fe..3eea5c457598d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx @@ -7,6 +7,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; +import { useGetUserCasesPermissions } from '../../../../common/lib/kibana'; import type { TimelineTabs } from '../../../../../common/types'; import { InspectButton } from '../../../../common/components/inspect'; import { InputsModelId } from '../../../../common/store/inputs/constants'; @@ -28,6 +29,7 @@ export const TimelineActionMenu = ({ activeTab, isInspectButtonDisabled, }: TimelineActionMenuProps) => { + const userCasesPermissions = useGetUserCasesPermissions(); return ( @@ -36,9 +38,11 @@ export const TimelineActionMenu = ({ - - - + {userCasesPermissions.create && userCasesPermissions.read ? ( + + + + ) : null} ({ - useTimelineKpis: jest.fn(), -})); -const useKibanaMock = useKibana as jest.Mocked; -jest.mock('../../../../common/lib/kibana'); -jest.mock('@kbn/i18n-react', () => { - const originalModule = jest.requireActual('@kbn/i18n-react'); - const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); - - return { - ...originalModule, - FormattedRelative, - }; -}); -const mockUseTimelineKpiResponse = { - processCount: 1, - userCount: 1, - sourceIpCount: 1, - hostCount: 1, - destinationIpCount: 1, -}; - -const mockUseTimelineLargeKpiResponse = { - processCount: 1000, - userCount: 1000000, - sourceIpCount: 1000000000, - hostCount: 999, - destinationIpCount: 1, -}; -const defaultMocks = { - browserFields: mockBrowserFields, - indexPattern: mockIndexPattern, - loading: false, - selectedPatterns: mockIndexNames, -}; -describe('header', () => { - beforeEach(() => { - // Mocking these services is required for the header component to render. - mockUseSourcererDataView.mockImplementation(() => defaultMocks); - useKibanaMock().services.application.capabilities = { - navLinks: {}, - management: {}, - catalogue: {}, - actions: { show: true, crud: true }, - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe.skip('AddToCaseButton', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); - }); - - it('renders the button when the user has create and read permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); - - render(); - - expect(screen.getByTestId('attach-timeline-case-button')).toBeInTheDocument(); - }); - - it('does not render the button when the user does not have create permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); - - render(); - - expect(screen.queryByTestId('attach-timeline-case-button')).not.toBeInTheDocument(); - }); - }); - - describe.skip('Timeline KPIs', () => { - describe('when the data is not loading and the response contains data', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); - }); - it('renders the component, labels and values successfully', () => { - render(); - expect(screen.getByTestId('siem-timeline-kpis')).toBeInTheDocument(); - // label - expect(screen.getByText('Processes')).toBeInTheDocument(); - // value - expect(screen.getByTestId('siem-timeline-process-kpi').textContent).toContain('1'); - }); - }); - - describe('when the data is loading', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]); - }); - it('renders a loading indicator for values', async () => { - render(); - expect(screen.getAllByText('--')).not.toHaveLength(0); - }); - }); - - describe('when the response is null and timeline is blank', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, null]); - }); - it('renders labels and the default empty string', () => { - render(); - expect(screen.getByText('Processes')).toBeInTheDocument(); - expect(screen.getAllByText(getEmptyValue())).not.toHaveLength(0); - }); - }); - - describe('when the response contains numbers larger than one thousand', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]); - }); - it('formats the numbers correctly', () => { - render(); - expect(screen.getByText('1k', { selector: '.euiTitle' })).toBeInTheDocument(); - expect(screen.getByText('1m', { selector: '.euiTitle' })).toBeInTheDocument(); - expect(screen.getByText('1b', { selector: '.euiTitle' })).toBeInTheDocument(); - expect(screen.getByText('999', { selector: '.euiTitle' })).toBeInTheDocument(); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 95a67c451e1ad..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,623 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header rendering renders correctly against snapshot 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx index 905ed19278925..805c777aea474 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; import { coreMock } from '@kbn/core/public/mocks'; @@ -50,15 +49,6 @@ describe('Header', () => { }; describe('rendering', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - - - ); - expect(wrapper).toMatchSnapshot(); - }); - test('it renders the data providers when show is true', async () => { const testProps = { ...props, show: true }; const wrapper = await getWrapper( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.test.tsx new file mode 100644 index 0000000000000..1d91e828524c1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/index.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { useTimelineKpis } from '../../../containers/kpis'; +import { TimelineKpi } from '.'; +import { TimelineId } from '../../../../../common/types'; +import { getEmptyValue } from '../../../../common/components/empty_value'; + +jest.mock('../../../containers/kpis', () => ({ + useTimelineKpis: jest.fn(), +})); + +jest.mock('../../../../common/lib/kibana'); + +jest.mock('@kbn/i18n-react', () => { + const originalModule = jest.requireActual('@kbn/i18n-react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + + return { + ...originalModule, + FormattedRelative, + }; +}); + +const mockUseTimelineKpis: jest.Mock = useTimelineKpis as jest.Mock; + +const mockUseTimelineKpiResponse = { + processCount: 1, + userCount: 1, + sourceIpCount: 1, + hostCount: 1, + destinationIpCount: 1, +}; + +const mockUseTimelineLargeKpiResponse = { + processCount: 1000, + userCount: 1000000, + sourceIpCount: 1000000000, + hostCount: 999, + destinationIpCount: 1, +}; + +describe('Timeline KPIs', () => { + describe('when the data is not loading and the response contains data', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); + }); + it('renders the component, labels and values successfully', () => { + render( + + + + ); + expect(screen.getByTestId('siem-timeline-kpis')).toBeInTheDocument(); + // label + expect(screen.getByText('Processes :')).toBeInTheDocument(); + // value + expect(screen.getByTestId('siem-timeline-process-kpi').textContent).toContain('1'); + }); + }); + + describe('when the response is null and timeline is blank', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, null]); + }); + it('renders labels and the default empty string', () => { + render( + + + + ); + expect(screen.getByText('Processes :')).toBeInTheDocument(); + expect(screen.getAllByText(getEmptyValue())).not.toHaveLength(0); + }); + }); + + describe('when the response contains numbers larger than one thousand', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]); + }); + it('formats the numbers correctly', () => { + render( + + + + ); + expect(screen.getByTitle('1k')).toBeInTheDocument(); + expect(screen.getByTitle('1m')).toBeInTheDocument(); + expect(screen.getByTitle('1b')).toBeInTheDocument(); + expect(screen.getByTitle('999')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx index 032f631fdc68c..01889d2cfa022 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx @@ -61,8 +61,7 @@ export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)!); - const kqlQueryExpression = - isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) ? ' ' : kqlQueryTimeline; + const kqlQueryExpression = isEmpty(kqlQueryTimeline) ? ' ' : kqlQueryTimeline; const kqlQuery = useMemo( () => ({ query: kqlQueryExpression, language: 'kuery' }), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index c61edb8737a45..d331f7bc710f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -113,6 +113,11 @@ export const SearchOrFilter = React.memo( return DATA_PROVIDER_HIDDEN_POPULATED; }, [isDataProviderEmpty, isDataProviderVisible]); + const buttonColor = useMemo( + () => (isDataProviderEmpty || isDataProviderVisible ? 'primary' : 'warning'), + [isDataProviderEmpty, isDataProviderVisible] + ); + return ( <> @@ -153,9 +158,7 @@ export const SearchOrFilter = React.memo( > 0 && !isDataProviderVisible ? 'warning' : 'primary' - } + color={buttonColor} isSelected={isDataProviderVisible} iconType={'timeline'} data-test-subj="toggle-data-provider" From 9877b9b488d515535b5451393bac8c036088a134 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Mon, 13 Nov 2023 14:39:58 +0100 Subject: [PATCH 22/43] fix: remove unnecssary eslint exception --- .../timelines/components/timeline/kpi/kpi_container.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx index 01889d2cfa022..0d19cc386dd3f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx @@ -58,8 +58,8 @@ export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { ); const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterQuerySelector(), []); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)!); + + const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)); const kqlQueryExpression = isEmpty(kqlQueryTimeline) ? ' ' : kqlQueryTimeline; From 780b5fddc7881d500ad6d4235b0d54bb012c8a6c Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Mon, 13 Nov 2023 14:49:39 +0100 Subject: [PATCH 23/43] fix: types --- .../public/timelines/components/timeline/kpi/kpi_container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx index 0d19cc386dd3f..a5dae1098a95a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx @@ -61,7 +61,7 @@ export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)); - const kqlQueryExpression = isEmpty(kqlQueryTimeline) ? ' ' : kqlQueryTimeline; + const kqlQueryExpression = kqlQueryTimeline ?? ' '; const kqlQuery = useMemo( () => ({ query: kqlQueryExpression, language: 'kuery' }), From b63a5bc1a4cd090154a45bab4499c5a2b3b28772 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Wed, 15 Nov 2023 11:32:52 +0100 Subject: [PATCH 24/43] fix: review feedback --- .../flyout/action_menu/translations.ts | 9 ++-- .../flyout/header/timeline_status_info.tsx | 5 +- .../components/timeline/kpi/kpi_container.tsx | 13 +---- .../timeline/search_or_filter/index.tsx | 6 ++- .../search_or_filter/search_or_filter.tsx | 47 ++++++++++--------- 5 files changed, 36 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts index 6c19410ef3140..dd7f7e6a6e95f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts @@ -156,12 +156,9 @@ export const SAVE_TOUR_CLOSE = i18n.translate( } ); -export const TITLE = i18n.translate( - 'xpack.securitySolution.timeline.saveTimeline.modal.titleTitle', - { - defaultMessage: 'Title', - } -); +export const TITLE = i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.title', { + defaultMessage: 'Title', +}); export const SAVE_TOUR_TITLE = i18n.translate( 'xpack.securitySolution.timeline.flyout.saveTour.title', diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_status_info.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_status_info.tsx index 1c7912bec4e88..bccba33e789b0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_status_info.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_status_info.tsx @@ -20,7 +20,7 @@ export const TimelineStatusInfoComponent = React.memo<{ const isUnsaved = status === TimelineStatus.draft; let statusContent: React.ReactNode = null; - if (isUnsaved) { + if (isUnsaved || !updated) { statusContent = {i18n.UNSAVED}; } else if (changed) { statusContent = {i18n.UNSAVED_CHANGES}; @@ -31,8 +31,7 @@ export const TimelineStatusInfoComponent = React.memo<{ ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx index a5dae1098a95a..31eb9ad5e5e53 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/kpi/kpi_container.tsx @@ -41,18 +41,7 @@ export const TimelineKpisContainer = ({ timelineId }: KpiExpandedProps) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { dataProviders, filters, kqlMode } = useDeepEqualSelector((state) => pick( - [ - 'activeTab', - 'dataProviders', - 'kqlQuery', - 'status', - 'title', - 'timelineType', - 'updated', - 'show', - 'filters', - 'kqlMode', - ], + ['dataProviders', 'filters', 'kqlMode'], getTimeline(state, timelineId) ?? timelineDefaults ) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index cd270bcf2b00c..c881c406d6fd4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -62,6 +62,7 @@ const StatefulSearchOrFilterComponent = React.memo( toStr, updateKqlMode, updateReduxTime, + timelineType, }) => { const dispatch = useDispatch(); @@ -174,6 +175,7 @@ const StatefulSearchOrFilterComponent = React.memo( updateReduxTime={updateReduxTime} toggleDataProviderVisibility={toggleDataProviderVisibility} isDataProviderVisible={isDataProviderVisible} + timelineType={timelineType} /> @@ -213,7 +215,8 @@ const StatefulSearchOrFilterComponent = React.memo( deepEqual(prevProps.filterQuery, nextProps.filterQuery) && deepEqual(prevProps.kqlMode, nextProps.kqlMode) && deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && - deepEqual(prevProps.timelineId, nextProps.timelineId) + deepEqual(prevProps.timelineId, nextProps.timelineId) && + prevProps.timelineType === nextProps.timelineType ); } ); @@ -244,6 +247,7 @@ const makeMapStateToProps = () => { to: input.timerange.to, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion toStr: input.timerange.toStr!, + timelineType: timeline.timelineType, }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index d331f7bc710f3..d297fff403118 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -11,6 +11,7 @@ import styled, { createGlobalStyle } from 'styled-components'; import type { Filter } from '@kbn/es-query'; import type { FilterManager } from '@kbn/data-plugin/public'; +import { TimelineType } from '../../../../../common/api/timeline'; import { InputsModelId } from '../../../../common/store/inputs/constants'; import type { KqlMode } from '../../../store/timeline/model'; import type { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; @@ -67,6 +68,7 @@ interface Props { updateReduxTime: DispatchUpdateReduxTime; isDataProviderVisible: boolean; toggleDataProviderVisibility: () => void; + timelineType: TimelineType; } const SearchOrFilterContainer = styled.div``; @@ -96,10 +98,10 @@ export const SearchOrFilter = React.memo( setSavedQueryId, to, toStr, - updateKqlMode, updateReduxTime, isDataProviderVisible, toggleDataProviderVisibility, + timelineType, }) => { const isDataProviderEmpty = useMemo(() => dataProviders?.length === 0, [dataProviders]); @@ -149,27 +151,28 @@ export const SearchOrFilter = React.memo( updateReduxTime={updateReduxTime} /> - - - - - - - + { + /* + DataProvider toggle is not needed in template timeline because + it is always visible + */ + timelineType === TimelineType.default ? ( + + + + + + ) : null + } From 9147e34bf8770ded43f034b2a29e322da70a07b8 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Wed, 15 Nov 2023 16:13:04 +0100 Subject: [PATCH 25/43] fix: translations title --- x-pack/plugins/translations/translations/fr-FR.json | 2 +- x-pack/plugins/translations/translations/ja-JP.json | 2 +- x-pack/plugins/translations/translations/zh-CN.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3279c3e57be7e..cb030886726b4 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -36746,7 +36746,7 @@ "xpack.securitySolution.timeline.saveTimeline.modal.header": "Enregistrer la chronologie", "xpack.securitySolution.timeline.saveTimeline.modal.optionalLabel": "Facultatif", "xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel": "Titre", - "xpack.securitySolution.timeline.saveTimeline.modal.titleTitle": "Titre", + "xpack.securitySolution.timeline.saveTimeline.modal.title": "Titre", "xpack.securitySolution.timeline.saveTimelineTemplate.modal.discard.title": "Abandonner le modèle de chronologie", "xpack.securitySolution.timeline.saveTimelineTemplate.modal.header": "Enregistrer le modèle de chronologie", "xpack.securitySolution.timeline.searchOrFilter.filterDescription": "Les événements des fournisseurs de données ci-dessus sont filtrés par le KQL adjacent", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 50331bea5d8f0..b998b03f78f63 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -36744,7 +36744,7 @@ "xpack.securitySolution.timeline.saveTimeline.modal.header": "タイムラインを保存", "xpack.securitySolution.timeline.saveTimeline.modal.optionalLabel": "オプション", "xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel": "タイトル", - "xpack.securitySolution.timeline.saveTimeline.modal.titleTitle": "タイトル", + "xpack.securitySolution.timeline.saveTimeline.modal.title": "タイトル", "xpack.securitySolution.timeline.saveTimelineTemplate.modal.discard.title": "タイムラインテンプレートを破棄", "xpack.securitySolution.timeline.saveTimelineTemplate.modal.header": "タイムラインテンプレートを保存", "xpack.securitySolution.timeline.searchOrFilter.filterDescription": "上のデータプロバイダーからのイベントは、隣接の KQL でフィルターされます", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 04b702d75a397..d800af080fbe3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -36740,7 +36740,7 @@ "xpack.securitySolution.timeline.saveTimeline.modal.header": "保存时间线", "xpack.securitySolution.timeline.saveTimeline.modal.optionalLabel": "可选", "xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel": "标题", - "xpack.securitySolution.timeline.saveTimeline.modal.titleTitle": "标题", + "xpack.securitySolution.timeline.saveTimeline.modal.title": "标题", "xpack.securitySolution.timeline.saveTimelineTemplate.modal.discard.title": "丢弃时间线模板", "xpack.securitySolution.timeline.saveTimelineTemplate.modal.header": "保存时间线模板", "xpack.securitySolution.timeline.searchOrFilter.filterDescription": "上述数据提供程序的事件按相邻 KQL 进行筛选", From 7e05d5ddc4148dfb2badfb515ac14437b4b3426f Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Thu, 16 Nov 2023 16:36:45 +0100 Subject: [PATCH 26/43] fix: hide standard filters --- src/plugins/unified_search/public/filter_bar/filter_bar.tsx | 1 + .../public/timelines/components/timeline/query_bar/index.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/unified_search/public/filter_bar/filter_bar.tsx b/src/plugins/unified_search/public/filter_bar/filter_bar.tsx index 769d6bfcaf48f..e6338d30c74ac 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_bar.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_bar.tsx @@ -55,6 +55,7 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { gutterSize="none" // We use `gap` in the styles instead for better truncation of badges alignItems="center" tabIndex={-1} + data-test-subj="filter-items-group" > {props.prepend} Date: Mon, 20 Nov 2023 18:36:31 +0100 Subject: [PATCH 27/43] fix: more UI Fixes as per PR feedback --- .../flyout/action_menu/index.test.tsx | 6 +- .../action_menu/save_timeline_button.test.tsx | 6 +- .../flyout/header/active_timelines.tsx | 65 +++++++++---------- .../components/flyout/header/index.tsx | 32 ++++----- .../flyout/header/timeline_status_info.tsx | 9 ++- .../timeline/properties/helpers.tsx | 2 + .../timeline/tabs_content/index.tsx | 2 +- 7 files changed, 63 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx index 39dc27c540d56..4bfc3635de553 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx @@ -12,7 +12,7 @@ import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../com import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { allCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; -import { TimelineActionMenu } from '.'; +import { TimelineActionMenuComponent } from '.'; import { TimelineId, TimelineTabs } from '../../../../../common/types'; const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock; @@ -58,7 +58,7 @@ describe('Action menu', () => { render( - { render( - ( - + ); @@ -58,7 +58,7 @@ describe('SaveTimelineButton', () => { }); render( - + ); expect(screen.getByRole('button')).toBeDisabled(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx index ba7a06f7f1250..54118c39ba16d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx @@ -5,14 +5,14 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiHealth, EuiToolTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiHealth, EuiText } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { isEmpty } from 'lodash/fp'; import styled from 'styled-components'; -import { FormattedRelative } from '@kbn/i18n-react'; -import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline'; +import type { TimelineStatus } from '../../../../../common/api/timeline'; +import { TimelineType } from '../../../../../common/api/timeline'; import { TimelineEventsCountBadge } from '../../../../common/hooks/use_timeline_events_count'; import { ACTIVE_TIMELINE_BUTTON_CLASS_NAME, @@ -46,6 +46,7 @@ const TitleConatiner = styled(EuiFlexItem)` overflow: hidden; display: inline-block; text-overflow: ellipsis; + white-space: nowrap; `; const ActiveTimelinesComponent: React.FC = ({ @@ -70,33 +71,8 @@ const ActiveTimelinesComponent: React.FC = ({ ? UNTITLED_TEMPLATE : UNTITLED_TIMELINE; - const tooltipContent = useMemo(() => { - if (timelineStatus === TimelineStatus.draft) { - return <>{i18n.UNSAVED}; - } + const titleContent = useMemo(() => { return ( - <> - {i18n.SAVED}{' '} - - - ); - }, [timelineStatus, updated]); - - return ( - = ({ responsive={false} > - {title} + {isOpen ? ( + +

{title}

+
+ ) : ( + <>{title} + )}
{!isOpen && ( )} - - - - -
+ ); + }, [isOpen, title]); + + if (isOpen) { + return <>{titleContent}; + } + + return ( + + {titleContent} ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index b8a8073e9cd28..6c1e341f91897 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -5,20 +5,14 @@ * 2.0. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiToolTip, - EuiButtonIcon, - useEuiTheme, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { isEmpty, get, pick } from 'lodash/fp'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import type { State } from '../../../../common/store'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; @@ -38,10 +32,20 @@ interface FlyoutHeaderPanelProps { timelineId: string; } +const FlyoutHeaderPanelContentFlexGroupContainer = styled(EuiFlexGroup)` + overflow-x: auto; +`; + const ActiveTimelinesContainer = styled(EuiFlexItem)` overflow: hidden; `; +const TimelinePanel = euiStyled(EuiPanel)` + backgroundColor: ${(props) => props.theme.eui.euiColorEmptyShade}; + color: ${(props) => props.theme.eui.euiTextColor}; + padding-inline: ${(props) => props.theme.eui.euiSizeM}; +`; + const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline); @@ -115,19 +119,17 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline focusActiveTimelineButton(); }, [dispatch, timelineId]); - const { euiTheme } = useEuiTheme(); - return ( - - = ({ timeline
)} -
- + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_status_info.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_status_info.tsx index bccba33e789b0..77502b16bfbf1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_status_info.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/timeline_status_info.tsx @@ -9,9 +9,14 @@ import React from 'react'; import { EuiTextColor, EuiText } from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n-react'; +import styled from 'styled-components'; import { TimelineStatus } from '../../../../../common/api/timeline'; import * as i18n from './translations'; +const NoWrapText = styled(EuiText)` + white-space: nowrap; +`; + export const TimelineStatusInfoComponent = React.memo<{ status: TimelineStatus; updated?: number; @@ -37,9 +42,9 @@ export const TimelineStatusInfoComponent = React.memo<{ ); } return ( - + {statusContent} - + ); }); TimelineStatusInfoComponent.displayName = 'TimelineStatusInfoComponent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 8c4602f2d1f88..0330e3ed74d57 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -62,6 +62,7 @@ const AddToFavoritesButtonComponent: React.FC = ({ data-test-subj={`timeline-favorite-${isFavorite ? 'filled' : 'empty'}-star`} disabled={disableFavoriteButton} aria-label={label} + title={label} /> ) : ( = ({ data-test-subj={`timeline-favorite-${isFavorite ? 'filled' : 'empty'}-star`} disabled={disableFavoriteButton} aria-label={label} + title={label} > {label} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index b62e36e20b938..d1e2ce298bd35 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -389,7 +389,7 @@ const TabsContentComponent: React.FC = ({ return ( <> {!timelineFullScreen && ( - + Date: Tue, 21 Nov 2023 11:40:31 +0100 Subject: [PATCH 28/43] hide kpis till lens embeddables are working --- .../components/timeline/query_tab_content/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index b4a1cace9e297..0b63fe4ae142a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -62,7 +62,6 @@ import { getDefaultControlColumn } from '../body/control_columns'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { useLicense } from '../../../../common/hooks/use_license'; import { HeaderActions } from '../../../../common/components/header_actions/header_actions'; -import { TimelineKpi } from '../kpi'; const QueryTabHeaderContainer = styled.div` /* margin-top: 6px; */ width: 100%; @@ -367,9 +366,11 @@ export const QueryTabContentComponent: React.FC = ({ />
- - - + {/* TODO: This is a temporary solution to hide the KPIs until lens components play nicely with timelines */} + {/* https://github.com/elastic/kibana/issues/17156 */} + {/* */} + {/* */} + {/* */}
From 4ed7d4e1b70e1dfd6ebf2d23332cffd09ae04e73 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Tue, 21 Nov 2023 13:01:12 +0100 Subject: [PATCH 29/43] fix: modal padding + radius + alignment + more nits --- .../components/rules/eql_query_bar/footer.tsx | 190 ++++++++++-------- .../flyout/header/active_timelines.tsx | 4 + .../components/flyout/header/index.tsx | 5 +- .../components/flyout/pane/pane.styles.tsx | 3 - .../timeline/tabs_content/index.tsx | 8 +- 5 files changed, 116 insertions(+), 94 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx index a0647a2c953c2..7f7a6ab21a385 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx @@ -162,97 +162,113 @@ export const EqlQueryBarFooter: FC = ({ return ( - - - {errors.length > 0 && ( - - )} - {isLoading && } + + + + + {errors.length > 0 && ( + + )} + {isLoading && } + + - {!onOptionsChange && ( - - - - )} + + + {!onOptionsChange && ( + + + + )} - {onOptionsChange && ( - <> - - - - - - } - isOpen={openEqlSettings} - closePopover={closeEqlSettingsHandler} - anchorPosition="downCenter" - ownFocus={false} - > - {i18n.EQL_SETTINGS_TITLE} -
- {!isSizeOptionDisabled && ( - - + + + + + - - )} - - - - - - - - - -
-
-
- - )} + {i18n.EQL_SETTINGS_TITLE} +
+ {!isSizeOptionDisabled && ( + + + + )} + + + + + + + + + +
+
+ + + )} + + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx index 54118c39ba16d..87ad757bf6b12 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx @@ -37,6 +37,10 @@ interface ActiveTimelinesProps { } const StyledEuiButtonEmpty = styled(EuiButtonEmpty)` + &:active, + &:focus { + background: transparent; + } > span { padding: 0; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index 6c1e341f91897..2a13bf0c850f8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -40,10 +40,11 @@ const ActiveTimelinesContainer = styled(EuiFlexItem)` overflow: hidden; `; -const TimelinePanel = euiStyled(EuiPanel)` +const TimelinePanel = euiStyled(EuiPanel)<{ $isOpen?: boolean }>` backgroundColor: ${(props) => props.theme.eui.euiColorEmptyShade}; color: ${(props) => props.theme.eui.euiTextColor}; padding-inline: ${(props) => props.theme.eui.euiSizeM}; + border-radius: ${({ $isOpen, theme }) => ($isOpen ? theme.eui.euiBorderRadius : '0px')}; `; const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { @@ -121,7 +122,7 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline return ( { .timeline-template-badge { border-radius: ${euiTheme.border.radius.medium} ${euiTheme.border.radius.medium} 0 0; // top corners only } - .timeline-body { - padding: 0 ${euiTheme.size.s}; - } } `; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index d1e2ce298bd35..3f65e542ed30c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -276,6 +276,10 @@ const StyledEuiTab = styled(EuiTab)` } `; +const StyledEuiTabs = styled(EuiTabs)` + padding-inline: ${(props) => props.theme.eui.euiSizeM}; +`; + const TabsContentComponent: React.FC = ({ renderCellValue, rowRenderers, @@ -389,7 +393,7 @@ const TabsContentComponent: React.FC = ({ return ( <> {!timelineFullScreen && ( - + = ({ {i18n.SECURITY_ASSISTANT} )} - + )} Date: Tue, 21 Nov 2023 14:20:53 +0100 Subject: [PATCH 30/43] fix: more fixes --- .../public/common/components/inspect/index.tsx | 2 +- .../public/timelines/components/flyout/action_menu/index.tsx | 2 +- .../timelines/components/timeline/search_or_filter/index.tsx | 1 - .../components/timeline/search_or_filter/search_or_filter.tsx | 3 ++- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index 295fbdf84659b..b548a815bf100 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -40,7 +40,7 @@ interface InspectButtonProps { onCloseInspect?: () => void; queryId: string; showInspectButton?: boolean; - title: string | React.ReactElement | React.ReactNode; + title?: string | React.ReactElement | React.ReactNode; } const InspectButtonComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx index e222a6e76be98..e74fb7fdfb951 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx @@ -49,7 +49,7 @@ const TimelineActionMenuComponent = ({ queryId={`${timelineId}-${activeTab}`} inputId={InputsModelId.timeline} isDisabled={isInspectButtonDisabled} - title="Inspect" + title="" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index 59975eaca6b8b..c63eadfe5efa6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -159,7 +159,6 @@ const StatefulSearchOrFilterComponent = React.memo( alignItems="center" gutterSize="xs" responsive={false} - css={{ overflowX: 'auto' }} > Date: Tue, 21 Nov 2023 15:37:04 +0100 Subject: [PATCH 31/43] fix: notes tab spacing inconsistencies --- .../public/timelines/components/notes/add_note/index.tsx | 2 +- .../components/timeline/notes_tab_content/index.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx index 31df17fa40046..f770d3a46c46d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx @@ -33,7 +33,7 @@ const AddNotesContainer = styled.div` AddNotesContainer.displayName = 'AddNotesContainer'; const ButtonsContainer = styled(EuiFlexGroup)` - margin-top: 5px; + margin-top: ${({ theme }) => theme.eui.euiSizeS}; `; ButtonsContainer.displayName = 'ButtonsContainer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx index d43f92fc96e69..f7f94b3a7fa0c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx @@ -21,6 +21,7 @@ import React, { Fragment, useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import type { EuiTheme } from '@kbn/react-kibana-context-styled'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineActions } from '../../../store/timeline'; @@ -51,6 +52,8 @@ const FullWidthFlexGroup = styled(EuiFlexGroup)` const ScrollableFlexItem = styled(EuiFlexItem)` overflow-x: hidden; overflow-y: auto; + padding-inline: ${({ theme }) => (theme as EuiTheme).eui.euiSizeM}; + padding-block: ${({ theme }) => (theme as EuiTheme).eui.euiSizeS}; `; const VerticalRule = styled.div` @@ -218,9 +221,9 @@ const NotesTabContentComponent: React.FC = ({ timelineId } ); return ( - + - +

{NOTES}

From 6213e11a9a6f126ccb6f4f2e964063b9dae7b616 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Tue, 21 Nov 2023 16:38:16 +0100 Subject: [PATCH 32/43] fix: cypress tests --- .../flyout/__snapshots__/index.test.tsx.snap | 70 ++++++++----------- .../investigations/timelines/creation.cy.ts | 1 + .../timelines/flyout_button.cy.ts | 5 +- .../timelines/unsaved_timeline.cy.ts | 25 ++++--- .../cypress/tasks/security_main.ts | 10 +-- 5 files changed, 48 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap index 8f4c9185596e5..88bf414235613 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap @@ -9,53 +9,69 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` data-test-subj="flyout-pane" />
- .c3 { - display: block; + .c3:active, +.c3:focus { + background: transparent; } -.c1 > span { +.c3 > span { padding: 0; } -.c2 { +.c4 { overflow: hidden; display: inline-block; text-overflow: ellipsis; + white-space: nowrap; } -.c0 { +.c5 { + white-space: nowrap; +} + +.c1 { + overflow-x: auto; +} + +.c2 { overflow: hidden; } +.c0 { + backgroundColor: #1d1e24; + color: #dfe5ef; + padding-inline: 12px; + border-radius: 0px; +} +