diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx index bec790ca8cce0..14f010640ef12 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx @@ -8,7 +8,7 @@ import type { FieldSpec } from '@kbn/data-views-plugin/common'; import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { groupActions, groupByIdSelector } from './state'; import type { GroupOption } from './types'; @@ -64,19 +64,21 @@ export const useGetGroupSelectorStateless = ({ [onGroupChange] ); - return ( - - ); + return useMemo(() => { + return ( + + ); + }, [groupingId, fields, maxGroupingLevels, defaultGroupingOptions, onChange]); }; export const useGetGroupSelector = ({ @@ -198,18 +200,19 @@ export const useGetGroupSelector = ({ } }, [defaultGroupingOptions, options, selectedGroups, setOptions]); - return ( - - ); + return useMemo(() => { + return ( + + ); + }, [groupingId, fields, maxGroupingLevels, onChange, selectedGroups, options]); }; diff --git a/x-pack/examples/triggers_actions_ui_example/public/plugin.tsx b/x-pack/examples/triggers_actions_ui_example/public/plugin.tsx index ee5c3229cb713..5077795b620f7 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/plugin.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/plugin.tsx @@ -133,8 +133,11 @@ export class TriggersActionsUiExamplePlugin id: 'observabilityCases', columns, useInternalFlyout, - getRenderCellValue: () => (props) => { - const value = props.data.find((d) => d.field === props.columnId)?.value ?? []; + getRenderCellValue: (props: { + data?: Array<{ field: string; value: string }>; + columnId?: string; + }) => { + const value = props.data?.find((d) => d.field === props.columnId)?.value ?? []; if (Array.isArray(value)) { return <>{value.length ? value.join() : '--'}; diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx index 2c739cfe1b984..0fa30647b60ac 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useApplication } from '../../../common/lib/kibana/use_application'; import { CaseStatuses } from '../../../../common/types/domain'; import type { AllCasesSelectorModalProps } from '.'; @@ -158,9 +158,11 @@ export const useCasesAddToExistingCaseModal = ({ [closeModal, dispatch, handleOnRowClick, onClose, onCreateCaseClicked] ); - return { - open: openModal, - close: closeModal, - }; + return useMemo(() => { + return { + open: openModal, + close: closeModal, + }; + }, [openModal, closeModal]); }; export type UseCasesAddToExistingCaseModal = typeof useCasesAddToExistingCaseModal; diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx index e6d5bd2fc2a0b..ea2290bb49633 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx @@ -6,7 +6,7 @@ */ import type React from 'react'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import type { CaseAttachmentsWithoutOwner } from '../../../types'; import { useCasesToast } from '../../../common/use_cases_toast'; import type { CaseUI } from '../../../containers/types'; @@ -88,10 +88,12 @@ export const useCasesAddToNewCaseFlyout = ({ onClose, ] ); - return { - open: openFlyout, - close: closeFlyout, - }; + return useMemo(() => { + return { + open: openFlyout, + close: closeFlyout, + }; + }, [openFlyout, closeFlyout]); }; export type UseCasesAddToNewCaseFlyout = typeof useCasesAddToNewCaseFlyout; diff --git a/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/register_alerts_table_configuration.tsx b/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/register_alerts_table_configuration.tsx index 6c01c056bad99..25ffef0456e42 100644 --- a/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/register_alerts_table_configuration.tsx +++ b/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/register_alerts_table_configuration.tsx @@ -129,7 +129,7 @@ export function registerAlertsTableConfiguration( }, columns, useInternalFlyout: getAlertFlyout(columns, getAlertFormatters(fieldFormats)), - getRenderCellValue: getRenderCellValue(fieldFormats), + getRenderCellValue, sort, useActionsColumn: () => ({ renderCustomActionsRow: (props: RenderCustomActionsRowArgs) => { diff --git a/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/render_cell_value.tsx b/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/render_cell_value.tsx index bfc86e390c0e9..afba911e5ddea 100644 --- a/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/render_cell_value.tsx +++ b/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/render_cell_value.tsx @@ -6,10 +6,10 @@ */ import { isEmpty } from 'lodash'; -import React, { type ReactNode } from 'react'; +import React from 'react'; +import type { RenderCellValue } from '@elastic/eui'; import { isDefined } from '@kbn/ml-is-defined'; import { ALERT_DURATION, ALERT_END, ALERT_START } from '@kbn/rule-data-utils'; -import type { GetRenderCellValue } from '@kbn/triggers-actions-ui-plugin/public'; import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import { getFormattedSeverityScore, getSeverityColor } from '@kbn/ml-anomaly-utils'; @@ -21,11 +21,6 @@ import { } from '../../../common/constants/alerts'; import { getFieldFormatterProvider } from '../../application/contexts/kibana/use_field_formatter'; -interface Props { - columnId: string; - data: any; -} - export const getMappedNonEcsValue = ({ data, fieldName, @@ -54,22 +49,17 @@ const getRenderValue = (mappedNonEcsValue: any) => { return '—'; }; -export const getRenderCellValue = (fieldFormats: FieldFormatsRegistry): GetRenderCellValue => { +export const getRenderCellValue: RenderCellValue = ({ columnId, data, fieldFormats }) => { const alertValueFormatter = getAlertFormatters(fieldFormats); + if (!isDefined(data)) return; - return ({ setFlyoutAlert }) => - (props): ReactNode => { - const { columnId, data } = props as Props; - if (!isDefined(data)) return; + const mappedNonEcsValue = getMappedNonEcsValue({ + data, + fieldName: columnId, + }); + const value = getRenderValue(mappedNonEcsValue); - const mappedNonEcsValue = getMappedNonEcsValue({ - data, - fieldName: columnId, - }); - const value = getRenderValue(mappedNonEcsValue); - - return alertValueFormatter(columnId, value); - }; + return alertValueFormatter(columnId, value); }; export function getAlertFormatters(fieldFormats: FieldFormatsRegistry) { diff --git a/x-pack/plugins/ml/public/application/explorer/alerts/alerts_panel.tsx b/x-pack/plugins/ml/public/application/explorer/alerts/alerts_panel.tsx index d5a5821aae98a..4c76ebe628f4f 100644 --- a/x-pack/plugins/ml/public/application/explorer/alerts/alerts_panel.tsx +++ b/x-pack/plugins/ml/public/application/explorer/alerts/alerts_panel.tsx @@ -33,7 +33,9 @@ export const AlertsPanel: FC = () => { const [isOpen, setIsOpen] = useState(true); const [toggleSelected, setToggleSelected] = useState(`alertsSummary`); - + const { + services: { fieldFormats }, + } = useMlKibana(); const { anomalyDetectionAlertsStateService } = useAnomalyExplorerContext(); const countByStatus = useObservable(anomalyDetectionAlertsStateService.countByStatus$); @@ -48,6 +50,9 @@ export const AlertsPanel: FC = () => { query: alertsQuery, showExpandToDetails: true, showAlertStatusWithFlapping: true, + cellContext: { + fieldFormats, + }, }; const alertsStateTable = triggersActionsUi!.getAlertsStateTable(alertStateProps); diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_alerts_page_table_configuration.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_alerts_page_table_configuration.tsx index b263a683be35e..cabf1d6d6f34e 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_alerts_page_table_configuration.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_alerts_page_table_configuration.tsx @@ -37,11 +37,7 @@ export const getAlertsPageTableConfiguration = ( id: observabilityFeatureId, cases: { featureId: casesFeatureId, owner: [observabilityFeatureId] }, columns: getColumns({ showRuleName: true }), - getRenderCellValue: ({ setFlyoutAlert }) => - getRenderCellValue({ - observabilityRuleTypeRegistry, - setFlyoutAlert, - }), + getRenderCellValue, sort: [ { [ALERT_START]: { diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.test.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.test.tsx index 91c6e8d2c3b4a..d551f90f1097f 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.test.tsx @@ -7,7 +7,6 @@ import { ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; import type { DeprecatedCellValueElementProps } from '@kbn/timelines-plugin/common'; -import { createObservabilityRuleTypeRegistryMock } from '../../../rules/observability_rule_type_registry_mock'; import { render } from '../../../utils/test_helper'; import { getRenderCellValue } from './render_cell_value'; @@ -16,17 +15,10 @@ interface AlertsTableRow { } describe('getRenderCellValue', () => { - const observabilityRuleTypeRegistryMock = createObservabilityRuleTypeRegistryMock(); - - const renderCellValue = getRenderCellValue({ - setFlyoutAlert: jest.fn(), - observabilityRuleTypeRegistry: observabilityRuleTypeRegistryMock, - }); - describe('when column is alert status', () => { it('should return an active indicator when alert status is active', async () => { const cell = render( - renderCellValue({ + getRenderCellValue({ ...requiredProperties, columnId: ALERT_STATUS, data: makeAlertsTableRow({ alertStatus: ALERT_STATUS_ACTIVE }), @@ -38,7 +30,7 @@ describe('getRenderCellValue', () => { it('should return a recovered indicator when alert status is recovered', async () => { const cell = render( - renderCellValue({ + getRenderCellValue({ ...requiredProperties, columnId: ALERT_STATUS, data: makeAlertsTableRow({ alertStatus: ALERT_STATUS_RECOVERED }), diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.tsx index 5e4091a3080b0..5ab79c6dfa141 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.tsx @@ -5,7 +5,6 @@ * 2.0. */ import { EuiLink } from '@elastic/eui'; -import { GetRenderCellValue } from '@kbn/triggers-actions-ui-plugin/public'; import React from 'react'; import { ALERT_DURATION, @@ -23,12 +22,11 @@ import { } from '@kbn/rule-data-utils'; import { isEmpty } from 'lodash'; import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; - +import type { ObservabilityRuleTypeRegistry } from '../../..'; import { asDuration } from '../../../../common/utils/formatters'; import { AlertSeverityBadge } from '../../alert_severity_badge'; import { AlertStatusIndicator } from '../../alert_status_indicator'; import { parseAlert } from '../../../pages/alerts/helpers/parse_alert'; -import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry'; import { CellTooltip } from './cell_tooltip'; import { TimestampTooltip } from './timestamp_tooltip'; @@ -71,63 +69,65 @@ const getRenderValue = (mappedNonEcsValue: any) => { */ export const getRenderCellValue = ({ + columnId, + data, setFlyoutAlert, observabilityRuleTypeRegistry, }: { - setFlyoutAlert: (alertId: string) => void; - observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; -}): ReturnType => { - return ({ columnId, data }) => { - if (!data) return null; - const mappedNonEcsValue = getMappedNonEcsValue({ - data, - fieldName: columnId, - }); - const value = getRenderValue(mappedNonEcsValue); - - switch (columnId) { - case ALERT_STATUS: - if (value !== ALERT_STATUS_ACTIVE && value !== ALERT_STATUS_RECOVERED) { - // NOTE: This should only be needed to narrow down the type. - // Status should be either "active" or "recovered". - return null; - } - return ; - case TIMESTAMP: - return ; - case ALERT_DURATION: - return asDuration(Number(value)); - case ALERT_SEVERITY: - return ; - case ALERT_EVALUATION_VALUE: - const valuesField = getMappedNonEcsValue({ - data, - fieldName: ALERT_EVALUATION_VALUES, - }); - const values = getRenderValue(valuesField); - return valuesField ? values : value; - case ALERT_REASON: - const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); - const alert = parseAlert(observabilityRuleTypeRegistry)(dataFieldEs); + columnId: string; + data?: Array<{ field: string; value: any }>; + setFlyoutAlert?: (alertId: string) => void; + observabilityRuleTypeRegistry?: ObservabilityRuleTypeRegistry; +}) => { + if (!data) return null; + const mappedNonEcsValue = getMappedNonEcsValue({ + data, + fieldName: columnId, + }); + const value = getRenderValue(mappedNonEcsValue); - return ( - setFlyoutAlert && setFlyoutAlert(alert.fields[ALERT_UUID])} - > - {alert.reason} - - ); - case ALERT_RULE_NAME: - const ruleCategory = getMappedNonEcsValue({ - data, - fieldName: ALERT_RULE_CATEGORY, - }); - const tooltipContent = getRenderValue(ruleCategory); - return ; - default: - return <>{value}; - } - }; + switch (columnId) { + case ALERT_STATUS: + if (value !== ALERT_STATUS_ACTIVE && value !== ALERT_STATUS_RECOVERED) { + // NOTE: This should only be needed to narrow down the type. + // Status should be either "active" or "recovered". + return null; + } + return ; + case TIMESTAMP: + return ; + case ALERT_DURATION: + return asDuration(Number(value)); + case ALERT_SEVERITY: + return ; + case ALERT_EVALUATION_VALUE: + const valuesField = getMappedNonEcsValue({ + data, + fieldName: ALERT_EVALUATION_VALUES, + }); + const values = getRenderValue(valuesField); + return valuesField ? values : value; + case ALERT_REASON: + const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); + if (!observabilityRuleTypeRegistry) return <>{value}; + const alert = parseAlert(observabilityRuleTypeRegistry)(dataFieldEs); + return ( + setFlyoutAlert && setFlyoutAlert(alert.fields[ALERT_UUID])} + > + {alert.reason} + + ); + case ALERT_RULE_NAME: + const ruleCategory = getMappedNonEcsValue({ + data, + fieldName: ALERT_RULE_CATEGORY, + }); + const tooltipContent = getRenderValue(ruleCategory); + return ; + default: + return <>{value}; + } }; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/rule_details/get_rule_details_table_configuration.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/rule_details/get_rule_details_table_configuration.tsx index 0ea5cf279e93c..e65bd8f4cfad8 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/rule_details/get_rule_details_table_configuration.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/rule_details/get_rule_details_table_configuration.tsx @@ -38,11 +38,7 @@ export const getRuleDetailsTableConfiguration = ( id: RULE_DETAILS_ALERTS_TABLE_CONFIG_ID, cases: { featureId: casesFeatureId, owner: [observabilityFeatureId] }, columns: getColumns(), - getRenderCellValue: ({ setFlyoutAlert }) => - getRenderCellValue({ - observabilityRuleTypeRegistry, - setFlyoutAlert, - }), + getRenderCellValue, sort: [ { [ALERT_START]: { diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/slo/get_slo_alerts_table_configuration.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/slo/get_slo_alerts_table_configuration.tsx index 80c60169253c7..3748047398d36 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/slo/get_slo_alerts_table_configuration.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/slo/get_slo_alerts_table_configuration.tsx @@ -24,11 +24,7 @@ export const getSloAlertsTableConfiguration = ( id: SLO_ALERTS_TABLE_CONFIG_ID, cases: { featureId: casesFeatureId, owner: [observabilityFeatureId] }, columns, - getRenderCellValue: ({ setFlyoutAlert }) => - getRenderCellValue({ - observabilityRuleTypeRegistry, - setFlyoutAlert, - }), + getRenderCellValue, sort: [ { [ALERT_DURATION]: { diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx index b145dd92c300d..def57b3b4bd1b 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx @@ -67,7 +67,7 @@ function InternalAlertsPage() { }, uiSettings, } = kibanaServices; - const { ObservabilityPageTemplate } = usePluginContext(); + const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); const alertSearchBarStateProps = useAlertSearchBarStateContainer(ALERTS_URL_STORAGE_KEY, { replace: false, }); @@ -249,6 +249,7 @@ function InternalAlertsPage() { query={esQuery} showAlertStatusWithFlapping pageSize={ALERTS_PER_PAGE} + cellContext={{ observabilityRuleTypeRegistry }} /> )} diff --git a/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx b/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx index 19c0b0acacf39..d4cfba510fc46 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx @@ -54,7 +54,7 @@ export function OverviewPage() { kibanaVersion, } = useKibana().services; - const { ObservabilityPageTemplate } = usePluginContext(); + const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); useBreadcrumbs([ { @@ -244,6 +244,7 @@ export function OverviewPage() { pageSize={ALERTS_PER_PAGE} query={esQuery} showAlertStatusWithFlapping + cellContext={{ observabilityRuleTypeRegistry }} /> diff --git a/x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/rule_details_tabs.tsx b/x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/rule_details_tabs.tsx index f5e514d57a68a..095c5796946fd 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/rule_details_tabs.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/rule_details_tabs.tsx @@ -19,6 +19,7 @@ import type { AlertConsumers } from '@kbn/rule-data-utils'; import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import type { Query, BoolQuery } from '@kbn/es-query'; import { useKibana } from '../../../utils/kibana_react'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; import { ObservabilityAlertSearchbarWithUrlSync } from '../../../components/alert_search_bar/alert_search_bar_with_url_sync'; import { RULE_DETAILS_ALERTS_TABLE_CONFIG_ID } from '../../../constants'; import { @@ -62,6 +63,7 @@ export function RuleDetailsTabs({ getRuleEventLogList: RuleEventLogList, }, } = useKibana().services; + const { observabilityRuleTypeRegistry } = usePluginContext(); const ruleQuery = useRef([ { query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' }, @@ -96,6 +98,7 @@ export function RuleDetailsTabs({ featureIds={featureIds} query={esQuery} showAlertStatusWithFlapping + cellContext={{ observabilityRuleTypeRegistry }} /> )} diff --git a/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.ts b/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.ts index 2a2926f3f6a27..c9409252277e0 100644 --- a/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.ts +++ b/x-pack/plugins/security_solution/public/actions/toggle_column/cell_action/toggle_column.ts @@ -72,7 +72,7 @@ export const createToggleColumnCellActionFactory = createCellActionFactory( if (alertTableConfigurationId) { services.triggersActionsUi.alertsTableConfigurationRegistry .getActions(alertTableConfigurationId) - .toggleColumn(field.name); + ?.toggleColumn(field.name); return; } diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx index 881658c521a8e..7db76c66e53df 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx @@ -120,9 +120,9 @@ const ActionsComponent: React.FC = ({ const isDisabled = !useIsInvestigateInResolverActionEnabled(ecsData); const { setGlobalFullScreen } = useGlobalFullScreen(); const { setTimelineFullScreen } = useTimelineFullScreen(); - const scopedActions = getScopedActions(timelineId); const handleClick = useCallback(() => { startTransaction({ name: ALERTS_ACTIONS.OPEN_ANALYZER }); + const scopedActions = getScopedActions(timelineId); const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); if (scopedActions) { @@ -140,7 +140,6 @@ const ActionsComponent: React.FC = ({ } }, [ startTransaction, - scopedActions, timelineId, dispatch, ecsData._id, @@ -176,6 +175,7 @@ const ActionsComponent: React.FC = ({ const openSessionView = useCallback(() => { const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); startTransaction({ name: ALERTS_ACTIONS.OPEN_SESSION_VIEW }); + const scopedActions = getScopedActions(timelineId); if (timelineId === TimelineId.active) { if (dataGridIsFullScreen) { @@ -201,7 +201,6 @@ const ActionsComponent: React.FC = ({ setTimelineFullScreen, dispatch, setGlobalFullScreen, - scopedActions, ]); const { activeStep, isTourShown, incrementStep } = useTourContext(); diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx index 4e9aa8452877c..6088c8587a9fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, memo } from 'react'; import type { EuiDataGridSorting, EuiDataGridSchemaDetector } from '@elastic/eui'; -import { EuiButtonIcon, EuiCheckbox, EuiToolTip, useDataGridColumnSorting } from '@elastic/eui'; +import { EuiButtonIcon, EuiToolTip, useDataGridColumnSorting, EuiCheckbox } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; @@ -68,223 +68,226 @@ const ActionsContainer = styled.div` const emptySchema = {}; const emptySchemaDetectors: EuiDataGridSchemaDetector[] = []; -const HeaderActionsComponent: React.FC = ({ - width, - browserFields, - columnHeaders, - isEventViewer = false, - isSelectAllChecked, - onSelectAll, - showEventsSelect, - showSelectAllCheckbox, - sort, - tabType, - timelineId, - fieldBrowserOptions, -}) => { - const { triggersActionsUi } = useKibana().services; - const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); - const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); - const dispatch = useDispatch(); - - const getManageTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId)); - - const toggleFullScreen = useCallback(() => { - if (timelineId === TimelineId.active) { - setTimelineFullScreen(!timelineFullScreen); - } else { - setGlobalFullScreen(!globalFullScreen); - } - }, [ +const HeaderActionsComponent: React.FC = memo( + ({ + width, + browserFields, + columnHeaders, + isEventViewer = false, + isSelectAllChecked, + onSelectAll, + showEventsSelect, + showSelectAllCheckbox, + sort, + tabType, timelineId, - setTimelineFullScreen, - timelineFullScreen, - setGlobalFullScreen, - globalFullScreen, - ]); - - const fullScreen = useMemo( - () => - isFullScreen({ - globalFullScreen, - isActiveTimelines: isActiveTimeline(timelineId), - timelineFullScreen, - }), - [globalFullScreen, timelineFullScreen, timelineId] - ); - const handleSelectAllChange = useCallback( - (event: React.ChangeEvent) => { - onSelectAll({ isSelected: event.currentTarget.checked }); - }, - [onSelectAll] - ); - - const onSortColumns = useCallback( - (cols: EuiDataGridSorting['columns']) => - dispatch( - timelineActions.updateSort({ - id: timelineId, - sort: cols.map(({ id, direction }) => { - const columnHeader = columnHeaders.find((ch) => ch.id === id); - const columnType = columnHeader?.type ?? ''; - const esTypes = columnHeader?.esTypes ?? []; + fieldBrowserOptions, + }) => { + const { triggersActionsUi } = useKibana().services; + const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); + const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); + const dispatch = useDispatch(); - return { - columnId: id, - columnType, - esTypes, - sortDirection: direction as SortDirection, - }; - }), - }) - ), - [columnHeaders, dispatch, timelineId] - ); - - const sortedColumns = useMemo( - () => ({ - onSort: onSortColumns, - columns: - sort?.map<{ id: string; direction: 'asc' | 'desc' }>(({ columnId, sortDirection }) => ({ - id: columnId, - direction: sortDirection as 'asc' | 'desc', - })) ?? [], - }), - [onSortColumns, sort] - ); - const displayValues = useMemo( - () => - columnHeaders?.reduce((acc, ch) => ({ ...acc, [ch.id]: ch.displayAsText ?? ch.id }), {}) ?? - {}, - [columnHeaders] - ); + const getManageTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { defaultColumns } = useDeepEqualSelector((state) => + getManageTimeline(state, timelineId) + ); - const myColumns = useMemo( - () => - columnHeaders?.map(({ aggregatable, displayAsText, id, type }) => ({ - id, - isSortable: aggregatable, - displayAsText, - schema: type, - })) ?? [], - [columnHeaders] - ); + const toggleFullScreen = useCallback(() => { + if (timelineId === TimelineId.active) { + setTimelineFullScreen(!timelineFullScreen); + } else { + setGlobalFullScreen(!globalFullScreen); + } + }, [ + timelineId, + setTimelineFullScreen, + timelineFullScreen, + setGlobalFullScreen, + globalFullScreen, + ]); - const onResetColumns = useCallback(() => { - dispatch(timelineActions.updateColumns({ id: timelineId, columns: defaultColumns })); - }, [defaultColumns, dispatch, timelineId]); + const fullScreen = useMemo( + () => + isFullScreen({ + globalFullScreen, + isActiveTimelines: isActiveTimeline(timelineId), + timelineFullScreen, + }), + [globalFullScreen, timelineFullScreen, timelineId] + ); + const handleSelectAllChange = useCallback( + (event: React.ChangeEvent) => { + onSelectAll({ isSelected: event.currentTarget.checked }); + }, + [onSelectAll] + ); - const onToggleColumn = useCallback( - (columnId: string) => { - if (columnHeaders.some(({ id }) => id === columnId)) { - dispatch( - timelineActions.removeColumn({ - columnId, - id: timelineId, - }) - ); - } else { + const onSortColumns = useCallback( + (cols: EuiDataGridSorting['columns']) => dispatch( - timelineActions.upsertColumn({ - column: getColumnHeader(columnId, defaultColumns), + timelineActions.updateSort({ id: timelineId, - index: 1, + sort: cols.map(({ id, direction }) => { + const columnHeader = columnHeaders.find((ch) => ch.id === id); + const columnType = columnHeader?.type ?? ''; + const esTypes = columnHeader?.esTypes ?? []; + + return { + columnId: id, + columnType, + esTypes, + sortDirection: direction as SortDirection, + }; + }), }) - ); - } - }, - [columnHeaders, dispatch, timelineId, defaultColumns] - ); + ), + [columnHeaders, dispatch, timelineId] + ); - const ColumnSorting = useDataGridColumnSorting({ - columns: myColumns, - sorting: sortedColumns, - schema: emptySchema, - schemaDetectors: emptySchemaDetectors, - displayValues, - }); + const sortedColumns = useMemo( + () => ({ + onSort: onSortColumns, + columns: + sort?.map<{ id: string; direction: 'asc' | 'desc' }>(({ columnId, sortDirection }) => ({ + id: columnId, + direction: sortDirection as 'asc' | 'desc', + })) ?? [], + }), + [onSortColumns, sort] + ); + const displayValues = useMemo( + () => + columnHeaders?.reduce((acc, ch) => ({ ...acc, [ch.id]: ch.displayAsText ?? ch.id }), {}) ?? + {}, + [columnHeaders] + ); - return ( - - {showSelectAllCheckbox && ( - - - - - - )} + const myColumns = useMemo( + () => + columnHeaders?.map(({ aggregatable, displayAsText, id, type }) => ({ + id, + isSortable: aggregatable, + displayAsText, + schema: type, + })) ?? [], + [columnHeaders] + ); - - - {triggersActionsUi.getFieldBrowser({ - browserFields, - columnIds: columnHeaders.map(({ id }) => id), - onResetColumns, - onToggleColumn, - options: fieldBrowserOptions, - })} - - + const onResetColumns = useCallback(() => { + dispatch(timelineActions.updateColumns({ id: timelineId, columns: defaultColumns })); + }, [defaultColumns, dispatch, timelineId]); - - - + const onToggleColumn = useCallback( + (columnId: string) => { + if (columnHeaders.some(({ id }) => id === columnId)) { + dispatch( + timelineActions.removeColumn({ + columnId, + id: timelineId, + }) + ); + } else { + dispatch( + timelineActions.upsertColumn({ + column: getColumnHeader(columnId, defaultColumns), + id: timelineId, + index: 1, + }) + ); + } + }, + [columnHeaders, dispatch, timelineId, defaultColumns] + ); - - - - - - - - {tabType !== TimelineTabs.eql && ( - - - - {ColumnSorting} - - + const ColumnSorting = useDataGridColumnSorting({ + columns: myColumns, + sorting: sortedColumns, + schema: emptySchema, + schemaDetectors: emptySchemaDetectors, + displayValues, + }); + + return ( + + {showSelectAllCheckbox && ( + + + + + + )} + + + {triggersActionsUi.getFieldBrowser({ + browserFields, + columnIds: columnHeaders.map(({ id }) => id), + onResetColumns, + onToggleColumn, + options: fieldBrowserOptions, + })} + + + + + - )} - {showEventsSelect && ( - + + + - )} - - ); -}; + {tabType !== TimelineTabs.eql && ( + + + + {ColumnSorting} + + + + )} + + {showEventsSelect && ( + + + + + + )} + + ); + } +); HeaderActionsComponent.displayName = 'HeaderActionsComponent'; export const HeaderActions = React.memo(HeaderActionsComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/alert_bulk_tags.tsx b/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/alert_bulk_tags.tsx index bdb5c1d2d5f01..67ba6562abc23 100644 --- a/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/alert_bulk_tags.tsx +++ b/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/alert_bulk_tags.tsx @@ -8,7 +8,7 @@ import type { EuiSelectableOption } from '@elastic/eui'; import { EuiPopoverTitle, EuiSelectable, EuiButton } from '@elastic/eui'; import type { TimelineItem } from '@kbn/timelines-plugin/common'; -import React, { memo, useCallback, useMemo, useReducer } from 'react'; +import React, { memo, useCallback, useMemo, useReducer, useEffect } from 'react'; import { ALERT_WORKFLOW_TAGS } from '@kbn/rule-data-utils'; import type { EuiSelectableOnChangeEvent } from '@elastic/eui/src/components/selectable/selectable'; import { DEFAULT_ALERT_TAGS_KEY } from '../../../../../common/constants'; @@ -27,6 +27,7 @@ interface BulkAlertTagsPanelComponentProps { closePopoverMenu: () => void; onSubmit: SetAlertTagsFunc; } + const BulkAlertTagsPanelComponent: React.FC = ({ alertItems, refresh, @@ -47,12 +48,7 @@ const BulkAlertTagsPanelComponent: React.FC = ); const [{ selectableAlertTags, tagsToAdd, tagsToRemove }, dispatch] = useReducer( createAlertTagsReducer(), - { - ...initialState, - selectableAlertTags: createInitialTagsState(existingTags, defaultAlertTagOptions), - tagsToAdd: new Set(), - tagsToRemove: new Set(), - } + initialState ); const addAlertTag = useCallback( @@ -122,6 +118,13 @@ const BulkAlertTagsPanelComponent: React.FC = [addAlertTag, removeAlertTag, setSelectableAlertTags] ); + useEffect(() => { + dispatch({ + type: 'setSelectableAlertTags', + value: createInitialTagsState(existingTags, defaultAlertTagOptions), + }); + }, [existingTags, defaultAlertTagOptions]); + return ( <> { + return { + alertAssigneesItems, + alertAssigneesPanels, + }; + }, [alertAssigneesItems, alertAssigneesPanels]); }; diff --git a/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_bulk_alert_tags_items.tsx b/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_bulk_alert_tags_items.tsx index 977cb0bf8b315..c155d39e2a3a5 100644 --- a/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_bulk_alert_tags_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_bulk_alert_tags_items.tsx @@ -101,8 +101,10 @@ export const useBulkAlertTagsItems = ({ refetch }: UseBulkAlertTagsItemsProps) = [TitleContent, hasIndexWrite, renderContent] ); - return { - alertTagsItems, - alertTagsPanels, - }; + return useMemo(() => { + return { + alertTagsItems, + alertTagsPanels, + }; + }, [alertTagsItems, alertTagsPanels]); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 0ab8ba6a624f1..599c32414d966 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -26,6 +26,7 @@ import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useLicense } from '../../../common/hooks/use_license'; import { VIEW_SELECTION } from '../../../../common/constants'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; import { @@ -255,6 +256,15 @@ export const AlertsTableComponent: FC = ({ [dispatch, tableId, alertTableRefreshHandlerRef, setQuery] ); + const cellContext = useMemo(() => { + return { + rowRenderers: defaultRowRenderers, + isDetails: false, + truncate: true, + isDraggable: false, + }; + }, []); + const alertStateProps: AlertsTableStateProps = useMemo( () => ({ alertsTableConfigurationRegistry: triggersActionsUi.alertsTableConfigurationRegistry, @@ -269,6 +279,7 @@ export const AlertsTableComponent: FC = ({ columns: finalColumns, browserFields: finalBrowserFields, onUpdate: onAlertTableUpdate, + cellContext, runtimeMappings, toolbarVisibility: { showColumnSelector: !isEventRenderedView, @@ -288,6 +299,7 @@ export const AlertsTableComponent: FC = ({ onAlertTableUpdate, runtimeMappings, isEventRenderedView, + cellContext, ] ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx index 28ddfe1e12fc1..4c894918c593e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx @@ -80,24 +80,28 @@ export const useAddBulkToTimelineAction = ({ const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); - const timelineQuerySortField = sort.map(({ columnId, columnType, esTypes, sortDirection }) => ({ - field: columnId, - direction: sortDirection as Direction, - esTypes: esTypes ?? [], - type: columnType, - })); + const timelineQuerySortField = useMemo(() => { + return sort.map(({ columnId, columnType, esTypes, sortDirection }) => ({ + field: columnId, + direction: sortDirection as Direction, + esTypes: esTypes ?? [], + type: columnType, + })); + }, [sort]); const combinedFilters = useMemo(() => [...localFilters, ...filters], [localFilters, filters]); - const combinedQuery = combineQueries({ - config: esQueryConfig, - dataProviders: [], - indexPattern, - filters: combinedFilters, - kqlQuery: { query: '', language: 'kuery' }, - browserFields, - kqlMode: 'filter', - }); + const combinedQuery = useMemo(() => { + return combineQueries({ + config: esQueryConfig, + dataProviders: [], + indexPattern, + filters: combinedFilters, + kqlQuery: { query: '', language: 'kuery' }, + browserFields, + kqlMode: 'filter', + }); + }, [esQueryConfig, indexPattern, combinedFilters, browserFields]); const filterQuery = useMemo(() => { if (!combinedQuery) return ''; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx index de3c8782722fa..0fa09d4bf4354 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx @@ -65,7 +65,7 @@ export const useAddToCaseActions = ({ const { activeStep, incrementStep, setStep, isTourShown } = useTourContext(); - const onCaseSuccess = () => { + const onCaseSuccess = useCallback(() => { if (onSuccess) { onSuccess(); } @@ -73,7 +73,7 @@ export const useAddToCaseActions = ({ if (refetch) { refetch(); } - }; + }, [onSuccess, refetch]); const afterCaseCreated = useCallback(async () => { if (isTourShown(SecurityStepId.alertsCases)) { @@ -92,17 +92,25 @@ export const useAddToCaseActions = ({ [activeStep, isTourShown] ); - const createCaseFlyout = casesUi.hooks.useCasesAddToNewCaseFlyout({ - onClose: onMenuItemClick, - onSuccess: onCaseSuccess, - afterCaseCreated, - ...prefillCasesValue, - }); - - const selectCaseModal = casesUi.hooks.useCasesAddToExistingCaseModal({ - onClose: onMenuItemClick, - onSuccess: onCaseSuccess, - }); + const createCaseArgs = useMemo(() => { + return { + onClose: onMenuItemClick, + onSuccess: onCaseSuccess, + afterCaseCreated, + ...prefillCasesValue, + }; + }, [onMenuItemClick, onCaseSuccess, afterCaseCreated, prefillCasesValue]); + + const createCaseFlyout = casesUi.hooks.useCasesAddToNewCaseFlyout(createCaseArgs); + + const selectCaseArgs = useMemo(() => { + return { + onClose: onMenuItemClick, + onSuccess: onCaseSuccess, + }; + }, [onMenuItemClick, onCaseSuccess]); + + const selectCaseModal = casesUi.hooks.useCasesAddToExistingCaseModal(selectCaseArgs); const handleAddToNewCaseClick = useCallback(() => { // TODO rename this, this is really `closePopover()` diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx index c435333486f4d..7d21c428a33b8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import type { AlertWorkflowStatus } from '../../../../common/types'; @@ -58,16 +58,22 @@ export const useAlertsActions = ({ [dispatch, scopeId, scopedActions] ); - const actionItems = useBulkActionItems({ - eventIds: [eventId], - currentStatus: alertStatus as AlertWorkflowStatus, - setEventsLoading: localSetEventsLoading, - setEventsDeleted, - onUpdateSuccess: onStatusUpdate, - onUpdateFailure: onStatusUpdate, - }); + const eventIds = useMemo(() => [eventId], [eventId]); - return { - actionItems: hasIndexWrite ? actionItems : [], - }; + const actionItemArgs = useMemo(() => { + return { + eventIds, + currentStatus: alertStatus as AlertWorkflowStatus, + setEventsLoading: localSetEventsLoading, + setEventsDeleted, + onUpdateSuccess: onStatusUpdate, + onUpdateFailure: onStatusUpdate, + }; + }, [alertStatus, eventIds, localSetEventsLoading, onStatusUpdate, setEventsDeleted]); + + const actionItems = useBulkActionItems(actionItemArgs); + + return useMemo(() => { + return { actionItems: hasIndexWrite ? actionItems : [] }; + }, [actionItems, hasIndexWrite]); }; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/fetch_page_context.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/fetch_page_context.tsx index ebd8df15d92ea..45614aad4e96d 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/fetch_page_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/fetch_page_context.tsx @@ -25,19 +25,21 @@ export const useFetchPageContext: PreFetchPageContext = alerts, columns, }) => { - const uids = new Set(); - alerts.forEach((alert) => { - profileUidColumns.forEach((columnId) => { - if (columns.find((column) => column.id === columnId) != null) { - const userUids = alert[columnId]; - userUids?.forEach((uid) => uids.add(uid as string)); - } + const uids = useMemo(() => { + const ids = new Set(); + alerts.forEach((alert) => { + profileUidColumns.forEach((columnId) => { + if (columns.find((column) => column.id === columnId) != null) { + const userUids = alert[columnId]; + userUids?.forEach((uid) => ids.add(uid as string)); + } + }); }); - }); + return ids; + }, [alerts, columns]); const result = useBulkGetUserProfiles({ uids }); - const returnVal = useMemo( + return useMemo( () => ({ profiles: result.data, isLoading: result.isLoading }), [result.data, result.isLoading] ); - return returnVal; }; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx index e2e38b2390309..20afb552e045a 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx @@ -8,18 +8,27 @@ import { mount } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; - +import { TableId } from '@kbn/securitysolution-data-table'; import type { ColumnHeaderOptions } from '../../../../common/types'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { DragDropContextWrapper } from '../../../common/components/drag_and_drop/drag_drop_context_wrapper'; import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import type { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import type { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; - -import { RenderCellValue } from '.'; +import { getRenderCellValueHook } from './render_cell_value'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + browserFields: {}, + defaultIndex: 'defaultIndex', + loading: false, + indicesExist: true, + }), +})); describe('RenderCellValue', () => { const columnId = '@timestamp'; @@ -49,10 +58,20 @@ describe('RenderCellValue', () => { colIndex: 0, setCellProps: jest.fn(), scopeId, + rowRenderers: defaultRowRenderers, + asPlainText: false, + ecsData: undefined, + truncate: undefined, + context: undefined, + browserFields: {}, }; }); test('it forwards the `CellValueElementProps` to the `DefaultCellRenderer`', () => { + const RenderCellValue = getRenderCellValueHook({ + scopeId: SourcererScopeName.default, + tableId: TableId.test, + }); const wrapper = mount( @@ -61,6 +80,6 @@ describe('RenderCellValue', () => { ); - expect(wrapper.find(DefaultCellRenderer).first().props()).toEqual(props); + expect(wrapper.find(DefaultCellRenderer).props()).toEqual(props); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx index 84e14ad725e40..752675857ace2 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx @@ -5,16 +5,15 @@ * 2.0. */ -import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; import { EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import type { GetRenderCellValue } from '@kbn/triggers-actions-ui-plugin/public'; +import type { EuiDataGridCellProps } from '@elastic/eui'; +import React, { useMemo, memo } from 'react'; import { find, getOr } from 'lodash/fp'; import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; import { tableDefaults, dataTableSelectors } from '@kbn/securitysolution-data-table'; import type { TableId } from '@kbn/securitysolution-data-table'; import { useLicense } from '../../../common/hooks/use_license'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import type { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step'; @@ -25,137 +24,110 @@ import { } from '../../../common/components/guided_onboarding_tour/tour_config'; import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; - -import type { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { SUPPRESSED_ALERT_TOOLTIP } from './translations'; import { VIEW_SELECTION } from '../../../../common/constants'; import { getAllFieldsByName } from '../../../common/containers/source'; import { eventRenderedViewColumns, getColumns } from './columns'; -import type { RenderCellValueContext } from './fetch_page_context'; /** * This implementation of `EuiDataGrid`'s `renderCellValue` * accepts `EuiDataGridCellValueElementProps`, plus `data` * from the TGrid */ -export const RenderCellValue: React.FC = ( - props -) => { - const { columnId, rowIndex, scopeId } = props; - const isTourAnchor = useMemo( - () => - columnId === SIGNAL_RULE_NAME_FIELD_NAME && - isDetectionsAlertsTable(scopeId) && - rowIndex === 0 && - !props.isDetails, - [columnId, props.isDetails, rowIndex, scopeId] - ); - - // We check both ecsData and data for the suppression count because it could be in either one, - // depending on where RenderCellValue is being used - when used in cases, data is populated, - // whereas in the regular security alerts table it's in ecsData - const ecsSuppressionCount = props.ecsData?.kibana?.alert.suppression?.docs_count?.[0]; - const dataSuppressionCount = find({ field: 'kibana.alert.suppression.docs_count' }, props.data) - ?.value?.[0] as number | undefined; - const actualSuppressionCount = ecsSuppressionCount - ? parseInt(ecsSuppressionCount, 10) - : dataSuppressionCount; - const component = ( - - - - ); - - return columnId === SIGNAL_RULE_NAME_FIELD_NAME && - actualSuppressionCount && - actualSuppressionCount > 0 ? ( - - - - - - - {component} - - ) : ( - component - ); -}; - -export const getRenderCellValueHook = ({ - scopeId, - tableId, -}: { - scopeId: SourcererScopeName; - tableId: TableId; -}) => { - const useRenderCellValue: GetRenderCellValue = ({ context }) => { +export const RenderCellValue: React.FC = memo( + function RenderCellValue(props) { + const { + columnId, + rowIndex, + scopeId, + tableId, + header, + data, + ecsData, + linkValues, + rowRenderers, + isDetails, + isExpandable, + isDraggable, + isExpanded, + colIndex, + eventId, + setCellProps, + truncate, + context, + } = props; + const isTourAnchor = useMemo( + () => + columnId === SIGNAL_RULE_NAME_FIELD_NAME && + isDetectionsAlertsTable(tableId) && + rowIndex === 0 && + !props.isDetails, + [columnId, props.isDetails, rowIndex, tableId] + ); const { browserFields } = useSourcererDataView(scopeId); const browserFieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]); const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []); const license = useLicense(); - const viewMode = - useShallowEqualSelector((state) => (getTable(state, tableId) ?? tableDefaults).viewMode) ?? + useDeepEqualSelector((state) => (getTable(state, tableId) ?? tableDefaults).viewMode) ?? tableDefaults.viewMode; - const columnHeaders = - viewMode === VIEW_SELECTION.gridView ? getColumns(license) : eventRenderedViewColumns; - - const result = useCallback( - ({ - columnId, - colIndex, - data, - ecsData, - eventId, - header, - isDetails = false, - isDraggable = false, - isExpandable, - isExpanded, - rowIndex, - rowRenderers, - setCellProps, - linkValues, - truncate = true, - }) => { - const myHeader = header ?? { id: columnId, ...browserFieldsByName[columnId] }; - /** - * There is difference between how `triggers actions` fetched data v/s - * how security solution fetches data via timelineSearchStrategy - * - * _id and _index fields are array in timelineSearchStrategy but not in - * ruleStrategy - * - * - */ - - const finalData = (data as TimelineNonEcsData[]).map((field) => { - let localField = field; - if (['_id', '_index'].includes(field.field)) { - const newValue = field.value ?? ''; - localField = { - field: field.field, - value: Array.isArray(newValue) ? newValue : [newValue], - }; - } - return localField; - }); - - const colHeader = columnHeaders.find((col) => col.id === columnId); - - const localLinkValues = getOr([], colHeader?.linkField ?? '', ecsData); - - return ( - { + return getColumns(license); + }, [license]); + + const columnHeaders = useMemo(() => { + return viewMode === VIEW_SELECTION.gridView ? gridColumns : eventRenderedViewColumns; + }, [gridColumns, viewMode]); + + /** + * There is difference between how `triggers actions` fetched data v/s + * how security solution fetches data via timelineSearchStrategy + * + * _id and _index fields are array in timelineSearchStrategy but not in + * ruleStrategy + * + * + */ + + const finalData = useMemo(() => { + return (data as TimelineNonEcsData[]).map((field) => { + if (['_id', '_index'].includes(field.field)) { + const newValue = field.value ?? ''; + return { + field: field.field, + value: Array.isArray(newValue) ? newValue : [newValue], + }; + } else { + return field; + } + }); + }, [data]); + + const actualSuppressionCount = useMemo(() => { + // We check both ecsData and data for the suppression count because it could be in either one, + // depending on where RenderCellValue is being used - when used in cases, data is populated, + // whereas in the regular security alerts table it's in ecsData + const ecsSuppressionCount = ecsData?.kibana?.alert.suppression?.docs_count?.[0]; + const dataSuppressionCount = find({ field: 'kibana.alert.suppression.docs_count' }, data) + ?.value?.[0] as number | undefined; + return ecsSuppressionCount ? parseInt(ecsSuppressionCount, 10) : dataSuppressionCount; + }, [ecsData, data]); + + const Renderer = useMemo(() => { + const myHeader = header ?? { id: columnId, ...browserFieldsByName[columnId] }; + const colHeader = columnHeaders.find((col) => col.id === columnId); + const localLinkValues = getOr([], colHeader?.linkField ?? '', ecsData); + return ( + + - ); - }, - [browserFieldsByName, columnHeaders, browserFields, context] + + ); + }, [ + isTourAnchor, + finalData, + browserFieldsByName, + header, + columnId, + ecsData, + linkValues, + rowRenderers, + isDetails, + isExpandable, + isDraggable, + isExpanded, + colIndex, + eventId, + setCellProps, + truncate, + context, + tableId, + browserFields, + rowIndex, + columnHeaders, + ]); + + return columnId === SIGNAL_RULE_NAME_FIELD_NAME && actualSuppressionCount ? ( + + + + + + + {Renderer} + + ) : ( + <>{Renderer} ); - return result; - }; + } +); +export const getRenderCellValueHook = ({ + scopeId, + tableId, +}: { + scopeId: SourcererScopeName; + tableId: TableId; +}) => { + const useRenderCellValue = (props: EuiDataGridCellProps['cellContext']) => { + return ; + }; return useRenderCellValue; }; diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_alert_actions.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_alert_actions.tsx index decc2cb159b5c..8a45dfb45db74 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_alert_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_alert_actions.tsx @@ -6,7 +6,7 @@ */ import type { BulkActionsConfig } from '@kbn/triggers-actions-ui-plugin/public/types'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import type { Filter } from '@kbn/es-query'; import { buildEsQuery } from '@kbn/es-query'; import type { TableId } from '@kbn/securitysolution-data-table'; @@ -174,9 +174,11 @@ export const useBulkAlertActionItems = ({ [getOnAction] ); - return hasIndexWrite - ? [FILTER_OPEN, FILTER_CLOSED, FILTER_ACKNOWLEDGED].map((status) => - getUpdateAlertStatusAction(status as AlertWorkflowStatus) - ) - : []; + return useMemo(() => { + return hasIndexWrite + ? [FILTER_OPEN, FILTER_CLOSED, FILTER_ACKNOWLEDGED].map((status) => { + return getUpdateAlertStatusAction(status as AlertWorkflowStatus); + }) + : []; + }, [getUpdateAlertStatusAction, hasIndexWrite]); }; diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_bulk_actions.tsx index 0ea04d0df93f2..03e2655ff4047 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_bulk_actions.tsx @@ -10,13 +10,10 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type import type { SerializableRecord } from '@kbn/utility-types'; import { isEqual } from 'lodash'; import type { Filter } from '@kbn/es-query'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import type { TableId } from '@kbn/securitysolution-data-table'; import { useBulkAlertAssigneesItems } from '../../../common/components/toolbar/bulk_actions/use_bulk_alert_assignees_items'; import { useBulkAlertTagsItems } from '../../../common/components/toolbar/bulk_actions/use_bulk_alert_tags_items'; -import type { inputsModel, State } from '../../../common/store'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; -import { inputsSelectors } from '../../../common/store'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useAddBulkToTimelineAction } from '../../components/alerts_table/timeline_actions/use_add_bulk_to_timeline'; @@ -62,43 +59,56 @@ function getFiltersForDSLQuery(datafeedQuery: QueryDslQueryContainer): Filter[] export const getBulkActionHook = (tableId: TableId): AlertsTableConfigurationRegistry['useBulkActions'] => - (query) => { + (query, refresh) => { const { from, to } = useGlobalTime(); - const filters = getFiltersForDSLQuery(query); - const getGlobalQueries = useMemo(() => inputsSelectors.globalQuery(), []); + const filters = useMemo(() => { + return getFiltersForDSLQuery(query); + }, [query]); + const assigneeProps = useMemo(() => { + return { + onAssigneesUpdate: refresh, + }; + }, [refresh]); - const globalQuery = useShallowEqualSelector((state: State) => getGlobalQueries(state)); + const { alertAssigneesItems, alertAssigneesPanels } = useBulkAlertAssigneesItems(assigneeProps); - const refetchGlobalQuery = useCallback(() => { - globalQuery.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); - }, [globalQuery]); + const timelineActionParams = useMemo(() => { + return { + localFilters: filters, + from, + to, + scopeId: SourcererScopeName.detections, + tableId, + }; + }, [filters, from, to]); - const timelineAction = useAddBulkToTimelineAction({ - localFilters: filters, - from, - to, - scopeId: SourcererScopeName.detections, - tableId, - }); + const alertActionParams = useMemo(() => { + return { + scopeId: SourcererScopeName.detections, + filters, + from, + to, + tableId, + refetch: refresh, + }; + }, [from, to, filters, refresh]); - const alertActions = useBulkAlertActionItems({ - scopeId: SourcererScopeName.detections, - filters, - from, - to, - tableId, - refetch: refetchGlobalQuery, - }); + const bulkAlertTagParams = useMemo(() => { + return { + refetch: refresh, + }; + }, [refresh]); - const { alertTagsItems, alertTagsPanels } = useBulkAlertTagsItems({ - refetch: refetchGlobalQuery, - }); + const timelineAction = useAddBulkToTimelineAction(timelineActionParams); - const { alertAssigneesItems, alertAssigneesPanels } = useBulkAlertAssigneesItems({ - onAssigneesUpdate: refetchGlobalQuery, - }); + const alertActions = useBulkAlertActionItems(alertActionParams); - const items = [...alertActions, timelineAction, ...alertTagsItems, ...alertAssigneesItems]; + const { alertTagsItems, alertTagsPanels } = useBulkAlertTagsItems(bulkAlertTagParams); - return [{ id: 0, items }, ...alertTagsPanels, ...alertAssigneesPanels]; + const items = useMemo(() => { + return [...alertActions, timelineAction, ...alertTagsItems, ...alertAssigneesItems]; + }, [alertActions, alertTagsItems, timelineAction, alertAssigneesItems]); + return useMemo(() => { + return [{ id: 0, items }, ...alertTagsPanels, ...alertAssigneesPanels]; + }, [alertTagsPanels, items, alertAssigneesPanels]); }; diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx index a36678841a904..f1d1de98a0ea3 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_cell_actions.tsx @@ -107,10 +107,12 @@ export const getUseCellActionsHook = (tableId: TableId) => { [cellActions] ); - return { - getCellActions, - visibleCellActions: 3, - }; + return useMemo(() => { + return { + getCellActions, + visibleCellActions: 3, + }; + }, [getCellActions]); }; return useCellActions; diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx index 4f4db100f0d05..f662914c870e9 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx @@ -129,9 +129,7 @@ export const getPersistentControlsHook = (tableId: TableId) => { [tableView, handleChangeTableView, additionalFiltersComponent, groupSelector] ); - return { - right: rightTopMenu, - }; + return useMemo(() => ({ right: rightTopMenu }), [rightTopMenu]); }; return usePersistentControls; diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_trigger_actions_browser_fields_options.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_trigger_actions_browser_fields_options.tsx index 443dde3fd6ee8..c2140c904c464 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_trigger_actions_browser_fields_options.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_trigger_actions_browser_fields_options.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { useCallback, useMemo } from 'react'; import type { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types'; import { useFieldBrowserOptions } from '../../../timelines/components/fields_browser'; import type { SourcererScopeName } from '../../../common/store/sourcerer/model'; @@ -12,17 +12,26 @@ import type { SourcererScopeName } from '../../../common/store/sourcerer/model'; export const getUseTriggersActionsFieldBrowserOptions = (scopeId: SourcererScopeName) => { const useTriggersActionsFieldBrowserOptions: AlertsTableConfigurationRegistry['useFieldBrowserOptions'] = ({ onToggleColumn }) => { - const options = useFieldBrowserOptions({ - sourcererScope: scopeId, - removeColumn: onToggleColumn, - upsertColumn: (column) => { + const upsertColumn = useCallback( + (column) => { onToggleColumn(column.id); }, - }); + [onToggleColumn] + ); + const fieldBrowserArgs = useMemo(() => { + return { + sourcererScope: scopeId, + removeColumn: onToggleColumn, + upsertColumn, + }; + }, [upsertColumn, onToggleColumn]); + const options = useFieldBrowserOptions(fieldBrowserArgs); - return { - createFieldButton: options.createFieldButton, - }; + return useMemo(() => { + return { + createFieldButton: options.createFieldButton, + }; + }, [options.createFieldButton]); }; return useTriggersActionsFieldBrowserOptions; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.test.tsx index 4b6c3c053d0da..10088446c045c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { mockTimelineModel } from '../../../common/mock'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { mockTimelineModel, TestProviders } from '../../../common/mock'; import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../common/store/inputs/actions'; import { applyKqlFilterQuery as dispatchApplyKqlFilterQuery, @@ -17,7 +18,6 @@ import { updateNote as dispatchUpdateNote, } from '../../../common/store/app/actions'; import { useUpdateTimeline } from './use_update_timeline'; -import type { DispatchUpdateTimeline } from './types'; import type { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; @@ -66,7 +66,6 @@ describe('dispatchUpdateTimeline', () => { const anchor = '2020-03-27T20:34:51.337Z'; const unix = moment(anchor).valueOf(); let clock: sinon.SinonFakeTimers; - let timelineDispatch: DispatchUpdateTimeline; const defaultArgs = { duplicate: true, @@ -81,149 +80,196 @@ describe('dispatchUpdateTimeline', () => { jest.clearAllMocks(); clock = sinon.useFakeTimers(unix); - timelineDispatch = useUpdateTimeline(); }); afterEach(function () { clock.restore(); }); - test('it invokes date range picker dispatch', () => { - timelineDispatch(defaultArgs); + it('it invokes date range picker dispatch', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitForNextUpdate(); + result.current(defaultArgs); - expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ - from: '2020-03-26T14:35:56.356Z', - to: '2020-03-26T14:41:56.356Z', + expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', + }); }); }); - test('it invokes add timeline dispatch', () => { - timelineDispatch(defaultArgs); + it('it invokes add timeline dispatch', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitForNextUpdate(); + result.current(defaultArgs); - expect(dispatchAddTimeline).toHaveBeenCalledWith({ - id: TimelineId.active, - savedTimeline: true, - timeline: { - ...mockTimelineModel, - version: null, - updated: undefined, - changed: undefined, - }, + expect(dispatchAddTimeline).toHaveBeenCalledWith({ + id: TimelineId.active, + savedTimeline: true, + timeline: { + ...mockTimelineModel, + version: null, + updated: undefined, + changed: undefined, + }, + }); }); }); - test('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', () => { - timelineDispatch(defaultArgs); + it('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitForNextUpdate(); + result.current(defaultArgs); - expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + }); }); - test('it does not invoke notes dispatch if duplicate is true', () => { - timelineDispatch(defaultArgs); + it('it does not invoke notes dispatch if duplicate is true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitForNextUpdate(); + result.current(defaultArgs); - expect(dispatchAddNotes).not.toHaveBeenCalled(); + expect(dispatchAddNotes).not.toHaveBeenCalled(); + }); }); - test('it does not invoke kql filter query dispatches if timeline.kqlQuery.kuery is null', () => { - const mockTimeline = { - ...mockTimelineModel, - kqlQuery: { - filterQuery: { - kuery: null, - serializedQuery: 'some-serialized-query', + it('it does not invoke kql filter query dispatches if timeline.kqlQuery.kuery is null', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitForNextUpdate(); + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: null, + serializedQuery: 'some-serialized-query', + }, }, - }, - }; - timelineDispatch({ - ...defaultArgs, - timeline: mockTimeline, - }); + }; + result.current({ + ...defaultArgs, + timeline: mockTimeline, + }); - expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + }); }); - test('it invokes kql filter query dispatches if timeline.kqlQuery.filterQuery.kuery is not null', () => { - const mockTimeline = { - ...mockTimelineModel, - kqlQuery: { - filterQuery: { - kuery: { expression: 'expression', kind: 'kuery' as KueryFilterQueryKind }, - serializedQuery: 'some-serialized-query', + it('it invokes kql filter query dispatches if timeline.kqlQuery.filterQuery.kuery is not null', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitForNextUpdate(); + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: { expression: 'expression', kind: 'kuery' as KueryFilterQueryKind }, + serializedQuery: 'some-serialized-query', + }, }, - }, - }; - timelineDispatch({ - ...defaultArgs, - timeline: mockTimeline, - }); + }; + result.current({ + ...defaultArgs, + timeline: mockTimeline, + }); - expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ - id: TimelineId.active, - filterQuery: { - kuery: { - kind: 'kuery', - expression: 'expression', + expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ + id: TimelineId.active, + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'expression', + }, + serializedQuery: 'some-serialized-query', }, - serializedQuery: 'some-serialized-query', - }, + }); }); }); - test('it invokes dispatchAddNotes if duplicate is false', () => { - timelineDispatch({ - ...defaultArgs, - duplicate: false, - notes: [ - { - created: 1585233356356, - updated: 1585233356356, - noteId: 'note-id', - note: 'I am a note', - timelineId: 'abc', - version: 'testVersion', - }, - ], - }); + it('it invokes dispatchAddNotes if duplicate is false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitForNextUpdate(); + result.current({ + ...defaultArgs, + duplicate: false, + notes: [ + { + created: 1585233356356, + updated: 1585233356356, + noteId: 'note-id', + note: 'I am a note', + timelineId: 'abc', + version: 'testVersion', + }, + ], + }); - expect(dispatchAddGlobalTimelineNote).not.toHaveBeenCalled(); - expect(dispatchUpdateNote).not.toHaveBeenCalled(); - expect(dispatchAddNotes).toHaveBeenCalledWith({ - notes: [ - { - created: new Date('2020-03-26T14:35:56.356Z'), - eventId: null, - id: 'note-id', - lastEdit: new Date('2020-03-26T14:35:56.356Z'), - note: 'I am a note', - user: 'unknown', - saveObjectId: 'note-id', - timelineId: 'abc', - version: 'testVersion', - }, - ], + expect(dispatchAddGlobalTimelineNote).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).not.toHaveBeenCalled(); + expect(dispatchAddNotes).toHaveBeenCalledWith({ + notes: [ + { + created: new Date('2020-03-26T14:35:56.356Z'), + eventId: null, + id: 'note-id', + lastEdit: new Date('2020-03-26T14:35:56.356Z'), + note: 'I am a note', + user: 'unknown', + saveObjectId: 'note-id', + timelineId: 'abc', + version: 'testVersion', + }, + ], + }); }); }); - test('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', () => { - timelineDispatch({ - ...defaultArgs, - ruleNote: '# this would be some markdown', - }); - const expectedNote: Note = { - created: new Date(anchor), - id: 'uuidv4()', - lastEdit: null, - note: '# this would be some markdown', - saveObjectId: null, - user: 'elastic', - version: null, - }; - - expect(dispatchAddNotes).not.toHaveBeenCalled(); - expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); - expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ - id: TimelineId.active, - noteId: 'uuidv4()', + it('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitForNextUpdate(); + result.current({ + ...defaultArgs, + ruleNote: '# this would be some markdown', + }); + const expectedNote: Note = { + created: new Date(anchor), + id: 'uuidv4()', + lastEdit: null, + note: '# this would be some markdown', + saveObjectId: null, + user: 'elastic', + version: null, + }; + + expect(dispatchAddNotes).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); + expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ + id: TimelineId.active, + noteId: 'uuidv4()', + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx index 4d598eb5ba4bb..361d7217fd9ee 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { isEmpty } from 'lodash/fp'; import type { Note } from '../../../../common/api/timeline'; @@ -36,102 +37,105 @@ import type { UpdateTimeline } from './types'; export const useUpdateTimeline = () => { const dispatch = useDispatch(); - return ({ - duplicate, - id, - forceNotes = false, - from, - notes, - resolveTimelineConfig, - timeline, - to, - ruleNote, - ruleAuthor, - preventSettingQuery, - }: UpdateTimeline) => { - let _timeline = timeline; - if (duplicate) { - _timeline = { ...timeline, updated: undefined, changed: undefined, version: null }; - } - if (!isEmpty(_timeline.indexNames)) { + return useCallback( + ({ + duplicate, + id, + forceNotes = false, + from, + notes, + resolveTimelineConfig, + timeline, + to, + ruleNote, + ruleAuthor, + preventSettingQuery, + }: UpdateTimeline) => { + let _timeline = timeline; + if (duplicate) { + _timeline = { ...timeline, updated: undefined, changed: undefined, version: null }; + } + if (!isEmpty(_timeline.indexNames)) { + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: _timeline.dataViewId, + selectedPatterns: _timeline.indexNames, + }) + ); + } + if ( + _timeline.status === TimelineStatus.immutable && + _timeline.timelineType === TimelineType.template + ) { + dispatch( + dispatchSetRelativeRangeDatePicker({ + id: InputsModelId.timeline, + fromStr: 'now-24h', + toStr: 'now', + from: DEFAULT_FROM_MOMENT.toISOString(), + to: DEFAULT_TO_MOMENT.toISOString(), + }) + ); + } else { + dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); + } dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: _timeline.dataViewId, - selectedPatterns: _timeline.indexNames, - }) - ); - } - if ( - _timeline.status === TimelineStatus.immutable && - _timeline.timelineType === TimelineType.template - ) { - dispatch( - dispatchSetRelativeRangeDatePicker({ - id: InputsModelId.timeline, - fromStr: 'now-24h', - toStr: 'now', - from: DEFAULT_FROM_MOMENT.toISOString(), - to: DEFAULT_TO_MOMENT.toISOString(), - }) - ); - } else { - dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); - } - dispatch( - dispatchAddTimeline({ - id, - timeline: _timeline, - resolveTimelineConfig, - savedTimeline: duplicate, - }) - ); - if ( - !preventSettingQuery && - _timeline.kqlQuery != null && - _timeline.kqlQuery.filterQuery != null && - _timeline.kqlQuery.filterQuery.kuery != null && - _timeline.kqlQuery.filterQuery.kuery.expression !== '' - ) { - dispatch( - dispatchApplyKqlFilterQuery({ + dispatchAddTimeline({ id, - filterQuery: { - kuery: { - kind: _timeline.kqlQuery.filterQuery.kuery.kind ?? 'kuery', - expression: _timeline.kqlQuery.filterQuery.kuery.expression || '', - }, - serializedQuery: _timeline.kqlQuery.filterQuery.serializedQuery || '', - }, + timeline: _timeline, + resolveTimelineConfig, + savedTimeline: duplicate, }) ); - } + if ( + !preventSettingQuery && + _timeline.kqlQuery != null && + _timeline.kqlQuery.filterQuery != null && + _timeline.kqlQuery.filterQuery.kuery != null && + _timeline.kqlQuery.filterQuery.kuery.expression !== '' + ) { + dispatch( + dispatchApplyKqlFilterQuery({ + id, + filterQuery: { + kuery: { + kind: _timeline.kqlQuery.filterQuery.kuery.kind ?? 'kuery', + expression: _timeline.kqlQuery.filterQuery.kuery.expression || '', + }, + serializedQuery: _timeline.kqlQuery.filterQuery.serializedQuery || '', + }, + }) + ); + } - if (duplicate && ruleNote != null && !isEmpty(ruleNote)) { - const newNote = createNote({ newNote: ruleNote, user: ruleAuthor || 'elastic' }); - dispatch(dispatchUpdateNote({ note: newNote })); - dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id })); - } + if (duplicate && ruleNote != null && !isEmpty(ruleNote)) { + const newNote = createNote({ newNote: ruleNote, user: ruleAuthor || 'elastic' }); + dispatch(dispatchUpdateNote({ note: newNote })); + dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id })); + } - if (!duplicate || forceNotes) { - dispatch( - dispatchAddNotes({ - notes: - notes != null - ? notes.map((note: Note) => ({ - created: note.created != null ? new Date(note.created) : new Date(), - id: note.noteId, - lastEdit: note.updated != null ? new Date(note.updated) : new Date(), - note: note.note || '', - user: note.updatedBy || 'unknown', - saveObjectId: note.noteId, - version: note.version, - eventId: note.eventId ?? null, - timelineId: note.timelineId ?? null, - })) - : [], - }) - ); - } - }; + if (!duplicate || forceNotes) { + dispatch( + dispatchAddNotes({ + notes: + notes != null + ? notes.map((note: Note) => ({ + created: note.created != null ? new Date(note.created) : new Date(), + id: note.noteId, + lastEdit: note.updated != null ? new Date(note.updated) : new Date(), + note: note.note || '', + user: note.updatedBy || 'unknown', + saveObjectId: note.noteId, + version: note.version, + eventId: note.eventId ?? null, + timelineId: note.timelineId ?? null, + })) + : [], + }) + ); + } + }, + [dispatch] + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 94270d2280cc5..dc31ad380635b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -806,7 +806,11 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` "headerCellRender": Object { "$$typeof": Symbol(react.memo), "compare": null, - "type": [Function], + "type": Object { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": [Function], + }, }, "id": "default-timeline-control-column", "rowCellRender": Object { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/alert_table_config_registry.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/alert_table_config_registry.test.ts index 1f3b2525aec14..2910a6ee6b37f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/alert_table_config_registry.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/alert_table_config_registry.test.ts @@ -112,7 +112,7 @@ describe('update()', () => { "id": "test-alert-table-config", } `); - expect(alertTableConfigRegistry.getActions('test-alert-table-config').toggleColumn).toEqual( + expect(alertTableConfigRegistry.getActions('test-alert-table-config')?.toggleColumn).toEqual( toggleColumn ); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/alert_table_config_registry.ts b/x-pack/plugins/triggers_actions_ui/public/application/alert_table_config_registry.ts index 37b2ddbd3b4e0..76e0dc5b39a38 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/alert_table_config_registry.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/alert_table_config_registry.ts @@ -8,16 +8,10 @@ import { i18n } from '@kbn/i18n'; import { noop } from 'lodash'; import { ALERT_TABLE_GENERIC_CONFIG_ID } from './constants'; -import { - AlertsTableConfigurationRegistry, - AlertsTableConfigurationRegistryWithActions, -} from '../types'; +import { AlertsTableConfigurationRegistry } from '../types'; export class AlertTableConfigRegistry { - private readonly objectTypes: Map< - string, - AlertsTableConfigurationRegistry | AlertsTableConfigurationRegistryWithActions - > = new Map(); + private readonly objectTypes: Map = new Map(); /** * Returns if the object type registry has the given type registered @@ -63,9 +57,9 @@ export class AlertTableConfigRegistry { return this.objectTypes.get(id)!; } - public getActions(id: string): AlertsTableConfigurationRegistryWithActions['actions'] { + public getActions(id: string): AlertsTableConfigurationRegistry['actions'] { return ( - (this.objectTypes.get(id) as AlertsTableConfigurationRegistryWithActions)?.actions ?? { + (this.objectTypes.get(id) as AlertsTableConfigurationRegistry)?.actions ?? { toggleColumn: noop, } ); @@ -78,7 +72,7 @@ export class AlertTableConfigRegistry { /** * Returns an object type, throw error if not registered */ - public update(id: string, objectType: AlertsTableConfigurationRegistryWithActions) { + public update(id: string, objectType: AlertsTableConfigurationRegistry) { if (!this.has(id)) { throw new Error( i18n.translate('xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx index d264ad4d7b220..9d29e452bf9a3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React, { useMemo, useReducer } from 'react'; - +import { identity } from 'lodash'; import { fireEvent, render, screen, within, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; @@ -16,6 +16,7 @@ import { ALERT_STATUS, ALERT_CASE_IDS, } from '@kbn/rule-data-utils'; +import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; import { AlertsTable } from './alerts_table'; import { AlertsField, @@ -40,6 +41,13 @@ import { AlertsTableContext, AlertsTableQueryContext } from './contexts/alerts_t const mockCaseService = createCasesServiceMock(); +const mockFieldFormatsRegistry = { + deserialize: jest.fn().mockImplementation(() => ({ + id: 'string', + convert: jest.fn().mockImplementation(identity), + })), +} as unknown as FieldFormatsRegistry; + jest.mock('@kbn/data-plugin/public'); jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({ useUiSetting$: jest.fn((value: string) => ['0,0']), @@ -213,25 +221,6 @@ afterAll(() => { }); describe('AlertsTable', () => { - const fetchAlertsData = { - activePage: 0, - alerts, - alertsCount: alerts.length, - isInitializing: false, - isLoading: false, - getInspectQuery: jest.fn().mockImplementation(() => ({ request: {}, response: {} })), - onPageChange: jest.fn(), - onSortChange: jest.fn(), - refresh: jest.fn(), - sort: [], - ecsAlertsData, - oldAlertsData, - }; - - const useFetchAlertsData = () => { - return fetchAlertsData; - }; - const alertsTableConfiguration: AlertsTableConfigurationRegistry = { id: '', columns, @@ -266,6 +255,22 @@ describe('AlertsTable', () => { ), }; }, + useActionsColumn: () => { + return { + renderCustomActionsRow: () => ( + + {}} + size="s" + data-test-subj="fake-action" + aria-label="fake-action" + /> + + ), + }; + }, }; const browserFields: BrowserFields = { @@ -297,19 +302,28 @@ describe('AlertsTable', () => { columns, deletedEventIds: [], disabledCellActions: [], - pageSize: 1, pageSizeOptions: [1, 10, 20, 50, 100], leadingControlColumns: [], trailingControlColumns: [], - useFetchAlertsData, visibleColumns: columns.map((c) => c.id), 'data-test-subj': 'testTable', - updatedAt: Date.now(), onToggleColumn: () => {}, onResetColumns: () => {}, onChangeVisibleColumns: () => {}, browserFields, query: {}, + pagination: { pageIndex: 0, pageSize: 1 }, + sort: [], + isLoading: false, + alerts, + oldAlertsData, + ecsAlertsData, + getInspectQuery: () => ({ request: [], response: [] }), + refetch: () => {}, + alertsCount: alerts.length, + onSortChange: jest.fn(), + onPageChange: jest.fn(), + fieldFormats: mockFieldFormatsRegistry, }; const defaultBulkActionsState = { @@ -317,6 +331,7 @@ describe('AlertsTable', () => { isAllSelected: false, areAllVisibleRowsSelected: false, rowCount: 4, + updatedAt: Date.now(), }; const useCaseViewNavigationMock = useCaseViewNavigation as jest.Mock; @@ -376,19 +391,20 @@ describe('AlertsTable', () => { skipPointerEventsCheck: true, }); - expect(fetchAlertsData.onSortChange).toHaveBeenCalledWith([ + expect(tableProps.onSortChange).toHaveBeenCalledWith([ { direction: 'asc', id: 'kibana.alert.rule.name' }, ]); }); it('should support pagination', async () => { - const renderResult = render(); - + const renderResult = render( + + ); userEvent.click(renderResult.getByTestId('pagination-button-1'), undefined, { skipPointerEventsCheck: true, }); - expect(fetchAlertsData.onPageChange).toHaveBeenCalledWith({ pageIndex: 1, pageSize: 1 }); + expect(tableProps.onPageChange).toHaveBeenCalledWith({ pageIndex: 1, pageSize: 1 }); }); it('should show when it was updated', () => { @@ -405,7 +421,7 @@ describe('AlertsTable', () => { const props = { ...tableProps, showAlertStatusWithFlapping: true, - pageSize: alerts.length, + pagination: { pageIndex: 0, pageSize: 10 }, alertsTableConfiguration: { ...alertsTableConfiguration, getRenderCellValue: undefined, @@ -431,6 +447,7 @@ describe('AlertsTable', () => { rowCellRender: () =>

Test cell

, }, ], + pagination: { pageIndex: 0, pageSize: 1 }, }; const wrapper = render(); expect(wrapper.queryByTestId('testHeader')).not.toBe(null); @@ -529,6 +546,10 @@ describe('AlertsTable', () => { it('should render no action column if there is neither the action nor the expand action config is set', () => { const customTableProps = { ...tableProps, + alertsTableConfiguration: { + ...alertsTableConfiguration, + useActionsColumn: undefined, + }, }; const { queryByTestId } = render(); @@ -544,7 +565,7 @@ describe('AlertsTable', () => { mockedFn = jest.fn(); customTableProps = { ...tableProps, - pageSize: 2, + pagination: { pageIndex: 0, pageSize: 10 }, alertsTableConfiguration: { ...alertsTableConfiguration, useActionsColumn: () => { @@ -623,7 +644,6 @@ describe('AlertsTable', () => { beforeEach(() => { customTableProps = { ...tableProps, - pageSize: 2, alertsTableConfiguration: { ...alertsTableConfiguration, useCellActions: mockedUseCellActions, @@ -692,13 +712,19 @@ describe('AlertsTable', () => { }); it('should show the cases titles correctly', async () => { - render(); + render(); expect(await screen.findByText('Test case')).toBeInTheDocument(); expect(await screen.findByText('Test case 2')).toBeInTheDocument(); }); it('show loading skeleton if it loads cases', async () => { - render(); + render( + + ); expect((await screen.findAllByTestId('cases-cell-loading')).length).toBe(4); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx index b30d8613c080a..b87633ad22b54 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx @@ -18,12 +18,12 @@ import React, { } from 'react'; import { EuiDataGrid, - EuiDataGridCellValueElementProps, EuiDataGridStyle, EuiSkeletonText, EuiDataGridRefProps, EuiFlexGroup, EuiDataGridProps, + RenderCellValue, EuiDataGridCellPopoverElementProps, EuiCodeBlock, EuiText, @@ -37,11 +37,15 @@ import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/c import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { useSorting, usePagination, useBulkActions, useActionsColumn } from './hooks'; -import { AlertsTableProps, FetchAlertData } from '../../../types'; +import type { + AlertsTableProps, + FetchAlertData, + AlertsTableConfigurationRegistry, +} from '../../../types'; import { ALERTS_TABLE_CONTROL_COLUMNS_ACTIONS_LABEL } from './translations'; import './alerts_table.scss'; -import { getToolbarVisibility } from './toolbar'; +import { useGetToolbarVisibility } from './toolbar'; import { InspectButtonContainer } from './toolbar/components/inspect'; import { SystemCellId } from './types'; import { SystemCellFactory, systemCells } from './cells'; @@ -61,25 +65,163 @@ const getCellActionsStub = { disabledCellActions: [], }; -const basicRenderCellValue = ({ - data, - columnId, -}: { +const fieldBrowserStub = () => ({}); +const stableMappedRowClasses: EuiDataGridStyle['rowClasses'] = {}; + +const BasicRenderCellValue: React.FC<{ data: Array<{ field: string; value: string[] }>; - ecsData?: FetchAlertData['ecsAlertsData'][number]; columnId: string; -}) => { - const value = data.find((d) => d.field === columnId)?.value ?? []; +}> = memo(({ data, columnId }) => { + const value = (Array.isArray(data) && data.find((d) => d.field === columnId)?.value) ?? []; if (Array.isArray(value)) { return <>{value.length ? value.join() : '--'}; } return <>{value}; -}; +}); + +const FullFeaturedRenderCellValue: RenderCellValue = memo((props) => { + const { + columnId, + cases, + maintenanceWindows, + showAlertStatusWithFlapping, + isLoading, + isLoadingCases, + isLoadingMaintenanceWindows, + casesConfig, + rowIndex, + pagination, + RenderCell, + ecsData, + alerts, + } = props; + const idx = rowIndex - pagination.pageSize * pagination.pageIndex; + const alert = alerts[idx]; + if (isSystemCell(columnId)) { + return ( + + ); + } else if (alert) { + // ecsAlert is needed for security solution + const ecsAlert = ecsData[idx]; + const data: Array<{ field: string; value: string[] }> = []; + Object.entries(alert ?? {}).forEach(([key, value]) => { + data.push({ field: key, value: value as string[] }); + }); + if (RenderCell && ecsAlert) { + return ; + } else { + return ; + } + } else if (isLoading) { + return ; + } + return null; +}); + +const ControlColumnHeaderRenderCell = memo(() => { + return ( + + {ALERTS_TABLE_CONTROL_COLUMNS_ACTIONS_LABEL} + + ); +}); + +const ControlColumnRowRenderCell: RenderCellValue = memo((props) => { + const { + visibleRowIndex, + alerts, + ecsData, + setFlyoutAlert, + oldAlertsData, + id, + getSetIsActionLoadingCallback, + refresh, + clearSelection, + renderCustomActionsRow, + } = props; + if (!ecsData[visibleRowIndex]) { + return null; + } + + return ( + + {renderCustomActionsRow({ + alert: alerts[visibleRowIndex], + ecsAlert: ecsData[visibleRowIndex], + nonEcsData: oldAlertsData[visibleRowIndex], + rowIndex: visibleRowIndex, + setFlyoutAlert, + id, + cveProps: props, + setIsActionLoading: getSetIsActionLoadingCallback(visibleRowIndex), + refresh, + clearSelection, + })} + + ); +}); const isSystemCell = (columnId: string): columnId is SystemCellId => { return systemCells.includes(columnId as SystemCellId); }; +const useFieldBrowserOptionsOrDefault = ( + useFieldBrowserOptions: + | NonNullable + | (() => undefined), + onToggleColumn: (columnId: string) => void +) => { + const args = useMemo(() => ({ onToggleColumn }), [onToggleColumn]); + return useFieldBrowserOptions(args); +}; + +// Here we force the error callout to be the same height as the cell content +// so that the error detail gets hidden in the overflow area and only shown in +// the cell popover +const errorCalloutStyles = css` + height: 1lh; +`; + +/** + * An error callout that displays the error stack in a code block + */ +const ViewError = ({ error }: { error: Error }) => ( + <> + + + + + + + + + + + + + + {error.stack} + +); + const Row = styled.div` display: flex; min-width: fit-content; @@ -136,96 +278,74 @@ const CustomGridBody = memo( } ); -// Here we force the error callout to be the same height as the cell content -// so that the error detail gets hidden in the overflow area and only shown in -// the cell popover -const errorCalloutStyles = css` - height: 1lh; -`; - -/** - * An error callout that displays the error stack in a code block - */ -const ViewError = ({ error }: { error: Error }) => ( - <> - - - - - - - - - - - - - - {error.stack} - -); - -const AlertsTable: React.FunctionComponent = (props: AlertsTableProps) => { +const AlertsTable: React.FunctionComponent = memo((props: AlertsTableProps) => { const { visibleColumns, onToggleColumn, onResetColumns, - updatedAt, browserFields, onChangeVisibleColumns, onColumnResize, showAlertStatusWithFlapping = false, showInspectButton = false, - } = props; - - const dataGridRef = useRef(null); - const [activeRowClasses, setActiveRowClasses] = useState< - NonNullable - >({}); - const alertsData = props.useFetchAlertsData(); - const { - activePage, + cellContext: passedCellContext, + leadingControlColumns: passedControlColumns, + trailingControlColumns, + alertsTableConfiguration, + pagination, + columns, alerts, - oldAlertsData, - ecsAlertsData, alertsCount, isLoading, - onPageChange, + oldAlertsData, + ecsAlertsData, onSortChange, + onPageChange, sort: sortingFields, - refresh: alertsRefresh, + refetch: alertsRefresh, getInspectQuery, - } = alertsData; + rowHeightsOptions, + dynamicRowHeight, + query, + featureIds, + cases: { data: cases, isLoading: isLoadingCases }, + maintenanceWindows: { data: maintenanceWindows, isLoading: isLoadingMaintenanceWindows }, + controls, + toolbarVisibility: toolbarVisibilityProp, + shouldHighlightRow, + fieldFormats, + } = props; + + const dataGridRef = useRef(null); + const [activeRowClasses, setActiveRowClasses] = useState< + NonNullable + >({}); const queryClient = useQueryClient({ context: AlertsTableQueryContext }); - const { data: cases, isLoading: isLoadingCases } = props.cases; - const { data: maintenanceWindows, isLoading: isLoadingMaintenanceWindows } = - props.maintenanceWindows; const { sortingColumns, onSort } = useSorting(onSortChange, visibleColumns, sortingFields); - const { - renderCustomActionsRow: CustomActionsRow, - actionsColumnWidth, - getSetIsActionLoadingCallback, - } = useActionsColumn({ - options: props.alertsTableConfiguration.useActionsColumn, - }); - const casesConfig = props.alertsTableConfiguration.cases; - const renderCellContext = props.alertsTableConfiguration.useFetchPageContext?.({ + const { renderCustomActionsRow, actionsColumnWidth, getSetIsActionLoadingCallback } = + useActionsColumn({ + options: alertsTableConfiguration.useActionsColumn, + }); + + const userAssigneeContext = alertsTableConfiguration.useFetchPageContext?.({ alerts, - columns: props.columns, + columns, }); + const bulkActionArgs = useMemo(() => { + return { + alerts, + casesConfig: alertsTableConfiguration.cases, + query, + useBulkActionsConfig: alertsTableConfiguration.useBulkActions, + refresh: alertsRefresh, + featureIds, + }; + }, [alerts, alertsTableConfiguration, query, alertsRefresh, featureIds]); + const { isBulkActionsColumnActive, getBulkActionsLeadingControlColumn, @@ -233,14 +353,7 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab bulkActions, setIsBulkActionsLoading, clearSelection, - } = useBulkActions({ - alerts, - casesConfig, - query: props.query, - useBulkActionsConfig: props.alertsTableConfiguration.useBulkActions, - refresh: alertsRefresh, - featureIds: props.featureIds, - }); + } = useBulkActions(bulkActionArgs); const refreshData = useCallback(() => { alertsRefresh(); @@ -255,7 +368,7 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab }, [clearSelection, refreshData]); const { - pagination, + pagination: updatedPagination, onChangePageSize, onChangePageIndex, onPaginateFlyout, @@ -263,8 +376,8 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab setFlyoutAlertIndex, } = usePagination({ onPageChange, - pageIndex: activePage, - pageSize: props.pageSize, + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, }); // TODO when every solution is using this table, we will be able to simplify it by just passing the alert index @@ -276,216 +389,149 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab [alerts, setFlyoutAlertIndex] ); - const fieldBrowserOptions = props.alertsTableConfiguration.useFieldBrowserOptions - ? props.alertsTableConfiguration?.useFieldBrowserOptions({ - onToggleColumn, - }) - : undefined; + const fieldBrowserOptions = useFieldBrowserOptionsOrDefault( + alertsTableConfiguration.useFieldBrowserOptions ?? fieldBrowserStub, + onToggleColumn + ); - const toolbarVisibility = useCallback(() => { - const { rowSelection } = bulkActionsState; - return getToolbarVisibility({ + const toolbarVisibilityArgs = useMemo(() => { + return { bulkActions, alertsCount, - rowSelection, - alerts: alertsData.alerts, - updatedAt, + rowSelection: bulkActionsState.rowSelection, + alerts, isLoading, columnIds: visibleColumns, onToggleColumn, onResetColumns, browserFields, - controls: props.controls, + controls, setIsBulkActionsLoading, clearSelection, refresh, fieldBrowserOptions, getInspectQuery, showInspectButton, - toolbarVisibilityProp: props.toolbarVisibility, - }); + toolbarVisibilityProp, + }; }, [ - bulkActionsState, bulkActions, alertsCount, - alertsData.alerts, - updatedAt, + bulkActionsState, isLoading, visibleColumns, onToggleColumn, onResetColumns, browserFields, - props.controls, setIsBulkActionsLoading, clearSelection, refresh, fieldBrowserOptions, getInspectQuery, showInspectButton, - props.toolbarVisibility, - ])(); + toolbarVisibilityProp, + alerts, + controls, + ]); - const leadingControlColumns = useMemo(() => { - let controlColumns = [...props.leadingControlColumns]; + const toolbarVisibility = useGetToolbarVisibility(toolbarVisibilityArgs); - if (CustomActionsRow) { - controlColumns = [ - { + const customActionsRow = useMemo(() => { + return renderCustomActionsRow + ? { id: 'expandColumn', width: actionsColumnWidth, - headerCellRender: () => { - return ( - - {ALERTS_TABLE_CONTROL_COLUMNS_ACTIONS_LABEL} - - ); - }, - rowCellRender: (cveProps: EuiDataGridCellValueElementProps) => { - const { visibleRowIndex } = cveProps as EuiDataGridCellValueElementProps & { - visibleRowIndex: number; - }; - - if (!ecsAlertsData[visibleRowIndex]) { - return null; - } - - return ( - - - - ); - }, - }, - ...controlColumns, - ]; - } + headerCellRender: ControlColumnHeaderRenderCell, + rowCellRender: ControlColumnRowRenderCell, + } + : undefined; + }, [renderCustomActionsRow, actionsColumnWidth]); + const bulkActionsColumn = useMemo(() => { + return isBulkActionsColumnActive ? getBulkActionsLeadingControlColumn() : undefined; + }, [isBulkActionsColumnActive, getBulkActionsLeadingControlColumn]); - if (isBulkActionsColumnActive) { - controlColumns = [getBulkActionsLeadingControlColumn(), ...controlColumns]; + const leadingControlColumns = useMemo(() => { + const controlColumns = passedControlColumns ?? []; + const usedBulkActionsColumn = bulkActionsColumn ? [bulkActionsColumn] : []; + const usedCustomActionsRow = customActionsRow ? [customActionsRow] : []; + const mergedControlColumns = [ + ...controlColumns, + ...usedBulkActionsColumn, + ...usedCustomActionsRow, + ]; + if (mergedControlColumns.length) { + return mergedControlColumns; + } else { + return undefined; } + }, [bulkActionsColumn, customActionsRow, passedControlColumns]); - return controlColumns; - }, [ - props.leadingControlColumns, - props.id, - CustomActionsRow, - isBulkActionsColumnActive, - actionsColumnWidth, - ecsAlertsData, - alerts, - oldAlertsData, - handleFlyoutAlert, - getSetIsActionLoadingCallback, - refresh, - clearSelection, - getBulkActionsLeadingControlColumn, - ]); - + const rowIndex = flyoutAlertIndex + pagination.pageIndex * pagination.pageSize; useEffect(() => { // Row classes do not deal with visible row indices, so we need to handle page offset - const rowIndex = flyoutAlertIndex + pagination.pageIndex * pagination.pageSize; setActiveRowClasses({ [rowIndex]: 'alertsTableActiveRow', }); - }, [flyoutAlertIndex, pagination.pageIndex, pagination.pageSize]); - - // Update highlighted rows when alerts or pagination changes - const highlightedRowClasses = useMemo(() => { - let mappedRowClasses: EuiDataGridStyle['rowClasses'] = {}; - const shouldHighlightRowCheck = props.shouldHighlightRow; - if (shouldHighlightRowCheck) { - mappedRowClasses = alerts.reduce>( - (rowClasses, alert, index) => { - if (shouldHighlightRowCheck(alert)) { - rowClasses[index + pagination.pageIndex * pagination.pageSize] = - 'alertsTableHighlightedRow'; - } - - return rowClasses; - }, - {} - ); - } - return mappedRowClasses; - }, [props.shouldHighlightRow, alerts, pagination.pageIndex, pagination.pageSize]); + }, [rowIndex]); const handleFlyoutClose = useCallback(() => setFlyoutAlertIndex(-1), [setFlyoutAlertIndex]); - const renderCellValue = useCallback( - () => - props.alertsTableConfiguration?.getRenderCellValue?.({ - setFlyoutAlert: handleFlyoutAlert, - context: renderCellContext, - }) ?? basicRenderCellValue, - [handleFlyoutAlert, props.alertsTableConfiguration, renderCellContext] - )(); - - const handleRenderCellValue = useCallback( - (_props: EuiDataGridCellValueElementProps) => { - try { - // https://github.com/elastic/eui/issues/5811 - const idx = _props.rowIndex - pagination.pageSize * pagination.pageIndex; - const alert = alerts[idx]; - // ecsAlert is needed for security solution - const ecsAlert = ecsAlertsData[idx]; - if (alert) { - const data: Array<{ field: string; value: string[] }> = []; - Object.entries(alert ?? {}).forEach(([key, value]) => { - data.push({ field: key, value: value as string[] }); - }); - if (isSystemCell(_props.columnId)) { - return ( - - ); - } - - return renderCellValue({ - ..._props, - data, - ecsData: ecsAlert, - }); - } else if (isLoading) { - return ; - } - return null; - } catch (e) { - return ; - } - }, - [ + const RenderCell = useMemo(() => { + if (props.alertsTableConfiguration?.getRenderCellValue) { + return props.alertsTableConfiguration.getRenderCellValue; + } else { + return FullFeaturedRenderCellValue; + } + }, [props.alertsTableConfiguration]); + + const renderCellContext = useMemo(() => { + const additionalContext = passedCellContext ? passedCellContext : {}; + return { + ...additionalContext, + ...alertsTableConfiguration, + ecsData: ecsAlertsData, + oldAlertsData, + context: userAssigneeContext, alerts, - cases, - casesConfig?.appId, - ecsAlertsData, + browserFields, + pagination: updatedPagination, isLoading, + setFlyoutAlert: handleFlyoutAlert, + RenderCell, isLoadingCases, isLoadingMaintenanceWindows, + getSetIsActionLoadingCallback, + cases, maintenanceWindows, - pagination.pageIndex, - pagination.pageSize, - renderCellValue, showAlertStatusWithFlapping, - ] - ); + refresh, + clearSelection, + renderCustomActionsRow, + fieldFormats, + }; + }, [ + passedCellContext, + alertsTableConfiguration, + ecsAlertsData, + oldAlertsData, + refresh, + clearSelection, + renderCustomActionsRow, + handleFlyoutAlert, + RenderCell, + browserFields, + isLoading, + updatedPagination, + alerts, + isLoadingCases, + isLoadingMaintenanceWindows, + cases, + maintenanceWindows, + showAlertStatusWithFlapping, + getSetIsActionLoadingCallback, + userAssigneeContext, + fieldFormats, + ]); const renderCellPopover = useMemo( () => @@ -519,29 +565,36 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab const dataGridPagination = useMemo( () => ({ - ...pagination, + pageIndex: updatedPagination.pageIndex, + pageSize: updatedPagination.pageSize, pageSizeOptions: props.pageSizeOptions, onChangeItemsPerPage: onChangePageSize, onChangePage: onChangePageIndex, }), - [onChangePageIndex, onChangePageSize, pagination, props.pageSizeOptions] + [ + onChangePageIndex, + onChangePageSize, + updatedPagination.pageIndex, + updatedPagination.pageSize, + props.pageSizeOptions, + ] ); - const { getCellActions, visibleCellActions, disabledCellActions } = props.alertsTableConfiguration - ?.useCellActions - ? props.alertsTableConfiguration?.useCellActions({ - columns: props.columns, - data: oldAlertsData, - ecsData: ecsAlertsData, - dataGridRef, - pageSize: pagination.pageSize, - pageIndex: pagination.pageIndex, - }) - : getCellActionsStub; + const { getCellActions, visibleCellActions, disabledCellActions } = + alertsTableConfiguration?.useCellActions + ? alertsTableConfiguration?.useCellActions({ + columns, + data: oldAlertsData, + ecsData: ecsAlertsData, + dataGridRef, + pageSize: pagination.pageSize, + pageIndex: pagination.pageIndex, + }) + : getCellActionsStub; const columnsWithCellActions = useMemo(() => { if (getCellActions) { - return props.columns.map((col, idx) => ({ + return columns.map((col, idx) => ({ ...col, ...(!(disabledCellActions ?? []).includes(col.id) ? { @@ -551,14 +604,33 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab : {}), })); } - return props.columns; - }, [getCellActions, disabledCellActions, props.columns, visibleCellActions]); + return columns; + }, [getCellActions, disabledCellActions, columns, visibleCellActions]); - // Merges the default grid style with the grid style that comes in through props. - const actualGridStyle = useMemo(() => { + // // Update highlighted rows when alerts or pagination changes + const highlightedRowClasses = useMemo(() => { + if (shouldHighlightRow) { + const emptyShouldHighlightRow: EuiDataGridStyle['rowClasses'] = {}; + return alerts.reduce>( + (rowClasses, alert, index) => { + if (shouldHighlightRow(alert)) { + rowClasses[index + pagination.pageIndex * pagination.pageSize] = + 'alertsTableHighlightedRow'; + } + + return rowClasses; + }, + emptyShouldHighlightRow + ); + } else { + return stableMappedRowClasses; + } + }, [shouldHighlightRow, alerts, pagination.pageIndex, pagination.pageSize]); + + const mergedGridStyle = useMemo(() => { const propGridStyle: NonNullable = props.gridStyle ?? {}; // Merges default row classes, custom ones and adds the active row class style - const mergedGridStyle: EuiDataGridStyle = { + return { ...DefaultGridStyle, ...propGridStyle, rowClasses: { @@ -568,7 +640,11 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab ...activeRowClasses, }, }; + }, [activeRowClasses, highlightedRowClasses, props.gridStyle]); + // Merges the default grid style with the grid style that comes in through props. + const actualGridStyle = useMemo(() => { + const propGridStyle: NonNullable = props.gridStyle ?? {}; // If ANY additional rowClasses have been provided, we need to merge them with our internal ones if (propGridStyle.rowClasses) { // Get all row indices with a rowClass. @@ -593,7 +669,7 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab mergedGridStyle.rowClasses = mergedRowClasses; } return mergedGridStyle; - }, [activeRowClasses, highlightedRowClasses, props.gridStyle]); + }, [props.gridStyle, mergedGridStyle]); const renderCustomGridBody = useCallback>( ({ visibleColumns: _visibleColumns, Cell }) => ( @@ -610,6 +686,13 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab [actualGridStyle, oldAlertsData, pagination, isLoading, props.gridStyle?.stripes] ); + const sortProps = useMemo(() => { + return { columns: sortingColumns, onSort }; + }, [sortingColumns, onSort]); + + const columnVisibility = useMemo(() => { + return { visibleColumns, setVisibleColumns: onChangeVisibleColumns }; + }, [visibleColumns, onChangeVisibleColumns]); return (
@@ -619,7 +702,7 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab alert={alerts[flyoutAlertIndex]} alertsCount={alertsCount} onClose={handleFlyoutClose} - alertsTableConfiguration={props.alertsTableConfiguration} + alertsTableConfiguration={alertsTableConfiguration} flyoutIndex={flyoutAlertIndex + pagination.pageIndex * pagination.pageSize} onPaginate={onPaginateFlyout} isLoading={isLoading} @@ -632,26 +715,29 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab aria-label="Alerts table" data-test-subj="alertsTable" columns={columnsWithCellActions} - columnVisibility={{ visibleColumns, setVisibleColumns: onChangeVisibleColumns }} - trailingControlColumns={props.trailingControlColumns} + columnVisibility={columnVisibility} + trailingControlColumns={trailingControlColumns} leadingControlColumns={leadingControlColumns} rowCount={alertsCount} - renderCellValue={handleRenderCellValue} + renderCellValue={FullFeaturedRenderCellValue} gridStyle={actualGridStyle} - sorting={{ columns: sortingColumns, onSort }} + sorting={sortProps} toolbarVisibility={toolbarVisibility} + cellContext={renderCellContext} pagination={dataGridPagination} - rowHeightsOptions={props.rowHeightsOptions} + rowHeightsOptions={rowHeightsOptions} onColumnResize={onColumnResize} ref={dataGridRef} - renderCustomGridBody={props.dynamicRowHeight ? renderCustomGridBody : undefined} + renderCustomGridBody={dynamicRowHeight ? renderCustomGridBody : undefined} renderCellPopover={handleRenderCellPopover} /> )}
); -}; +}); + +AlertsTable.displayName = 'AlertsTable'; export { AlertsTable }; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.test.tsx index 0564d2fdcddc6..8f54eaf5c6278 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.test.tsx @@ -151,6 +151,8 @@ const alerts = [ [AlertsField.name]: ['five'], [AlertsField.reason]: ['six'], [AlertsField.uuid]: ['1047d115-5afd-469e-baf6-f28c2b68db46'], + [ALERT_CASE_IDS]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], }, ] as unknown as Alerts; @@ -260,10 +262,6 @@ const getMock = jest.fn().mockImplementation((plugin: string) => { header: () => <>{'header'}, footer: () => <>{'footer'}, }), - getRenderCellValue: () => - jest.fn().mockImplementation((props) => { - return `${props.colIndex}:${props.rowIndex}`; - }), useActionsColumn: () => ({ renderCustomActionsRow: ({ setFlyoutAlert }: RenderCustomActionsRowArgs) => { return ( @@ -326,12 +324,19 @@ const AlertsTableWithLocale: React.FunctionComponent = (p ); describe('AlertsTableState', () => { - const tableProps = { + const tableProps: AlertsTableStateProps = { alertsTableConfigurationRegistry: alertsTableConfigurationRegistryMock, configurationId: PLUGIN_ID, - id: `test-alerts`, + id: PLUGIN_ID, featureIds: [AlertConsumers.LOGS], query: {}, + columns, + pagination: { + pageIndex: 0, + pageSize: 10, + onChangePage: jest.fn(), + onChangeItemsPerPage: jest.fn(), + }, }; const mockCustomProps = (customProps: Partial) => { @@ -349,6 +354,7 @@ describe('AlertsTableState', () => { has: hasMock, get: getMockWithUsePersistentControls, update: updateMock, + getActions: getActionsMock, } as unknown as AlertTableConfigRegistry; return { @@ -454,15 +460,19 @@ describe('AlertsTableState', () => { const props = mockCustomProps({ cases: { featureId: 'test-feature-id', owner: ['test-owner'] }, - columns: [ - { - id: AlertsField.name, - displayAsText: 'Name', - }, - ], }); - render(); + render( + + ); await waitFor(() => { expect(useBulkGetCasesMock).toHaveBeenCalledWith(['test-id-2'], false); }); @@ -643,16 +653,17 @@ describe('AlertsTableState', () => { it('should not fetch maintenance windows if the user does not have permission', async () => {}); it('should not fetch maintenance windows if the column is not visible', async () => { - const props = mockCustomProps({ - columns: [ - { - id: AlertsField.name, - displayAsText: 'Name', - }, - ], - }); - - render(); + render( + + ); await waitFor(() => { expect(useBulkGetMaintenanceWindowsMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -798,6 +809,7 @@ describe('AlertsTableState', () => { }; beforeEach(() => { + jest.clearAllMocks(); hookUseFetchBrowserFieldCapabilities.mockClear(); hookUseFetchBrowserFieldCapabilities.mockImplementation(() => [true, browserFields]); useBulkGetCasesMock.mockReturnValue({ data: new Map(), isFetching: false }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx index 1bcd8ceb7d466..70aaf7a304453 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useCallback, useRef, useMemo, useReducer, useEffect } from 'react'; +import React, { useState, useCallback, useRef, useMemo, useReducer, useEffect, memo } from 'react'; import { isEmpty } from 'lodash'; import { EuiDataGridColumn, @@ -20,11 +20,8 @@ import { EuiDataGridControlColumn, } from '@elastic/eui'; import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - ALERT_CASE_IDS, - ALERT_MAINTENANCE_WINDOW_IDS, - ALERT_RULE_UUID, -} from '@kbn/rule-data-utils'; +import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; +import { ALERT_CASE_IDS, ALERT_MAINTENANCE_WINDOW_IDS } from '@kbn/rule-data-utils'; import type { ValidFeatureId } from '@kbn/rule-data-utils'; import type { BrowserFields, @@ -107,7 +104,7 @@ const EmptyConfiguration: AlertsTableConfigurationRegistry = { id: '', columns: [], sort: [], - getRenderCellValue: () => () => null, + getRenderCellValue: () => null, }; type AlertWithCaseIds = Alert & Required>; @@ -142,6 +139,19 @@ const isCasesColumnEnabled = (columns: EuiDataGridColumn[]): boolean => const isMaintenanceWindowColumnEnabled = (columns: EuiDataGridColumn[]): boolean => columns.some(({ id }) => id === ALERT_MAINTENANCE_WINDOW_IDS); +const stableEmptyArray: string[] = []; +const defaultPageSizeOptions = [10, 20, 50, 100]; + +const emptyRowSelection = new Map(); + +const initialBulkActionsState = { + rowSelection: emptyRowSelection, + isAllSelected: false, + areAllVisibleRowsSelected: false, + rowCount: 0, + updatedAt: Date.now(), +}; + const ErrorBoundaryFallback: FallbackComponent = ({ error }) => { return ( { ); }; -const AlertsTableState = (props: AlertsTableStateProps) => { +const AlertsTableState = memo((props: AlertsTableStateProps) => { return ( @@ -175,327 +185,369 @@ const AlertsTableState = (props: AlertsTableStateProps) => { ); -}; +}); + +AlertsTableState.displayName = 'AlertsTableState'; const DEFAULT_LEADING_CONTROL_COLUMNS: EuiDataGridControlColumn[] = []; -const AlertsTableStateWithQueryProvider = ({ - alertsTableConfigurationRegistry, - configurationId, - id, - featureIds, - query, - pageSize, - leadingControlColumns = DEFAULT_LEADING_CONTROL_COLUMNS, - rowHeightsOptions, - renderCellValue, - renderCellPopover, - columns: propColumns, - gridStyle, - browserFields: propBrowserFields, - onUpdate, - onLoaded, - runtimeMappings, - showAlertStatusWithFlapping, - toolbarVisibility, - shouldHighlightRow, - dynamicRowHeight, - lastReloadRequestTime, -}: AlertsTableStateProps) => { - const { cases: casesService } = useKibana<{ cases?: CasesService }>().services; - const hasAlertsTableConfiguration = - alertsTableConfigurationRegistry?.has(configurationId) ?? false; - - if (!hasAlertsTableConfiguration) - // eslint-disable-next-line no-console - console.warn(`Missing Alert Table configuration for configuration ID: ${configurationId}`); - - const alertsTableConfiguration = hasAlertsTableConfiguration - ? alertsTableConfigurationRegistry.get(configurationId) - : EmptyConfiguration; - - const storage = useRef(new Storage(window.localStorage)); - const localStorageAlertsTableConfig = storage.current.get(id) as Partial; - const persistentControls = alertsTableConfiguration?.usePersistentControls?.(); - const showInspectButton = alertsTableConfiguration?.showInspectButton ?? false; - - const columnConfigByClient = - propColumns && !isEmpty(propColumns) ? propColumns : alertsTableConfiguration?.columns ?? []; - - const columnsLocal = - localStorageAlertsTableConfig && - localStorageAlertsTableConfig.columns && - !isEmpty(localStorageAlertsTableConfig?.columns) - ? localStorageAlertsTableConfig?.columns - : columnConfigByClient; - - const getStorageConfig = () => ({ - columns: columnsLocal, - sort: - localStorageAlertsTableConfig && - localStorageAlertsTableConfig.sort && - !isEmpty(localStorageAlertsTableConfig?.sort) - ? localStorageAlertsTableConfig?.sort - : alertsTableConfiguration?.sort ?? [], - visibleColumns: - localStorageAlertsTableConfig && - localStorageAlertsTableConfig.visibleColumns && - !isEmpty(localStorageAlertsTableConfig?.visibleColumns) - ? localStorageAlertsTableConfig?.visibleColumns - : columnsLocal.map((c) => c.id), - }); - const storageAlertsTable = useRef(getStorageConfig()); - - storageAlertsTable.current = getStorageConfig(); - - const [sort, setSort] = useState(storageAlertsTable.current.sort); - const [pagination, setPagination] = useState({ - ...DefaultPagination, - pageSize: pageSize ?? DefaultPagination.pageSize, - }); - - const { - columns, - browserFields, - isBrowserFieldDataLoading, - onToggleColumn, - onResetColumns, - visibleColumns, - onChangeVisibleColumns, - onColumnResize, - fields, - } = useColumns({ - featureIds, - storageAlertsTable, - storage, +const AlertsTableStateWithQueryProvider = memo( + ({ + alertsTableConfigurationRegistry, + configurationId, id, - defaultColumns: columnConfigByClient, - initialBrowserFields: propBrowserFields, - }); - - const onPageChange = useCallback((_pagination: RuleRegistrySearchRequestPagination) => { - setPagination(_pagination); - }, []); - - const [ - isLoading, - { - alerts, - oldAlertsData, - ecsAlertsData, - isInitializing, - getInspectQuery, - refetch: refresh, - totalAlerts: alertsCount, - updatedAt, - }, - ] = useFetchAlerts({ - fields, featureIds, query, - pagination, - onPageChange, + pageSize, + leadingControlColumns = DEFAULT_LEADING_CONTROL_COLUMNS, + trailingControlColumns, + rowHeightsOptions, + cellContext, + columns: propColumns, + gridStyle, + browserFields: propBrowserFields, + onUpdate, onLoaded, runtimeMappings, - sort, - skip: false, - }); - - const { data: mutedAlerts } = useGetMutedAlerts([ - ...new Set(alerts.map((a) => a[ALERT_RULE_UUID]![0])), - ]); - - useEffect(() => { - if (hasAlertsTableConfiguration) { - alertsTableConfigurationRegistry.update(configurationId, { - ...alertsTableConfiguration, - actions: { toggleColumn: onToggleColumn }, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [onToggleColumn]); - - useEffect(() => { - if (onUpdate) { - onUpdate({ isLoading, totalCount: alertsCount, refresh }); - } - }, [isLoading, alertsCount, onUpdate, refresh]); - useEffect(() => { - if (lastReloadRequestTime) { - refresh(); - } - }, [lastReloadRequestTime, refresh]); - - const caseIds = useMemo(() => getCaseIdsFromAlerts(alerts), [alerts]); - const maintenanceWindowIds = useMemo(() => getMaintenanceWindowIdsFromAlerts(alerts), [alerts]); - - const casesPermissions = casesService?.helpers.canUseCases( - alertsTableConfiguration?.cases?.owner ?? [] - ); - - const hasCaseReadPermissions = Boolean(casesPermissions?.read); - const fetchCases = isCasesColumnEnabled(columns) && hasCaseReadPermissions; - const fetchMaintenanceWindows = isMaintenanceWindowColumnEnabled(columns); + showAlertStatusWithFlapping, + toolbarVisibility, + shouldHighlightRow, + dynamicRowHeight, + lastReloadRequestTime, + }: AlertsTableStateProps) => { + const { cases: casesService, fieldFormats } = useKibana<{ + cases?: CasesService; + fieldFormats: FieldFormatsRegistry; + }>().services; + const hasAlertsTableConfiguration = + alertsTableConfigurationRegistry?.has(configurationId) ?? false; + + if (!hasAlertsTableConfiguration) + // eslint-disable-next-line no-console + console.warn(`Missing Alert Table configuration for configuration ID: ${configurationId}`); + + const alertsTableConfiguration = hasAlertsTableConfiguration + ? alertsTableConfigurationRegistry.get(configurationId) + : EmptyConfiguration; + + const storage = useRef(new Storage(window.localStorage)); + const localStorageAlertsTableConfig = storage.current.get(id) as Partial; + const persistentControls = alertsTableConfiguration?.usePersistentControls?.(); + const showInspectButton = alertsTableConfiguration?.showInspectButton ?? false; + + const columnConfigByClient = useMemo(() => { + return propColumns && !isEmpty(propColumns) + ? propColumns + : alertsTableConfiguration?.columns ?? []; + }, [propColumns, alertsTableConfiguration]); + + const columnsLocal = + localStorageAlertsTableConfig && + localStorageAlertsTableConfig.columns && + !isEmpty(localStorageAlertsTableConfig?.columns) + ? localStorageAlertsTableConfig?.columns + : columnConfigByClient; + + const getStorageConfig = useCallback( + () => ({ + columns: columnsLocal, + sort: + localStorageAlertsTableConfig && + localStorageAlertsTableConfig.sort && + !isEmpty(localStorageAlertsTableConfig?.sort) + ? localStorageAlertsTableConfig?.sort + : alertsTableConfiguration?.sort ?? [], + visibleColumns: + localStorageAlertsTableConfig && + localStorageAlertsTableConfig.visibleColumns && + !isEmpty(localStorageAlertsTableConfig?.visibleColumns) + ? localStorageAlertsTableConfig?.visibleColumns + : columnsLocal.map((c) => c.id), + }), + [columnsLocal, alertsTableConfiguration?.sort, localStorageAlertsTableConfig] + ); + const storageAlertsTable = useRef(getStorageConfig()); - const { data: cases, isFetching: isLoadingCases } = useBulkGetCases( - Array.from(caseIds.values()), - fetchCases - ); + storageAlertsTable.current = getStorageConfig(); - const { data: maintenanceWindows, isFetching: isLoadingMaintenanceWindows } = - useBulkGetMaintenanceWindows({ - ids: Array.from(maintenanceWindowIds.values()), - canFetchMaintenanceWindows: fetchMaintenanceWindows, - queryContext: AlertsTableQueryContext, + const [sort, setSort] = useState(storageAlertsTable.current.sort); + const [pagination, setPagination] = useState({ + ...DefaultPagination, + pageSize: pageSize ?? DefaultPagination.pageSize, }); - const initialBulkActionsState = useReducer(bulkActionsReducer, { - rowSelection: new Map(), - isAllSelected: false, - areAllVisibleRowsSelected: false, - rowCount: alerts.length, - }); - - const onSortChange = useCallback( - (_sort: EuiDataGridSorting['columns']) => { - const newSort = _sort.map((sortItem) => { - return { - [sortItem.id]: { - order: sortItem.direction, - }, - }; - }); + const onPageChange = useCallback((_pagination: RuleRegistrySearchRequestPagination) => { + setPagination(_pagination); + }, []); - storageAlertsTable.current = { - ...storageAlertsTable.current, - sort: newSort, - }; - storage.current.set(id, storageAlertsTable.current); - setSort(newSort); - }, - [id] - ); - - const useFetchAlertsData = useCallback(() => { - return { - activePage: pagination.pageIndex, - alerts, - alertsCount, - isInitializing, - isLoading, - getInspectQuery, - onPageChange, - onSortChange, - refresh, - sort, - updatedAt, - oldAlertsData, - ecsAlertsData, - }; - }, [ - alerts, - alertsCount, - ecsAlertsData, - getInspectQuery, - isInitializing, - isLoading, - oldAlertsData, - onPageChange, - onSortChange, - pagination, - refresh, - sort, - updatedAt, - ]); - - const CasesContext = casesService?.ui.getCasesContext(); - const isCasesContextAvailable = casesService && CasesContext; - - const memoizedCases = useMemo( - () => ({ - data: cases ?? new Map(), - isLoading: isLoadingCases, - }), - [cases, isLoadingCases] - ); - - const memoizedMaintenanceWindows = useMemo( - () => ({ - data: maintenanceWindows ?? new Map(), - isLoading: isLoadingMaintenanceWindows, - }), - [maintenanceWindows, isLoadingMaintenanceWindows] - ); - - const tableProps: AlertsTableProps = useMemo( - () => ({ - alertsTableConfiguration, - cases: memoizedCases, - maintenanceWindows: memoizedMaintenanceWindows, + const { columns, - bulkActions: [], - deletedEventIds: [], - disabledCellActions: [], - pageSize: pagination.pageSize, - pageSizeOptions: [10, 20, 50, 100], - id, - leadingControlColumns, - showAlertStatusWithFlapping, - trailingControlColumns: [], - useFetchAlertsData, - visibleColumns, - 'data-test-subj': 'internalAlertsState', - updatedAt, browserFields, + isBrowserFieldDataLoading, onToggleColumn, onResetColumns, + visibleColumns, onChangeVisibleColumns, onColumnResize, - query, - rowHeightsOptions, - renderCellValue, - renderCellPopover, - gridStyle, - controls: persistentControls, - showInspectButton, - toolbarVisibility, - shouldHighlightRow, - dynamicRowHeight, + fields, + } = useColumns({ featureIds, - }), - [ - alertsTableConfiguration, - memoizedCases, - memoizedMaintenanceWindows, - columns, - pagination.pageSize, + storageAlertsTable, + storage, id, - leadingControlColumns, - showAlertStatusWithFlapping, - useFetchAlertsData, - visibleColumns, - updatedAt, - browserFields, - onToggleColumn, - onResetColumns, - onChangeVisibleColumns, - onColumnResize, - query, - rowHeightsOptions, - renderCellValue, - renderCellPopover, - gridStyle, - persistentControls, - showInspectButton, - toolbarVisibility, - shouldHighlightRow, - dynamicRowHeight, + defaultColumns: columnConfigByClient, + initialBrowserFields: propBrowserFields, + }); + + const [ + isLoading, + { + alerts, + oldAlertsData, + ecsAlertsData, + isInitializing, + getInspectQuery, + refetch: refresh, + totalAlerts: alertsCount, + }, + ] = useFetchAlerts({ + fields, featureIds, - ] - ); + query, + pagination, + onPageChange, + onLoaded, + runtimeMappings, + sort, + skip: false, + }); + + const mutedAlertIds = useMemo(() => { + return [...new Set(alerts.map((a) => a['kibana.alert.rule.uuid']![0]))]; + }, [alerts]); + + const { data: mutedAlerts } = useGetMutedAlerts(mutedAlertIds); + const overriddenActions = useMemo(() => { + return { toggleColumn: onToggleColumn }; + }, [onToggleColumn]); + + const configWithToggle = useMemo(() => { + return { + ...alertsTableConfiguration, + actions: overriddenActions, + }; + }, [alertsTableConfiguration, overriddenActions]); + + useEffect(() => { + const currentToggle = + alertsTableConfigurationRegistry.getActions(configurationId)?.toggleColumn; + if (onToggleColumn !== currentToggle) { + alertsTableConfigurationRegistry.update(configurationId, configWithToggle); + } + }, [configurationId, alertsTableConfigurationRegistry, configWithToggle, onToggleColumn]); + + useEffect(() => { + if (onUpdate) { + onUpdate({ isLoading, totalCount: alertsCount, refresh }); + } + }, [isLoading, alertsCount, onUpdate, refresh]); + useEffect(() => { + if (lastReloadRequestTime) { + refresh(); + } + }, [lastReloadRequestTime, refresh]); + + const caseIds = useMemo(() => getCaseIdsFromAlerts(alerts), [alerts]); + const maintenanceWindowIds = useMemo(() => getMaintenanceWindowIdsFromAlerts(alerts), [alerts]); + + const casesPermissions = useMemo(() => { + return casesService?.helpers.canUseCases(alertsTableConfiguration?.cases?.owner ?? []); + }, [alertsTableConfiguration, casesService]); + + const hasCaseReadPermissions = Boolean(casesPermissions?.read); + const fetchCases = isCasesColumnEnabled(columns) && hasCaseReadPermissions; + const fetchMaintenanceWindows = isMaintenanceWindowColumnEnabled(columns); - if (!hasAlertsTableConfiguration) { - return ( + const caseIdsForBulk = useMemo(() => { + return Array.from(caseIds.values()); + }, [caseIds]); + + const { data: cases, isFetching: isLoadingCases } = useBulkGetCases(caseIdsForBulk, fetchCases); + + const maintenanceWindowIdsForBulk = useMemo(() => { + return { + ids: Array.from(maintenanceWindowIds.values()), + canFetchMaintenanceWindows: fetchMaintenanceWindows, + queryContext: AlertsTableQueryContext, + }; + }, [fetchMaintenanceWindows, maintenanceWindowIds]); + + const { data: maintenanceWindows, isFetching: isLoadingMaintenanceWindows } = + useBulkGetMaintenanceWindows(maintenanceWindowIdsForBulk); + + const activeBulkActionsReducer = useReducer(bulkActionsReducer, initialBulkActionsState); + + const onSortChange = useCallback( + (_sort: EuiDataGridSorting['columns']) => { + const newSort = _sort.map((sortItem) => { + return { + [sortItem.id]: { + order: sortItem.direction, + }, + }; + }); + + storageAlertsTable.current = { + ...storageAlertsTable.current, + sort: newSort, + }; + storage.current.set(id, storageAlertsTable.current); + setSort(newSort); + }, + [id] + ); + + const CasesContext = useMemo(() => { + return casesService?.ui.getCasesContext(); + }, [casesService?.ui]); + + const isCasesContextAvailable = casesService && CasesContext; + + const memoizedCases = useMemo( + () => ({ + data: cases ?? new Map(), + isLoading: isLoadingCases, + }), + [cases, isLoadingCases] + ); + + const memoizedMaintenanceWindows = useMemo( + () => ({ + data: maintenanceWindows ?? new Map(), + isLoading: isLoadingMaintenanceWindows, + }), + [maintenanceWindows, isLoadingMaintenanceWindows] + ); + + const tableProps: AlertsTableProps = useMemo( + () => ({ + alertsTableConfiguration, + cases: memoizedCases, + maintenanceWindows: memoizedMaintenanceWindows, + columns, + bulkActions: stableEmptyArray, + deletedEventIds: stableEmptyArray, + disabledCellActions: stableEmptyArray, + pageSizeOptions: defaultPageSizeOptions, + id, + leadingControlColumns, + showAlertStatusWithFlapping, + trailingControlColumns, + visibleColumns, + 'data-test-subj': 'internalAlertsState', + browserFields, + onToggleColumn, + onResetColumns, + onChangeVisibleColumns, + onColumnResize, + query, + rowHeightsOptions, + cellContext, + gridStyle, + controls: persistentControls, + showInspectButton, + toolbarVisibility, + shouldHighlightRow, + dynamicRowHeight, + featureIds, + isInitializing, + pagination, + sort, + isLoading, + alerts, + oldAlertsData, + ecsAlertsData, + getInspectQuery, + refetch: refresh, + alertsCount, + onSortChange, + onPageChange, + fieldFormats, + }), + [ + alertsTableConfiguration, + memoizedCases, + memoizedMaintenanceWindows, + columns, + id, + leadingControlColumns, + trailingControlColumns, + showAlertStatusWithFlapping, + visibleColumns, + browserFields, + onToggleColumn, + onResetColumns, + onChangeVisibleColumns, + onColumnResize, + query, + rowHeightsOptions, + gridStyle, + persistentControls, + showInspectButton, + toolbarVisibility, + shouldHighlightRow, + dynamicRowHeight, + featureIds, + cellContext, + isInitializing, + pagination, + sort, + isLoading, + alerts, + oldAlertsData, + ecsAlertsData, + getInspectQuery, + refresh, + alertsCount, + onSortChange, + onPageChange, + fieldFormats, + ] + ); + + const alertsTableContext = useMemo(() => { + return { + mutedAlerts: mutedAlerts ?? {}, + bulkActions: activeBulkActionsReducer, + }; + }, [activeBulkActionsReducer, mutedAlerts]); + + return hasAlertsTableConfiguration ? ( + + {!isLoading && alertsCount === 0 && ( + + + + )} + {(isLoading || isBrowserFieldDataLoading) && ( + + )} + {alertsCount !== 0 && isCasesContextAvailable && ( + + + + )} + {alertsCount !== 0 && !isCasesContextAvailable && } + + ) : ( ); } +); - return ( - - {!isLoading && alertsCount === 0 && ( - - - - )} - {(isLoading || isBrowserFieldDataLoading) && ( - - )} - {alertsCount !== 0 && isCasesContextAvailable && ( - - - - )} - {alertsCount !== 0 && !isCasesContextAvailable && } - - ); -}; +AlertsTableStateWithQueryProvider.displayName = 'AlertsTableStateWithQueryProvider'; export { AlertsTableState }; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx index aae92622dbe0a..4a8243eb0269a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx @@ -5,8 +5,9 @@ * 2.0. */ import React, { useMemo, useReducer } from 'react'; - +import { identity } from 'lodash'; import { render, screen, within, fireEvent, waitFor } from '@testing-library/react'; +import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import { AlertsTable } from '../alerts_table'; import { @@ -44,6 +45,13 @@ const columns = [ }, ]; +const mockFieldFormatsRegistry = { + deserialize: jest.fn().mockImplementation(() => ({ + id: 'string', + convert: jest.fn().mockImplementation(identity), + })), +} as unknown as FieldFormatsRegistry; + const mockCaseService = createCasesServiceMock(); const mockKibana = jest.fn().mockReturnValue({ @@ -58,6 +66,62 @@ const mockKibana = jest.fn().mockReturnValue({ }, }); +const oldAlertsData = [ + [ + { + field: AlertsField.name, + value: ['one'], + }, + { + field: AlertsField.reason, + value: ['two'], + }, + ], + [ + { + field: AlertsField.name, + value: ['three'], + }, + { + field: AlertsField.reason, + value: ['four'], + }, + ], +] as FetchAlertData['oldAlertsData']; + +const ecsAlertsData = [ + [ + { + '@timestamp': ['2023-01-28T10:48:49.559Z'], + _id: 'SomeId', + _index: 'SomeIndex', + kibana: { + alert: { + rule: { + name: ['one'], + }, + reason: ['two'], + }, + }, + }, + ], + [ + { + '@timestamp': ['2023-01-27T10:48:49.559Z'], + _id: 'SomeId2', + _index: 'SomeIndex', + kibana: { + alert: { + rule: { + name: ['three'], + }, + reason: ['four'], + }, + }, + }, + ], +] as FetchAlertData['ecsAlertsData']; + jest.mock('@kbn/kibana-react-plugin/public', () => { const original = jest.requireActual('@kbn/kibana-react-plugin/public'); @@ -166,23 +230,33 @@ describe('AlertsTable.BulkActions', () => { columns, deletedEventIds: [], disabledCellActions: [], - pageSize: 2, pageSizeOptions: [2, 4], leadingControlColumns: [], trailingControlColumns: [], - useFetchAlertsData: () => alertsData, visibleColumns: columns.map((c) => c.id), 'data-test-subj': 'testTable', - updatedAt: Date.now(), onToggleColumn: () => {}, onResetColumns: () => {}, onChangeVisibleColumns: () => {}, browserFields: {}, query: {}, + pagination: { pageIndex: 0, pageSize: 1 }, + sort: [], + isLoading: false, + alerts, + oldAlertsData, + ecsAlertsData, + getInspectQuery: () => ({ request: [], response: [] }), + refetch: refreshMockFn, + alertsCount: alerts.length, + onSortChange: () => {}, + onPageChange: () => {}, + fieldFormats: mockFieldFormatsRegistry, }; const tablePropsWithBulkActions = { ...tableProps, + pagination: { pageIndex: 0, pageSize: 10 }, alertsTableConfiguration: { ...alertsTableConfiguration, @@ -239,6 +313,7 @@ describe('AlertsTable.BulkActions', () => { isAllSelected: false, areAllVisibleRowsSelected: false, rowCount: 2, + updatedAt: Date.now(), }; const AlertsTableWithBulkActionsContext: React.FunctionComponent< @@ -350,6 +425,7 @@ describe('AlertsTable.BulkActions', () => { rowCount: 1, rowSelection: new Map([[0, { isLoading: false }]]), }, + alerts: newAlertsData.alerts, alertsTableConfiguration: { ...alertsTableConfiguration, useBulkActions: () => [ @@ -492,13 +568,14 @@ describe('AlertsTable.BulkActions', () => { _id: 'alert2', }, ] as unknown as Alerts; + const allAlerts = [...alerts, ...secondPageAlerts]; const props = { ...tablePropsWithBulkActions, - alerts: secondPageAlerts, + alerts: allAlerts, + alertsCount: allAlerts.length, useFetchAlertsData: () => { return { ...alertsData, - alerts: secondPageAlerts, alertsCount: secondPageAlerts.length, activePage: 1, }; @@ -508,6 +585,7 @@ describe('AlertsTable.BulkActions', () => { areAllVisibleRowsSelected: true, rowSelection: new Map([[0, { isLoading: false }]]), }, + pagination: { pageIndex: 1, pageSize: 2 }, }; render(); @@ -965,7 +1043,6 @@ describe('AlertsTable.BulkActions', () => { it('should call refresh function of use fetch alerts when bulk action 3 is clicked', async () => { const props = { ...tablePropsWithBulkActions, - initialBulkActionsState: { ...defaultBulkActionsState, areAllVisibleRowsSelected: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/components/column_header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/components/column_header.tsx index 5fcd2d3eb348c..e51c14b710921 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/components/column_header.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/components/column_header.tsx @@ -6,7 +6,7 @@ */ import { EuiCheckbox } from '@elastic/eui'; -import React, { ChangeEvent, useContext } from 'react'; +import React, { ChangeEvent, useContext, useCallback } from 'react'; import { BulkActionsVerbs } from '../../../../../types'; import { COLUMN_HEADER_ARIA_LABEL } from '../translations'; import { AlertsTableContext } from '../../contexts/alerts_table_context'; @@ -16,18 +16,23 @@ const BulkActionsHeaderComponent: React.FunctionComponent = () => { bulkActions: [{ isAllSelected, areAllVisibleRowsSelected }, updateSelectedRows], } = useContext(AlertsTableContext); + const onChange = useCallback( + (e: ChangeEvent) => { + if (e.target.checked) { + updateSelectedRows({ action: BulkActionsVerbs.selectCurrentPage }); + } else { + updateSelectedRows({ action: BulkActionsVerbs.clear }); + } + }, + [updateSelectedRows] + ); + return ( ) => { - if (e.target.checked) { - updateSelectedRows({ action: BulkActionsVerbs.selectCurrentPage }); - } else { - updateSelectedRows({ action: BulkActionsVerbs.clear }); - } - }} + onChange={onChange} data-test-subj="bulk-actions-header" /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/components/row_cell.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/components/row_cell.tsx index 4e029f1eaf38a..efa91e90f1a51 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/components/row_cell.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/components/row_cell.tsx @@ -6,7 +6,7 @@ */ import { EuiCheckbox, EuiLoadingSpinner } from '@elastic/eui'; -import React, { ChangeEvent } from 'react'; +import React, { ChangeEvent, useCallback } from 'react'; import { useContext } from 'react'; import { AlertsTableContext } from '../../contexts/alerts_table_context'; import { BulkActionsVerbs } from '../../../../../types'; @@ -17,7 +17,16 @@ const BulkActionsRowCellComponent = ({ rowIndex }: { rowIndex: number }) => { } = useContext(AlertsTableContext); const isChecked = rowSelection.has(rowIndex); const isLoading = isChecked && rowSelection.get(rowIndex)?.isLoading; - + const onChange = useCallback( + (e: ChangeEvent) => { + if (e.target.checked) { + updateSelectedRows({ action: BulkActionsVerbs.add, rowIndex }); + } else { + updateSelectedRows({ action: BulkActionsVerbs.delete, rowIndex }); + } + }, + [rowIndex, updateSelectedRows] + ); if (isLoading) { return ; } @@ -29,13 +38,7 @@ const BulkActionsRowCellComponent = ({ rowIndex }: { rowIndex: number }) => { ) => { - if (e.target.checked) { - updateSelectedRows({ action: BulkActionsVerbs.add, rowIndex }); - } else { - updateSelectedRows({ action: BulkActionsVerbs.delete, rowIndex }); - } - }} + onChange={onChange} data-test-subj="bulk-actions-row-cell" /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/get_leading_control_column.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/get_leading_control_column.tsx index b249321311ee9..2cf6b678cf2a3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/get_leading_control_column.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/get_leading_control_column.tsx @@ -5,23 +5,25 @@ * 2.0. */ -import React from 'react'; +import React, { memo } from 'react'; import { EuiDataGridCellValueElementProps, EuiDataGridControlColumn } from '@elastic/eui'; import { BulkActionsHeader, BulkActionsRowCell } from './components'; export type GetLeadingControlColumn = () => EuiDataGridControlColumn; -export const getLeadingControlColumn: GetLeadingControlColumn = (): EuiDataGridControlColumn => ({ - id: 'bulkActions', - width: 30, - headerCellRender: () => { - return ; - }, - rowCellRender: (cveProps: EuiDataGridCellValueElementProps) => { +const BulkActionLeadingControlColumnRowCellRender = memo( + (cveProps: EuiDataGridCellValueElementProps) => { const { visibleRowIndex: rowIndex } = cveProps as EuiDataGridCellValueElementProps & { visibleRowIndex: number; }; return ; - }, + } +); + +export const getLeadingControlColumn: GetLeadingControlColumn = (): EuiDataGridControlColumn => ({ + id: 'bulkActions', + width: 30, + headerCellRender: BulkActionsHeader, + rowCellRender: BulkActionLeadingControlColumnRowCellRender, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/reducer.ts index db9c8523fb0af..38f79e1eba9e0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/reducer.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/reducer.ts @@ -35,6 +35,7 @@ export const bulkActionsReducer = ( nextState.isAllSelected = false; } else if (action === BulkActionsVerbs.rowCountUpdate && rowCount !== undefined) { nextState.rowCount = rowCount; + nextState.updatedAt = Date.now(); } else if (action === BulkActionsVerbs.updateAllLoadingState) { const nextRowSelection = new Map( Array.from(rowSelection.keys()).map((idx: number) => [idx, { isLoading }]) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cases/cell.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cases/cell.tsx index 005e6db83d188..9c81da7906313 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cases/cell.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cases/cell.tsx @@ -30,7 +30,7 @@ const CasesCellComponent: React.FC = (props) => { const { isLoading, alert, cases, caseAppId } = props; const { navigateToCaseView } = useCaseViewNavigation(caseAppId); - const caseIds = alert[ALERT_CASE_IDS] ?? []; + const caseIds = (alert && alert[ALERT_CASE_IDS]) ?? []; const validCases = caseIds .map((id) => cases.get(id)) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/alert_lifecycle_status_cell.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/alert_lifecycle_status_cell.tsx index 27f5558a13035..c050ac2e4a3e2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/alert_lifecycle_status_cell.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/alert_lifecycle_status_cell.tsx @@ -28,10 +28,10 @@ const AlertLifecycleStatusCellComponent: React.FC = (props) return null; } - const alertStatus = alert[ALERT_STATUS] ?? []; + const alertStatus = (alert && alert[ALERT_STATUS]) ?? []; if (Array.isArray(alertStatus) && alertStatus.length) { - const flapping = alert[ALERT_FLAPPING] ?? []; + const flapping = (alert && alert[ALERT_FLAPPING]) ?? []; return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/default_cell.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/default_cell.tsx index 2ecd96c2e8bcd..d34bc07b90df5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/default_cell.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/default_cell.tsx @@ -9,7 +9,7 @@ import React, { memo } from 'react'; import { CellComponentProps } from '../types'; const DefaultCellComponent: React.FC = ({ columnId, alert }) => { - const value = alert[columnId] ?? []; + const value = (alert && alert[columnId]) ?? []; if (Array.isArray(value)) { return <>{value.length ? value.join(', ') : '--'}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/render_cell_value.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/render_cell_value.tsx index 1bd62f120497a..2d77b8a94e46a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/render_cell_value.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/render_cell_value.tsx @@ -6,7 +6,7 @@ */ import { isEmpty } from 'lodash'; -import React, { type ReactNode } from 'react'; +import React from 'react'; import { ALERT_DURATION, AlertConsumers, @@ -22,16 +22,10 @@ import { FieldFormatParams, FieldFormatsRegistry, } from '@kbn/field-formats-plugin/common'; -import { EuiBadge, EuiLink } from '@elastic/eui'; +import { EuiBadge, EuiLink, RenderCellValue } from '@elastic/eui'; import { alertProducersData, observabilityFeatureIds } from '../constants'; -import { GetRenderCellValue } from '../../../../types'; import { useKibana } from '../../../../common/lib/kibana'; -interface Props { - columnId: string; - data: any; -} - export const getMappedNonEcsValue = ({ data, fieldName, @@ -59,22 +53,17 @@ const getRenderValue = (mappedNonEcsValue: any) => { return '—'; }; -export const getRenderCellValue = (fieldFormats: FieldFormatsRegistry): GetRenderCellValue => { +export const getRenderCellValue: RenderCellValue = ({ columnId, data, fieldFormats }) => { const alertValueFormatter = getAlertFormatters(fieldFormats); + if (data == null) return null; - return () => - (props): ReactNode => { - const { columnId, data } = props as Props; - if (data == null) return null; - - const mappedNonEcsValue = getMappedNonEcsValue({ - data, - fieldName: columnId, - }); - const value = getRenderValue(mappedNonEcsValue); + const mappedNonEcsValue = getMappedNonEcsValue({ + data, + fieldName: columnId, + }); + const value = getRenderValue(mappedNonEcsValue); - return alertValueFormatter(columnId, value, data); - }; + return alertValueFormatter(columnId, value, data); }; const defaultParam: Record = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/configuration.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/configuration.tsx index a170e67adc5b5..59e160cb77289 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/configuration.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/configuration.tsx @@ -159,7 +159,7 @@ export const createGenericAlertsTableConfigurations = ( { id: ALERT_TABLE_GENERIC_CONFIG_ID, columns: [firstColumn, ...genericColumns], - getRenderCellValue: getRenderCellValue(fieldFormats), + getRenderCellValue, useInternalFlyout: getDefaultAlertFlyout(columns, getAlertFormatters(fieldFormats)), sort, useActionsColumn, @@ -167,7 +167,7 @@ export const createGenericAlertsTableConfigurations = ( { id: ALERT_TABLE_GLOBAL_CONFIG_ID, columns, - getRenderCellValue: getRenderCellValue(fieldFormats), + getRenderCellValue, useInternalFlyout: getDefaultAlertFlyout(columns, getAlertFormatters(fieldFormats)), sort, useActionsColumn, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_alert_muted_state.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_alert_muted_state.ts index ea2ffa2f21bc9..e59a683d68b91 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_alert_muted_state.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_alert_muted_state.ts @@ -10,10 +10,10 @@ import { ALERT_INSTANCE_ID, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; import { AlertsTableContext } from '../../../../..'; import { Alert } from '../../../../../types'; -export const useAlertMutedState = (alert: Alert) => { +export const useAlertMutedState = (alert?: Alert) => { const { mutedAlerts } = useContext(AlertsTableContext); - const alertInstanceId = alert[ALERT_INSTANCE_ID]?.[0]; - const ruleId = alert[ALERT_RULE_UUID]?.[0]; + const alertInstanceId = alert && alert[ALERT_INSTANCE_ID]?.[0]; + const ruleId = alert && alert[ALERT_RULE_UUID]?.[0]; return useMemo(() => { const rule = ruleId ? mutedAlerts[ruleId] : []; return { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_actions_column.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_actions_column.ts index c8cee91b579e9..0378388474d48 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_actions_column.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_actions_column.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useContext } from 'react'; +import { useCallback, useContext, useMemo } from 'react'; import { AlertsTableContext } from '../contexts/alerts_table_context'; import { UseActionsColumnRegistry, BulkActionsVerbs } from '../../../../types'; @@ -20,12 +20,15 @@ export const useActionsColumn = ({ options }: UseActionsColumnProps) => { bulkActions: [, updateBulkActionsState], } = useContext(AlertsTableContext); - const useUserActionsColumn = options - ? options - : () => ({ - renderCustomActionsRow: undefined, - width: undefined, - }); + const defaultActionsColum = useCallback( + () => ({ + renderCustomActionsRow: undefined, + width: undefined, + }), + [] + ); + + const useUserActionsColumn = options ? options : defaultActionsColum; const { renderCustomActionsRow, width: actionsColumnWidth = DEFAULT_ACTIONS_COLUMNS_WIDTH } = useUserActionsColumn(); @@ -44,9 +47,11 @@ export const useActionsColumn = ({ options }: UseActionsColumnProps) => { [updateBulkActionsState] ); - return { - renderCustomActionsRow, - actionsColumnWidth, - getSetIsActionLoadingCallback, - }; + return useMemo(() => { + return { + renderCustomActionsRow, + actionsColumnWidth, + getSetIsActionLoadingCallback, + }; + }, [renderCustomActionsRow, actionsColumnWidth, getSetIsActionLoadingCallback]); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts index aec60b89e5e1e..3bc36d1bdd049 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts @@ -105,8 +105,10 @@ export const useBulkAddToCaseActions = ({ }: UseBulkAddToCaseActionsProps): BulkActionsConfig[] => { const { cases: casesService } = useKibana<{ cases?: CasesService }>().services; - const userCasesPermissions = casesService?.helpers.canUseCases(casesConfig?.owner ?? []); - const CasesContext = casesService?.ui.getCasesContext(); + const userCasesPermissions = useMemo(() => { + return casesService?.helpers.canUseCases(casesConfig?.owner ?? []); + }, [casesConfig?.owner, casesService]); + const CasesContext = useMemo(() => casesService?.ui.getCasesContext(), [casesService]); const isCasesContextAvailable = Boolean(casesService && CasesContext); const onSuccess = useCallback(() => { @@ -197,50 +199,66 @@ export const useBulkUntrackActions = ({ const { mutateAsync: untrackAlerts } = useBulkUntrackAlerts(); const { mutateAsync: untrackAlertsByQuery } = useBulkUntrackAlertsByQuery(); - // Check if at least one Observability feature is enabled - if (!application?.capabilities) return []; - const hasApmPermission = application.capabilities.apm?.['alerting:show']; - const hasInfrastructurePermission = application.capabilities.infrastructure?.show; - const hasLogsPermission = application.capabilities.logs?.show; - const hasUptimePermission = application.capabilities.uptime?.show; - const hasSloPermission = application.capabilities.slo?.show; - const hasObservabilityPermission = application.capabilities.observability?.show; + const hasApmPermission = application?.capabilities.apm?.['alerting:show']; + const hasInfrastructurePermission = application?.capabilities.infrastructure?.show; + const hasLogsPermission = application?.capabilities.logs?.show; + const hasUptimePermission = application?.capabilities.uptime?.show; + const hasSloPermission = application?.capabilities.slo?.show; + const hasObservabilityPermission = application?.capabilities.observability?.show; - if ( - !hasApmPermission && - !hasInfrastructurePermission && - !hasLogsPermission && - !hasUptimePermission && - !hasSloPermission && - !hasObservabilityPermission - ) - return []; - - return [ - { - label: MARK_AS_UNTRACKED, - key: 'mark-as-untracked', - disableOnQuery: false, - disabledLabel: MARK_AS_UNTRACKED, - 'data-test-subj': 'mark-as-untracked', - onClick: async (alerts?: TimelineItem[]) => { - if (!alerts) return; - const alertUuids = alerts.map((alert) => alert._id); - const indices = alerts.map((alert) => alert._index ?? ''); - try { - setIsBulkActionsLoading(true); - if (isAllSelected) { - await untrackAlertsByQuery({ query, featureIds }); - } else { - await untrackAlerts({ indices, alertUuids }); + return useMemo(() => { + // Check if at least one Observability feature is enabled + if (!application?.capabilities) return []; + if ( + !hasApmPermission && + !hasInfrastructurePermission && + !hasLogsPermission && + !hasUptimePermission && + !hasSloPermission && + !hasObservabilityPermission + ) + return []; + return [ + { + label: MARK_AS_UNTRACKED, + key: 'mark-as-untracked', + disableOnQuery: false, + disabledLabel: MARK_AS_UNTRACKED, + 'data-test-subj': 'mark-as-untracked', + onClick: async (alerts?: TimelineItem[]) => { + if (!alerts) return; + const alertUuids = alerts.map((alert) => alert._id); + const indices = alerts.map((alert) => alert._index ?? ''); + try { + setIsBulkActionsLoading(true); + if (isAllSelected) { + await untrackAlertsByQuery({ query, featureIds }); + } else { + await untrackAlerts({ indices, alertUuids }); + } + onSuccess(); + } finally { + setIsBulkActionsLoading(false); } - onSuccess(); - } finally { - setIsBulkActionsLoading(false); - } + }, }, - }, - ]; + ]; + }, [ + onSuccess, + setIsBulkActionsLoading, + untrackAlerts, + application?.capabilities, + hasApmPermission, + hasInfrastructurePermission, + hasLogsPermission, + hasUptimePermission, + hasSloPermission, + hasObservabilityPermission, + featureIds, + query, + isAllSelected, + untrackAlertsByQuery, + ]); }; export function useBulkActions({ @@ -254,14 +272,17 @@ export function useBulkActions({ const { bulkActions: [bulkActionsState, updateBulkActionsState], } = useContext(AlertsTableContext); - const configBulkActionPanels = useBulkActionsConfig(query); + const configBulkActionPanels = useBulkActionsConfig(query, refresh); const clearSelection = useCallback(() => { updateBulkActionsState({ action: BulkActionsVerbs.clear }); }, [updateBulkActionsState]); - const setIsBulkActionsLoading = (isLoading: boolean = true) => { - updateBulkActionsState({ action: BulkActionsVerbs.updateAllLoadingState, isLoading }); - }; + const setIsBulkActionsLoading = useCallback( + (isLoading: boolean = true) => { + updateBulkActionsState({ action: BulkActionsVerbs.updateAllLoadingState, isLoading }); + }, + [updateBulkActionsState] + ); const caseBulkActions = useBulkAddToCaseActions({ casesConfig, refresh, clearSelection }); const untrackBulkActions = useBulkUntrackActions({ setIsBulkActionsLoading, @@ -271,32 +292,42 @@ export function useBulkActions({ featureIds, isAllSelected: bulkActionsState.isAllSelected, }); - - const initialItems = [ - ...caseBulkActions, - // SECURITY SOLUTION WORKAROUND: Disable untrack action for SIEM - ...(featureIds?.includes('siem') ? [] : untrackBulkActions), - ]; - - const bulkActions = initialItems.length - ? addItemsToInitialPanel({ - panels: configBulkActionPanels, - items: initialItems, - }) - : configBulkActionPanels; + const initialItems = useMemo(() => { + return [...caseBulkActions, ...(featureIds?.includes('siem') ? [] : untrackBulkActions)]; + }, [caseBulkActions, featureIds, untrackBulkActions]); + const bulkActions = useMemo(() => { + return initialItems.length + ? addItemsToInitialPanel({ + panels: configBulkActionPanels, + items: initialItems, + }) + : configBulkActionPanels; + }, [configBulkActionPanels, initialItems]); const isBulkActionsColumnActive = bulkActions.length !== 0; useEffect(() => { - updateBulkActionsState({ action: BulkActionsVerbs.rowCountUpdate, rowCount: alerts.length }); + updateBulkActionsState({ + action: BulkActionsVerbs.rowCountUpdate, + rowCount: alerts.length, + }); }, [alerts, updateBulkActionsState]); - - return { - isBulkActionsColumnActive, - getBulkActionsLeadingControlColumn, - bulkActionsState, + return useMemo(() => { + return { + isBulkActionsColumnActive, + getBulkActionsLeadingControlColumn, + bulkActionsState, + bulkActions, + setIsBulkActionsLoading, + clearSelection, + updateBulkActionsState, + }; + }, [ bulkActions, - setIsBulkActionsLoading, + bulkActionsState, clearSelection, - }; + isBulkActionsColumnActive, + setIsBulkActionsLoading, + updateBulkActionsState, + ]); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.test.tsx index fdc7282f6c817..b3a9ba275e6aa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.test.tsx @@ -83,7 +83,6 @@ const expectedResponse: FetchAlertResp = { refetch: expect.anything(), isInitializing: true, totalAlerts: -1, - updatedAt: 0, oldAlertsData: [], ecsAlertsData: [], }; @@ -162,7 +161,6 @@ describe('useFetchAlerts', () => { ], totalAlerts: 2, isInitializing: false, - updatedAt: 1609502400000, getInspectQuery: expect.anything(), refetch: expect.anything(), ecsAlertsData: [ @@ -281,7 +279,6 @@ describe('useFetchAlerts', () => { refetch: expect.anything(), isInitializing: true, totalAlerts: -1, - updatedAt: 0, }, ]); }); @@ -300,7 +297,6 @@ describe('useFetchAlerts', () => { refetch: expect.anything(), isInitializing: true, totalAlerts: -1, - updatedAt: 0, }, ]); @@ -321,7 +317,6 @@ describe('useFetchAlerts', () => { refetch: expect.anything(), isInitializing: true, totalAlerts: -1, - updatedAt: 0, }, ]); }); @@ -432,7 +427,6 @@ describe('useFetchAlerts', () => { refetch: expect.anything(), isInitializing: true, totalAlerts: -1, - updatedAt: 0, }, ]); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx index 9b62174661e62..5a0a5efb18c32 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx @@ -63,7 +63,6 @@ export interface FetchAlertResp { getInspectQuery: GetInspectQuery; refetch: Refetch; totalAlerts: number; - updatedAt: number; } type AlertResponseState = Omit; @@ -105,7 +104,6 @@ const initialAlertState: AlertStateReducer = { ecsAlertsData: [], totalAlerts: -1, isInitializing: true, - updatedAt: 0, }, }; @@ -123,7 +121,6 @@ function alertReducer(state: AlertStateReducer, action: AlertActions) { totalAlerts: action.totalAlerts, oldAlertsData: action.oldAlertsData, ecsAlertsData: action.ecsAlertsData, - updatedAt: Date.now(), }, }; case 'resetPagination': diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts index fb2e0f81b0f0c..e713c1820cb81 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts @@ -14,7 +14,6 @@ type PaginationProps = RuleRegistrySearchRequestPagination & { }; export type UsePagination = (props: PaginationProps) => { - pagination: RuleRegistrySearchRequestPagination; onChangePageSize: (pageSize: number) => void; onChangePageIndex: (pageIndex: number) => void; onPaginateFlyoutNext: () => void; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/maintenance_windows/cell.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/maintenance_windows/cell.tsx index 9a654f46560d4..e46807d9b38eb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/maintenance_windows/cell.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/maintenance_windows/cell.tsx @@ -71,14 +71,14 @@ export const MaintenanceWindowCell = memo((props: CellComponentProps) => { const { alert, maintenanceWindows, isLoading } = props; const validMaintenanceWindows = useMemo(() => { - const maintenanceWindowIds = alert[ALERT_MAINTENANCE_WINDOW_IDS] || []; + const maintenanceWindowIds = (alert && alert[ALERT_MAINTENANCE_WINDOW_IDS]) || []; return maintenanceWindowIds .map((id) => maintenanceWindows.get(id)) .filter(isMaintenanceWindowValid); }, [alert, maintenanceWindows]); const idsWithoutMaintenanceWindow = useMemo(() => { - const maintenanceWindowIds = alert[ALERT_MAINTENANCE_WINDOW_IDS] || []; + const maintenanceWindowIds = (alert && alert[ALERT_MAINTENANCE_WINDOW_IDS]) || []; return maintenanceWindowIds.filter((id) => !maintenanceWindows.get(id)); }, [alert, maintenanceWindows]); @@ -91,7 +91,7 @@ export const MaintenanceWindowCell = memo((props: CellComponentProps) => { maintenanceWindows={validMaintenanceWindows} maintenanceWindowIds={idsWithoutMaintenanceWindow} isLoading={isLoading} - timestamp={alert[TIMESTAMP]?.[0]} + timestamp={alert && alert[TIMESTAMP]?.[0]} /> ); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.tsx index 1a4215a55ca17..2be7ae9b15bcc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.tsx @@ -6,7 +6,7 @@ */ import { EuiButtonIcon } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useState, memo, useCallback } from 'react'; import { GetInspectQuery } from '../../../../../../types'; import { HoverVisibilityContainer } from './hover_visibility_container'; @@ -15,19 +15,19 @@ import { ModalInspectQuery } from './modal'; import * as i18n from './translations'; export const BUTTON_CLASS = 'inspectButtonComponent'; +const VISIBILITY_CLASSES = [BUTTON_CLASS]; interface InspectButtonContainerProps { hide?: boolean; children: React.ReactNode; } -export const InspectButtonContainer: React.FC = ({ - children, - hide, -}) => ( - - {children} - +export const InspectButtonContainer: React.FC = memo( + ({ children, hide }) => ( + + {children} + + ) ); interface InspectButtonProps { @@ -43,13 +43,13 @@ const InspectButtonComponent: React.FC = ({ }) => { const [isShowingModal, setIsShowingModal] = useState(false); - const onOpenModal = () => { + const onOpenModal = useCallback(() => { setIsShowingModal(true); - }; + }, []); - const onCloseModal = () => { + const onCloseModal = useCallback(() => { setIsShowingModal(false); - }; + }, []); return ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/toolbar_visibility.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/toolbar_visibility.tsx index 48742adcf6ea2..54e2466b524cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/toolbar_visibility.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/toolbar_visibility.tsx @@ -9,9 +9,10 @@ import { EuiDataGridToolBarAdditionalControlsOptions, EuiDataGridToolBarVisibilityOptions, } from '@elastic/eui'; -import React, { lazy, Suspense } from 'react'; +import React, { lazy, Suspense, memo, useMemo, useContext } from 'react'; import { BrowserFields } from '@kbn/rule-registry-plugin/common'; import { AlertsCount } from './components/alerts_count/alerts_count'; +import { AlertsTableContext } from '../contexts/alerts_table_context'; import type { Alerts, BulkActionsPanelConfig, @@ -26,31 +27,69 @@ import { ALERTS_TABLE_TITLE } from '../translations'; const BulkActionsToolbar = lazy(() => import('../bulk_actions/components/toolbar')); -const rightControl = ({ - controls, - updatedAt, - getInspectQuery, - showInspectButton, -}: { - controls?: EuiDataGridToolBarAdditionalControlsOptions; - updatedAt: number; - getInspectQuery: GetInspectQuery; - showInspectButton: boolean; -}) => { - return ( - <> - {showInspectButton && ( - - )} - - {controls?.right} - - ); -}; +const RightControl = memo( + ({ + controls, + getInspectQuery, + showInspectButton, + }: { + controls?: EuiDataGridToolBarAdditionalControlsOptions; + getInspectQuery: GetInspectQuery; + showInspectButton: boolean; + }) => { + const { + bulkActions: [bulkActionsState], + } = useContext(AlertsTableContext); + return ( + <> + {showInspectButton && ( + + )} + + {controls?.right} + + ); + } +); + +const LeftAppendControl = memo( + ({ + alertsCount, + hasBrowserFields, + columnIds, + browserFields, + onResetColumns, + onToggleColumn, + fieldBrowserOptions, + }: { + alertsCount: number; + columnIds: string[]; + onToggleColumn: (columnId: string) => void; + onResetColumns: () => void; + controls?: EuiDataGridToolBarAdditionalControlsOptions; + fieldBrowserOptions?: FieldBrowserOptions; + hasBrowserFields: boolean; + browserFields: BrowserFields; + }) => { + return ( + <> + + {hasBrowserFields && ( + + )} + + ); + } +); -const getDefaultVisibility = ({ +const useGetDefaultVisibility = ({ alertsCount, - updatedAt, columnIds, onToggleColumn, onResetColumns, @@ -59,9 +98,9 @@ const getDefaultVisibility = ({ fieldBrowserOptions, getInspectQuery, showInspectButton, + toolbarVisibilityProp, }: { alertsCount: number; - updatedAt: number; columnIds: string[]; onToggleColumn: (columnId: string) => void; onResetColumns: () => void; @@ -70,44 +109,58 @@ const getDefaultVisibility = ({ fieldBrowserOptions?: FieldBrowserOptions; getInspectQuery: GetInspectQuery; showInspectButton: boolean; + toolbarVisibilityProp?: EuiDataGridToolBarVisibilityOptions; }): EuiDataGridToolBarVisibilityOptions => { - const hasBrowserFields = Object.keys(browserFields).length > 0; - const additionalControls = { - right: rightControl({ controls, updatedAt, getInspectQuery, showInspectButton }), - left: { - append: ( - <> - - {hasBrowserFields && ( - { + const hasBrowserFields = Object.keys(browserFields).length > 0; + return { + additionalControls: { + right: ( + + ), + left: { + append: ( + - )} - - ), - }, - }; - - return { - additionalControls, - showColumnSelector: { - allowHide: false, - }, - showSortSelector: true, - }; + ), + }, + }, + showColumnSelector: { + allowHide: false, + }, + showSortSelector: true, + }; + }, [ + alertsCount, + browserFields, + columnIds, + fieldBrowserOptions, + getInspectQuery, + onResetColumns, + onToggleColumn, + showInspectButton, + controls, + ]); + return defaultVisibility; }; -export const getToolbarVisibility = ({ +export const useGetToolbarVisibility = ({ bulkActions, alertsCount, rowSelection, alerts, isLoading, - updatedAt, columnIds, onToggleColumn, onResetColumns, @@ -126,7 +179,6 @@ export const getToolbarVisibility = ({ rowSelection: RowSelection; alerts: Alerts; isLoading: boolean; - updatedAt: number; columnIds: string[]; onToggleColumn: (columnId: string) => void; onResetColumns: () => void; @@ -141,9 +193,20 @@ export const getToolbarVisibility = ({ toolbarVisibilityProp?: EuiDataGridToolBarVisibilityOptions; }): EuiDataGridToolBarVisibilityOptions => { const selectedRowsCount = rowSelection.size; - const defaultVisibility = getDefaultVisibility({ + const defaultVisibilityProps = useMemo(() => { + return { + alertsCount, + columnIds, + onToggleColumn, + onResetColumns, + browserFields, + controls, + fieldBrowserOptions, + getInspectQuery, + showInspectButton, + }; + }, [ alertsCount, - updatedAt, columnIds, onToggleColumn, onResetColumns, @@ -152,41 +215,64 @@ export const getToolbarVisibility = ({ fieldBrowserOptions, getInspectQuery, showInspectButton, - }); - const isBulkActionsActive = - selectedRowsCount === 0 || selectedRowsCount === undefined || bulkActions.length === 0; - - if (isBulkActionsActive) - return { - ...defaultVisibility, - ...(toolbarVisibilityProp ?? {}), - }; + ]); + const defaultVisibility = useGetDefaultVisibility(defaultVisibilityProps); + const options = useMemo(() => { + const isBulkActionsActive = + selectedRowsCount === 0 || selectedRowsCount === undefined || bulkActions.length === 0; - const options = { - showColumnSelector: false, - showSortSelector: false, - additionalControls: { - right: rightControl({ controls, updatedAt, getInspectQuery, showInspectButton }), - left: { - append: ( - <> - - - - - - ), - }, - }, - ...(toolbarVisibilityProp ?? {}), - }; + if (isBulkActionsActive) { + return { + ...defaultVisibility, + ...(toolbarVisibilityProp ?? {}), + }; + } else { + return { + showColumnSelector: false, + showSortSelector: false, + additionalControls: { + right: ( + + ), + left: { + append: ( + <> + + + + + + ), + }, + }, + ...(toolbarVisibilityProp ?? {}), + }; + } + }, [ + alertsCount, + bulkActions, + defaultVisibility, + selectedRowsCount, + toolbarVisibilityProp, + alerts, + clearSelection, + refresh, + setIsBulkActionsLoading, + controls, + getInspectQuery, + showInspectButton, + ]); return options; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/types.ts index 6ee30b24decdc..819cc42bd90e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/types.ts @@ -23,7 +23,7 @@ export interface Consumer { export type ServerError = IHttpFetchError; export interface CellComponentProps { - alert: Alert; + alert?: Alert; cases: AlertsTableProps['cases']['data']; maintenanceWindows: AlertsTableProps['maintenanceWindows']['data']; columnId: SystemCellId; diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index a23388e24abd8..207a5b56b6558 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -37,7 +37,6 @@ export type { AlertsTableFlyoutBaseProps, RuleEventLogListProps, AlertTableFlyoutComponent, - GetRenderCellValue, FieldBrowserOptions, FieldBrowserProps, RuleDefinitionProps, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index ccaf20e284a82..218b6365457bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -13,6 +13,7 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; import type { IconType, RecursivePartial, @@ -24,6 +25,8 @@ import type { EuiDataGridToolBarVisibilityOptions, EuiSuperSelectOption, EuiDataGridOnColumnResizeHandler, + EuiDataGridCellProps, + RenderCellValue, EuiDataGridCellPopoverElementProps, } from '@elastic/eui'; import type { RuleCreationValidConsumer, ValidFeatureId } from '@kbn/rule-data-utils'; @@ -561,16 +564,14 @@ export type AlertsTableProps = { // defaultCellActions: TGridCellAction[]; deletedEventIds: string[]; disabledCellActions: string[]; - pageSize: number; pageSizeOptions: number[]; id?: string; - leadingControlColumns: EuiDataGridControlColumn[]; + leadingControlColumns?: EuiDataGridControlColumn[]; showAlertStatusWithFlapping?: boolean; - trailingControlColumns: EuiDataGridControlColumn[]; - useFetchAlertsData: () => FetchAlertData; + trailingControlColumns?: EuiDataGridControlColumn[]; + cellContext?: EuiDataGridCellProps['cellContext']; visibleColumns: string[]; 'data-test-subj': string; - updatedAt: number; browserFields: any; onToggleColumn: (columnId: string) => void; onResetColumns: () => void; @@ -589,7 +590,19 @@ export type AlertsTableProps = { */ dynamicRowHeight?: boolean; featureIds?: ValidFeatureId[]; + pagination: RuleRegistrySearchRequestPagination; + sort: SortCombinations[]; + isLoading: boolean; + alerts: Alerts; + oldAlertsData: FetchAlertData['oldAlertsData']; + ecsAlertsData: FetchAlertData['ecsAlertsData']; + getInspectQuery: GetInspectQuery; + refetch: () => void; + alertsCount: number; + onSortChange: (sort: EuiDataGridSorting['columns']) => void; + onPageChange: (pagination: RuleRegistrySearchRequestPagination) => void; renderCellPopover?: ReturnType; + fieldFormats: FieldFormatsRegistry; } & Partial>; export type SetFlyoutAlert = (alertId: string) => void; @@ -599,17 +612,6 @@ export interface TimelineNonEcsData { value?: string[] | null; } -// TODO We need to create generic type between our plugin, right now we have different one because of the old alerts table -export type GetRenderCellValue = ({ - setFlyoutAlert, - context, -}: { - setFlyoutAlert: SetFlyoutAlert; - context?: T; -}) => ( - props: EuiDataGridCellValueElementProps & { data: TimelineNonEcsData[] } -) => React.ReactNode | JSX.Element; - export type GetRenderCellPopover = ({ context, }: { @@ -681,7 +683,8 @@ interface ItemsPanelConfig extends PanelConfig { export type BulkActionsPanelConfig = ItemsPanelConfig | ContentPanelConfig; export type UseBulkActionsRegistry = ( - query: Pick + query: Pick, + refresh: () => void ) => BulkActionsPanelConfig[]; export type UseCellActions = (props: { @@ -750,7 +753,7 @@ export interface AlertsTableConfigurationRegistry { footer: AlertTableFlyoutComponent; }; sort?: SortCombinations[]; - getRenderCellValue?: GetRenderCellValue; + getRenderCellValue?: RenderCellValue; getRenderCellPopover?: GetRenderCellPopover; useActionsColumn?: UseActionsColumnRegistry; useBulkActions?: UseBulkActionsRegistry; @@ -762,11 +765,7 @@ export interface AlertsTableConfigurationRegistry { showInspectButton?: boolean; ruleTypeIds?: string[]; useFetchPageContext?: PreFetchPageContext; -} - -export interface AlertsTableConfigurationRegistryWithActions - extends AlertsTableConfigurationRegistry { - actions: { + actions?: { toggleColumn: (columnId: string) => void; }; } @@ -794,6 +793,7 @@ export interface BulkActionsState { isAllSelected: boolean; areAllVisibleRowsSelected: boolean; rowCount: number; + updatedAt: number; } export type RowSelection = Map; diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts index c23ffd1ac3c39..36ea9aeedce39 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts @@ -227,6 +227,7 @@ export default ({ getService }: FtrProviderContext) => { const actionsButton = await observability.alerts.common.getActionsButtonByIndex(0); await actionsButton.click(); await observability.alerts.common.viewRuleDetailsButtonClick(); + expect( await (await find.byCssSelector('[data-test-subj="breadcrumb first"]')).getVisibleText() ).to.eql('Observability');