diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock.ts new file mode 100644 index 0000000000000..3c65ea53adf9a --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CoverageOverviewRuleSource, + CoverageOverviewRuleActivity, +} from './coverage_overview_route'; + +export const getCoverageOverviewFilterMock = () => ({ + search_term: 'test query', + activity: [CoverageOverviewRuleActivity.Enabled], + source: [CoverageOverviewRuleSource.Prebuilt], +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/constants.ts index 76322812ecf27..85c734bbc077b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/constants.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/constants.ts @@ -6,6 +6,11 @@ */ import { euiPalettePositive } from '@elastic/eui'; +import { + CoverageOverviewRuleActivity, + CoverageOverviewRuleSource, +} from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; export const coverageOverviewPaletteColors = euiPalettePositive(5); @@ -13,6 +18,8 @@ export const coverageOverviewPanelWidth = 160; export const coverageOverviewLegendWidth = 380; +export const coverageOverviewFilterWidth = 300; + /** * Rules count -> color map * @@ -24,3 +31,31 @@ export const coverageOverviewCardColorThresholds = [ { threshold: 3, color: coverageOverviewPaletteColors[1] }, { threshold: 1, color: coverageOverviewPaletteColors[0] }, ]; + +export const ruleActivityFilterDefaultOptions = [ + { + label: CoverageOverviewRuleActivity.Enabled, + }, + { + label: CoverageOverviewRuleActivity.Disabled, + }, +]; + +export const ruleActivityFilterLabelMap: Record = { + [CoverageOverviewRuleActivity.Enabled]: i18n.CoverageOverviewEnabledRuleActivity, + [CoverageOverviewRuleActivity.Disabled]: i18n.CoverageOverviewDisabledRuleActivity, +}; + +export const ruleSourceFilterDefaultOptions = [ + { + label: CoverageOverviewRuleSource.Prebuilt, + }, + { + label: CoverageOverviewRuleSource.Custom, + }, +]; + +export const ruleSourceFilterLabelMap: Record = { + [CoverageOverviewRuleSource.Prebuilt]: i18n.CoverageOverviewElasticRuleSource, + [CoverageOverviewRuleSource.Custom]: i18n.CoverageOverviewCustomRuleSource, +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx similarity index 72% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx index 794a8ca09d1f5..a761153c1e8d0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx @@ -11,32 +11,35 @@ import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hook import { getMockCoverageOverviewDashboard } from '../../../rule_management/model/coverage_overview/__mocks__'; import { TestProviders } from '../../../../common/mock'; -import { CoverageOverviewPage } from './coverage_overview_page'; +import { CoverageOverviewDashboard } from './coverage_overview_dashboard'; +import { CoverageOverviewDashboardContextProvider } from './coverage_overview_dashboard_context'; jest.mock('../../../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null })); jest.mock('../../../rule_management/api/hooks/use_fetch_coverage_overview'); (useFetchCoverageOverviewQuery as jest.Mock).mockReturnValue({ data: getMockCoverageOverviewDashboard(), + isLoading: false, + refetch: jest.fn(), }); const renderCoverageOverviewDashboard = () => { return render( - + + + ); }; -describe('CoverageOverviewPage', () => { +describe('CoverageOverviewDashboard', () => { beforeEach(() => { jest.clearAllMocks(); }); test('it renders', () => { - const wrapper = renderCoverageOverviewDashboard(); - - expect(wrapper.getByTestId('coverageOverviewPage')).toBeInTheDocument(); + renderCoverageOverviewDashboard(); expect(useFetchCoverageOverviewQuery).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.tsx new file mode 100644 index 0000000000000..b9ed3be8ad6ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { HeaderPage } from '../../../../common/components/header_page'; + +import * as i18n from './translations'; +import { CoverageOverviewTacticPanel } from './tactic_panel'; +import { CoverageOverviewMitreTechniquePanelPopover } from './technique_panel_popover'; +import { CoverageOverviewFiltersPanel } from './filters_panel'; +import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context'; + +const CoverageOverviewDashboardComponent = () => { + const { + state: { data }, + } = useCoverageOverviewDashboardContext(); + return ( + <> + + + + + {data?.mitreTactics.map((tactic) => ( + + + + + + {tactic.techniques.map((technique, techniqueKey) => ( + + + + ))} + + ))} + + + ); +}; + +export const CoverageOverviewDashboard = CoverageOverviewDashboardComponent; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx new file mode 100644 index 0000000000000..db96f1a5b8018 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useReducer, +} from 'react'; +import { invariant } from '../../../../../common/utils/invariant'; +import type { + CoverageOverviewRuleActivity, + CoverageOverviewRuleSource, +} from '../../../../../common/api/detection_engine'; +import { BulkActionType } from '../../../../../common/api/detection_engine'; +import type { CoverageOverviewDashboardState } from './coverage_overview_dashboard_reducer'; +import { + SET_SHOW_EXPANDED_CELLS, + SET_RULE_ACTIVITY_FILTER, + SET_RULE_SOURCE_FILTER, + SET_RULE_SEARCH_FILTER, + createCoverageOverviewDashboardReducer, +} from './coverage_overview_dashboard_reducer'; +import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview'; +import { useExecuteBulkAction } from '../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; + +export interface CoverageOverviewDashboardActions { + refetch: () => void; + setShowExpandedCells: (value: boolean) => void; + setRuleActivityFilter: (value: CoverageOverviewRuleActivity[]) => void; + setRuleSourceFilter: (value: CoverageOverviewRuleSource[]) => void; + setRuleSearchFilter: (value: string) => void; + enableAllDisabled: (ruleIds: string[]) => Promise; +} + +export interface CoverageOverviewDashboardContextType { + state: CoverageOverviewDashboardState; + actions: CoverageOverviewDashboardActions; +} + +export const CoverageOverviewDashboardContext = + createContext(null); + +interface CoverageOverviewDashboardContextProviderProps { + children: React.ReactNode; +} + +export const initialState: CoverageOverviewDashboardState = { + showExpandedCells: false, + filter: {}, + data: undefined, + isLoading: false, +}; + +export const CoverageOverviewDashboardContextProvider = ({ + children, +}: CoverageOverviewDashboardContextProviderProps) => { + const [state, dispatch] = useReducer(createCoverageOverviewDashboardReducer(), initialState); + const { data, isLoading, refetch } = useFetchCoverageOverviewQuery(state.filter); + const { executeBulkAction } = useExecuteBulkAction(); + + useEffect(() => { + refetch(); + }, [refetch, state.filter]); + + const setShowExpandedCells = useCallback( + (value: boolean): void => { + dispatch({ + type: SET_SHOW_EXPANDED_CELLS, + value, + }); + }, + [dispatch] + ); + + const setRuleActivityFilter = useCallback( + (value: CoverageOverviewRuleActivity[]): void => { + dispatch({ + type: SET_RULE_ACTIVITY_FILTER, + value, + }); + }, + [dispatch] + ); + + const setRuleSourceFilter = useCallback( + (value: CoverageOverviewRuleSource[]): void => { + dispatch({ + type: SET_RULE_SOURCE_FILTER, + value, + }); + }, + [dispatch] + ); + + const setRuleSearchFilter = useCallback( + (value: string): void => { + dispatch({ + type: SET_RULE_SEARCH_FILTER, + value, + }); + }, + [dispatch] + ); + + const enableAllDisabled = useCallback( + async (ruleIds: string[]) => { + await executeBulkAction({ type: BulkActionType.enable, ids: ruleIds }); + }, + [executeBulkAction] + ); + + const actions = useMemo( + () => ({ + refetch, + setShowExpandedCells, + setRuleActivityFilter, + setRuleSourceFilter, + setRuleSearchFilter, + enableAllDisabled, + }), + [ + refetch, + setRuleActivityFilter, + setRuleSearchFilter, + setRuleSourceFilter, + setShowExpandedCells, + enableAllDisabled, + ] + ); + + const providerValue = useMemo(() => { + return { + state: { ...state, isLoading, data }, + actions, + }; + }, [actions, data, isLoading, state]); + + return ( + + {children} + + ); +}; + +export const useCoverageOverviewDashboardContext = (): CoverageOverviewDashboardContextType => { + const dashboardContext = useContext(CoverageOverviewDashboardContext); + invariant( + dashboardContext, + 'useCoverageOverviewDashboardContext should be used inside CoverageOverviewDashboardContextProvider' + ); + + return dashboardContext; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_reducer.ts new file mode 100644 index 0000000000000..f62835f625647 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_reducer.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CoverageOverviewFilter, + CoverageOverviewRuleActivity, + CoverageOverviewRuleSource, +} from '../../../../../common/api/detection_engine'; +import type { CoverageOverviewDashboard } from '../../../rule_management/model/coverage_overview/dashboard'; + +export interface CoverageOverviewDashboardState { + showExpandedCells: boolean; + filter: CoverageOverviewFilter; + isLoading: boolean; + data: CoverageOverviewDashboard | undefined; +} + +// Action type names +export const SET_SHOW_EXPANDED_CELLS = 'setShowExpandedCells' as const; +export const SET_RULE_ACTIVITY_FILTER = 'setRuleActivityFilter' as const; +export const SET_RULE_SOURCE_FILTER = 'setRuleSourceFilter' as const; +export const SET_RULE_SEARCH_FILTER = 'setRuleSearchFilter' as const; + +export type Action = + | { + type: typeof SET_SHOW_EXPANDED_CELLS; + value: boolean; + } + | { + type: typeof SET_RULE_ACTIVITY_FILTER; + value: CoverageOverviewRuleActivity[]; + } + | { + type: typeof SET_RULE_SOURCE_FILTER; + value: CoverageOverviewRuleSource[]; + } + | { + type: typeof SET_RULE_SEARCH_FILTER; + value: string; + }; + +export const createCoverageOverviewDashboardReducer = + () => + (state: CoverageOverviewDashboardState, action: Action): CoverageOverviewDashboardState => { + switch (action.type) { + case SET_SHOW_EXPANDED_CELLS: { + const { value } = action; + return { ...state, showExpandedCells: value }; + } + case SET_RULE_ACTIVITY_FILTER: { + const { value } = action; + const updatedFilter = { ...state.filter, activity: value.length !== 0 ? value : undefined }; + return { ...state, filter: updatedFilter }; + } + case SET_RULE_SOURCE_FILTER: { + const { value } = action; + const updatedFilter = { ...state.filter, source: value.length !== 0 ? value : undefined }; + return { ...state, filter: updatedFilter }; + } + case SET_RULE_SEARCH_FILTER: { + const { value } = action; + const updatedFilter = { + ...state.filter, + search_term: value.length !== 0 ? value : undefined, + }; + return { ...state, filter: updatedFilter }; + } + default: + return state; + } + }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.tsx deleted file mode 100644 index ae2115d031e50..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useCallback, useReducer } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; -import { SpyRoute } from '../../../../common/utils/route/spy_routes'; -import { SecurityPageName } from '../../../../app/types'; -import { HeaderPage } from '../../../../common/components/header_page'; - -import * as i18n from './translations'; -import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview'; -import { CoverageOverviewTacticPanel } from './tactic_panel'; -import { CoverageOverviewMitreTechniquePanelPopover } from './technique_panel_popover'; -import { CoverageOverviewFiltersPanel } from './filters_panel'; -import { createCoverageOverviewDashboardReducer, initialState } from './reducer'; - -const CoverageOverviewPageComponent = () => { - const { data } = useFetchCoverageOverviewQuery(); - - const [{ showExpandedCells }, dispatch] = useReducer( - createCoverageOverviewDashboardReducer(), - initialState - ); - - const setShowExpandedCells = useCallback( - (value: boolean): void => { - dispatch({ - type: 'setShowExpandedCells', - value, - }); - }, - [dispatch] - ); - - return ( - <> - - - - - - - - {data?.mitreTactics.map((tactic) => ( - - - - - - {tactic.techniques.map((technique, techniqueKey) => ( - - - - ))} - - ))} - - - - ); -}; - -export const CoverageOverviewPage = React.memo(CoverageOverviewPageComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filter_panel.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filter_panel.test.tsx new file mode 100644 index 0000000000000..6931298b7ed48 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filter_panel.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fireEvent, render, within } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../../../../common/mock'; +import { CoverageOverviewFiltersPanel } from './filters_panel'; +import { + ruleActivityFilterDefaultOptions, + ruleActivityFilterLabelMap, + ruleSourceFilterDefaultOptions, + ruleSourceFilterLabelMap, +} from './constants'; +import { + initialState, + useCoverageOverviewDashboardContext, +} from './coverage_overview_dashboard_context'; + +jest.mock('./coverage_overview_dashboard_context'); + +const setShowExpandedCells = jest.fn(); +const setRuleActivityFilter = jest.fn(); +const setRuleSourceFilter = jest.fn(); +const setRuleSearchFilter = jest.fn(); + +const mockCoverageOverviewContextReturn = { + state: initialState, + actions: { + setShowExpandedCells, + setRuleActivityFilter, + setRuleSourceFilter, + setRuleSearchFilter, + }, +}; + +(useCoverageOverviewDashboardContext as jest.Mock).mockReturnValue( + mockCoverageOverviewContextReturn +); + +const renderFiltersPanel = () => { + return render( + + + + ); +}; + +describe('CoverageOverviewFiltersPanel', () => { + test('it correctly populates rule activity filter state', () => { + const wrapper = renderFiltersPanel(); + + wrapper.getByTestId('coverageOverviewRuleActivityFilterButton').click(); + + within(wrapper.getByTestId('coverageOverviewFilterList')) + .getByText(ruleActivityFilterLabelMap[ruleActivityFilterDefaultOptions[0].label]) + .click(); + expect(setRuleActivityFilter).toHaveBeenCalledWith([ruleActivityFilterDefaultOptions[0].label]); + }); + + test('it correctly populates rule source filter state', () => { + const wrapper = renderFiltersPanel(); + + wrapper.getByTestId('coverageOverviewRuleSourceFilterButton').click(); + + within(wrapper.getByTestId('coverageOverviewFilterList')) + .getByText(ruleSourceFilterLabelMap[ruleSourceFilterDefaultOptions[0].label]) + .click(); + expect(setRuleSourceFilter).toHaveBeenCalledWith([ruleSourceFilterDefaultOptions[0].label]); + }); + + test('it correctly populates search filter state', () => { + const wrapper = renderFiltersPanel(); + + fireEvent.change(wrapper.getByTestId('coverageOverviewFilterSearchBar'), { + target: { value: 'test' }, + }); + fireEvent.submit(wrapper.getByTestId('coverageOverviewFilterSearchBar')); + + expect(setRuleSearchFilter).toHaveBeenCalledWith('test'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filters_panel.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filters_panel.tsx index 283f7a77d7036..e234b02257e19 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filters_panel.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filters_panel.tsx @@ -5,42 +5,91 @@ * 2.0. */ -import { EuiFilterButton, EuiFilterGroup, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import React, { memo } from 'react'; +import { + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSearchBar, +} from '@elastic/eui'; +import React, { memo, useCallback } from 'react'; +import { css } from '@emotion/css'; import { CoverageOverviewLegend } from './shared_components/dashboard_legend'; import * as i18n from './translations'; +import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context'; +import { RuleActivityFilter } from './rule_activity_filter'; +import { RuleSourceFilter } from './rule_source_filter'; -export interface CoverageOverviewFiltersPanelProps { - setShowExpandedCells: (arg: boolean) => void; - showExpandedCells: boolean; -} +const CoverageOverviewFiltersPanelComponent = () => { + const { + state: { filter, isLoading, showExpandedCells }, + actions: { + setShowExpandedCells, + setRuleActivityFilter, + setRuleSourceFilter, + setRuleSearchFilter, + }, + } = useCoverageOverviewDashboardContext(); -const CoverageOverviewFiltersPanelComponent = ({ - setShowExpandedCells, - showExpandedCells, -}: CoverageOverviewFiltersPanelProps) => { const handleExpandCellsFilterClick = () => setShowExpandedCells(true); const handleCollapseCellsFilterClick = () => setShowExpandedCells(false); + const handleRuleSearchOnChange = useCallback( + ({ queryText }: { queryText: string }) => { + setRuleSearchFilter(queryText); + }, + [setRuleSearchFilter] + ); + return ( - - - - {i18n.COLLAPSE_CELLS_FILTER_BUTTON} - - - {i18n.EXPAND_CELLS_FILTER_BUTTON} - - + + + + + + + + + + + + + {i18n.COLLAPSE_CELLS_FILTER_BUTTON} + + + {i18n.EXPAND_CELLS_FILTER_BUTTON} + + + + diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts index b6d6c48749a10..5a1aee424352a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts @@ -5,15 +5,23 @@ * 2.0. */ +import type { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; +import { getCoverageOverviewFilterMock } from '../../../../../common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock'; import { getMockCoverageOverviewMitreSubTechnique, getMockCoverageOverviewMitreTactic, getMockCoverageOverviewMitreTechnique, } from '../../../rule_management/model/coverage_overview/__mocks__'; -import { getNumOfCoveredSubtechniques, getNumOfCoveredTechniques } from './helpers'; +import { ruleActivityFilterDefaultOptions } from './constants'; +import { + extractSelected, + getNumOfCoveredSubtechniques, + getNumOfCoveredTechniques, + populateSelected, +} from './helpers'; describe('helpers', () => { - describe('getCoveredTechniques', () => { + describe('getNumOfCoveredTechniques', () => { it('returns 0 when no techniques are present', () => { const payload = getMockCoverageOverviewMitreTactic(); expect(getNumOfCoveredTechniques(payload)).toEqual(0); @@ -31,7 +39,7 @@ describe('helpers', () => { }); }); - describe('getCoveredSubtechniques', () => { + describe('getNumOfCoveredSubtechniques', () => { it('returns 0 when no subtechniques are present', () => { const payload = getMockCoverageOverviewMitreTechnique(); expect(getNumOfCoveredSubtechniques(payload)).toEqual(0); @@ -48,4 +56,36 @@ describe('helpers', () => { expect(getNumOfCoveredSubtechniques(payload)).toEqual(2); }); }); + + describe('extractSelected', () => { + it('returns empty array when no options are checked', () => { + const payload = ruleActivityFilterDefaultOptions; + expect(extractSelected(payload)).toEqual([]); + }); + + it('returns checked options when present', () => { + const payload = [ + ...ruleActivityFilterDefaultOptions, + { ...ruleActivityFilterDefaultOptions[0], checked: 'on' }, + ]; + expect(extractSelected(payload)).toEqual([ruleActivityFilterDefaultOptions[0].label]); + }); + }); + + describe('populateSelected', () => { + it('returns default status options when no filter is present', () => { + const payload: CoverageOverviewRuleActivity[] = []; + expect(populateSelected(ruleActivityFilterDefaultOptions, payload)).toEqual( + ruleActivityFilterDefaultOptions + ); + }); + + it('returns correct options checked when present in filter', () => { + const payload = getCoverageOverviewFilterMock().activity; + expect(populateSelected(ruleActivityFilterDefaultOptions, payload)).toEqual([ + { label: 'enabled', checked: 'on' }, + { label: 'disabled' }, + ]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts index 9611759fad271..82d50e7b9721b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts @@ -5,6 +5,11 @@ * 2.0. */ +import type { EuiSelectableOption } from '@elastic/eui'; +import type { + CoverageOverviewRuleActivity, + CoverageOverviewRuleSource, +} from '../../../../../common/api/detection_engine'; import type { CoverageOverviewMitreTactic } from '../../../rule_management/model/coverage_overview/mitre_tactic'; import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; import { coverageOverviewCardColorThresholds } from './constants'; @@ -22,3 +27,19 @@ export const getCardBackgroundColor = (value: number) => { } } }; + +export const extractSelected = < + T extends CoverageOverviewRuleSource | CoverageOverviewRuleActivity +>( + options: Array<{ checked?: string; label: T }> +): T[] => { + return options.filter((option) => option.checked === 'on').map((option) => option.label); +}; + +export const populateSelected = ( + allOptions: EuiSelectableOption[], + selected: string[] +): EuiSelectableOption[] => + allOptions.map((option) => + selected.includes(option.label) ? { ...option, checked: 'on' } : option + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.ts deleted file mode 100644 index 324ce06e2d418..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { CoverageOverviewPage } from './coverage_overview_page'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.tsx new file mode 100644 index 0000000000000..c8b264435111a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { SecurityPageName } from '../../../../app/types'; +import { CoverageOverviewDashboardContextProvider } from './coverage_overview_dashboard_context'; +import { CoverageOverviewDashboard } from './coverage_overview_dashboard'; + +export const CoverageOverviewPage = () => ( + <> + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/reducer.ts deleted file mode 100644 index cdafe0aa6b756..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/reducer.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface State { - showExpandedCells: boolean; -} - -export const initialState: State = { - showExpandedCells: false, -}; - -export interface Action { - type: 'setShowExpandedCells'; - value: boolean; -} - -export const createCoverageOverviewDashboardReducer = - () => - (state: State, action: Action): State => { - switch (action.type) { - case 'setShowExpandedCells': { - const { value } = action; - return { ...state, showExpandedCells: value }; - } - default: - return state; - } - }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_activity_filter.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_activity_filter.tsx new file mode 100644 index 0000000000000..0bb7e082e861e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_activity_filter.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { + EuiPopover, + EuiFilterButton, + EuiSelectable, + EuiFilterGroup, + EuiPopoverTitle, + EuiButtonEmpty, + EuiPopoverFooter, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; +import { + coverageOverviewFilterWidth, + ruleActivityFilterDefaultOptions, + ruleActivityFilterLabelMap, +} from './constants'; +import * as i18n from './translations'; +import { populateSelected, extractSelected } from './helpers'; + +export interface RuleActivityFilterComponentProps { + selected: CoverageOverviewRuleActivity[]; + onChange: (options: CoverageOverviewRuleActivity[]) => void; + isLoading: boolean; +} + +const RuleActivityFilterComponent = ({ + selected, + onChange, + isLoading, +}: RuleActivityFilterComponentProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [isPopoverOpen]); + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const numActiveFilters = useMemo(() => selected.length, [selected]); + + const options = populateSelected(ruleActivityFilterDefaultOptions, selected); + + const handleSelectableOnChange = useCallback( + (newOptions) => { + const formattedOptions = extractSelected(newOptions); + onChange(formattedOptions); + }, + [onChange] + ); + + const handleOnClear = useCallback(() => { + onChange([]); + }, [onChange]); + + const renderOptionLabel = (option: EuiSelectableOption) => + ruleActivityFilterLabelMap[option.label]; + + const button = useMemo( + () => ( + 0} + numActiveFilters={numActiveFilters} + > + {i18n.CoverageOverviewRuleActivityFilterLabel} + + ), + [isPopoverOpen, numActiveFilters, onButtonClick, isLoading] + ); + return ( + + + {i18n.CoverageOverviewFilterPopoverTitle} + + {(list) => ( +
+ {list} +
+ )} +
+ + + {i18n.CoverageOverviewFilterPopoverClearAll} + + +
+
+ ); +}; + +export const RuleActivityFilter = React.memo(RuleActivityFilterComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_source_filter.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_source_filter.tsx new file mode 100644 index 0000000000000..c17af658672da --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_source_filter.tsx @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { + EuiPopover, + EuiFilterButton, + EuiSelectable, + EuiFilterGroup, + EuiPopoverTitle, + EuiButtonEmpty, + EuiPopoverFooter, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { CoverageOverviewRuleSource } from '../../../../../common/api/detection_engine'; +import { + coverageOverviewFilterWidth, + ruleSourceFilterDefaultOptions, + ruleSourceFilterLabelMap, +} from './constants'; +import * as i18n from './translations'; +import { populateSelected, extractSelected } from './helpers'; + +export interface RuleSourceFilterComponentProps { + selected: CoverageOverviewRuleSource[]; + onChange: (options: CoverageOverviewRuleSource[]) => void; + isLoading: boolean; +} + +const RuleSourceFilterComponent = ({ + selected, + onChange, + isLoading, +}: RuleSourceFilterComponentProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [isPopoverOpen]); + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const numActiveFilters = useMemo(() => selected.length, [selected]); + + const options = populateSelected(ruleSourceFilterDefaultOptions, selected); + + const handleSelectableOnChange = useCallback( + (newOptions) => { + const formattedOptions = extractSelected(newOptions); + onChange(formattedOptions); + }, + [onChange] + ); + + const handleOnClear = useCallback(() => { + onChange([]); + }, [onChange]); + + const renderOptionLabel = (option: EuiSelectableOption) => ruleSourceFilterLabelMap[option.label]; + + const button = useMemo( + () => ( + 0} + numActiveFilters={numActiveFilters} + > + {i18n.CoverageOverviewRuleSourceFilterLabel} + + ), + [isPopoverOpen, numActiveFilters, onButtonClick, isLoading] + ); + return ( + + + {i18n.CoverageOverviewFilterPopoverTitle} + + {(list) => ( +
+ {list} +
+ )} +
+ + + {i18n.CoverageOverviewFilterPopoverClearAll} + + +
+
+ ); +}; + +export const RuleSourceFilter = React.memo(RuleSourceFilterComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/dashboard_legend.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/dashboard_legend.tsx index 5a72efc13f1f3..7a68d68c7aebe 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/dashboard_legend.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/dashboard_legend.tsx @@ -45,6 +45,7 @@ export const CoverageOverviewLegend = () => { ? `\u003E${threshold}` : `${threshold}-${thresholdsMap[index - 1].threshold}` } ${i18n.CoverageOverviewLegendRulesLabel}`} + key={index} color={color} /> )), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_metadata.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_rule_stats.tsx similarity index 77% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_metadata.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_rule_stats.tsx index 06b995c34ba83..c07dd6f10f0af 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_metadata.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_rule_stats.tsx @@ -10,7 +10,7 @@ import { css, cx } from '@emotion/css'; import React from 'react'; import * as i18n from '../translations'; -export interface CoverageOverviewPanelMetadataProps { +export interface CoverageOverviewPanelRuleStatsProps { disabledRules: number; enabledRules: number; } @@ -21,12 +21,16 @@ const metadataLabelClass = css` text-overflow: ellipsis; `; -export const CoverageOverviewPanelMetadata = ({ +export const CoverageOverviewPanelRuleStats = ({ disabledRules, enabledRules, -}: CoverageOverviewPanelMetadataProps) => { +}: CoverageOverviewPanelRuleStatsProps) => { return ( - + @@ -34,7 +38,7 @@ export const CoverageOverviewPanelMetadata = ({ - + {disabledRules} @@ -47,7 +51,7 @@ export const CoverageOverviewPanelMetadata = ({
- + {enabledRules} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.test.tsx deleted file mode 100644 index cddd257c130fa..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; - -import { getMockCoverageOverviewMitreTactic } from '../../../rule_management/model/coverage_overview/__mocks__'; -import { TestProviders } from '../../../../common/mock'; -import { CoverageOverviewTacticPanel } from './tactic_panel'; -import type { CoverageOverviewMitreTactic } from '../../../rule_management/model/coverage_overview/mitre_tactic'; - -const renderTacticPanel = ( - tactic: CoverageOverviewMitreTactic = getMockCoverageOverviewMitreTactic() -) => { - return render( - - - - ); -}; - -describe('CoverageOverviewTacticPanel', () => { - test('it renders information correctly', () => { - const wrapper = renderTacticPanel(); - - expect(wrapper.getByTestId('coverageOverviewTacticPanel')).toBeInTheDocument(); - expect(wrapper.getByTestId('metadataDisabledRulesCount')).toHaveTextContent('1'); - expect(wrapper.getByTestId('metadataEnabledRulesCount')).toHaveTextContent('1'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx index 12431fe237617..e1d1749ca264f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx @@ -13,7 +13,7 @@ import type { CoverageOverviewMitreTactic } from '../../../rule_management/model import { coverageOverviewPanelWidth } from './constants'; import { getNumOfCoveredTechniques } from './helpers'; import * as i18n from './translations'; -import { CoverageOverviewPanelMetadata } from './shared_components/panel_metadata'; +import { CoverageOverviewPanelRuleStats } from './shared_components/panel_rule_stats'; export interface CoverageOverviewTacticPanelProps { tactic: CoverageOverviewMitreTactic; @@ -68,7 +68,7 @@ const CoverageOverviewTacticPanelComponent = ({ tactic }: CoverageOverviewTactic max={tactic.techniques.length} /> - diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.test.tsx deleted file mode 100644 index 38e10e6299b8e..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; - -import { getMockCoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/__mocks__'; -import { TestProviders } from '../../../../common/mock'; -import { CoverageOverviewMitreTechniquePanel } from './technique_panel'; -import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; - -const renderTechniquePanel = ( - technique: CoverageOverviewMitreTechnique = getMockCoverageOverviewMitreTechnique(), - isExpanded: boolean = false -) => { - return render( - - {}} - isPopoverOpen={false} - isExpanded={isExpanded} - /> - - ); -}; - -describe('CoverageOverviewMitreTechniquePanel', () => { - test('it renders collapsed view', () => { - const wrapper = renderTechniquePanel(); - - expect(wrapper.getByTestId('coverageOverviewTechniquePanel')).toBeInTheDocument(); - expect(wrapper.queryByTestId('coverageOverviewPanelMetadata')).not.toBeInTheDocument(); - }); - - test('it renders expanded view', () => { - const wrapper = renderTechniquePanel(getMockCoverageOverviewMitreTechnique(), true); - - expect(wrapper.getByTestId('coverageOverviewTechniquePanel')).toBeInTheDocument(); - expect(wrapper.getByTestId('coverageOverviewPanelMetadata')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.tsx index d8af376d32bab..8de089d62e298 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.tsx @@ -11,7 +11,7 @@ import React, { memo, useCallback, useMemo } from 'react'; import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; import { coverageOverviewPanelWidth } from './constants'; import { getCardBackgroundColor } from './helpers'; -import { CoverageOverviewPanelMetadata } from './shared_components/panel_metadata'; +import { CoverageOverviewPanelRuleStats } from './shared_components/panel_rule_stats'; import * as i18n from './translations'; export interface CoverageOverviewMitreTechniquePanelProps { @@ -80,7 +80,7 @@ const CoverageOverviewMitreTechniquePanelComponent = ({
{isExpanded && ( - diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.test.tsx index dba2b381deb88..a41cdad7abb58 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.test.tsx @@ -12,28 +12,52 @@ import { getMockCoverageOverviewMitreTechnique } from '../../../rule_management/ import { TestProviders } from '../../../../common/mock'; import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; import { CoverageOverviewMitreTechniquePanelPopover } from './technique_panel_popover'; -import { useExecuteBulkAction } from '../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; +import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context'; -jest.mock('../../../rule_management/logic/bulk_actions/use_execute_bulk_action'); +jest.mock('./coverage_overview_dashboard_context'); -const mockExecuteBulkAction = jest.fn(); - -(useExecuteBulkAction as jest.Mock).mockReturnValue({ - executeBulkAction: mockExecuteBulkAction, -}); +const mockEnableAllDisabled = jest.fn(); const renderTechniquePanelPopover = ( - technique: CoverageOverviewMitreTechnique = getMockCoverageOverviewMitreTechnique(), - isExpanded: boolean = false + technique: CoverageOverviewMitreTechnique = getMockCoverageOverviewMitreTechnique() ) => { return render( - + ); }; describe('CoverageOverviewMitreTechniquePanelPopover', () => { + beforeEach(() => { + (useCoverageOverviewDashboardContext as jest.Mock).mockReturnValue({ + state: { showExpandedCells: false }, + actions: { enableAllDisabled: mockEnableAllDisabled }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders panel with collapsed view', () => { + const wrapper = renderTechniquePanelPopover(); + + expect(wrapper.getByTestId('coverageOverviewTechniquePanel')).toBeInTheDocument(); + expect(wrapper.queryByTestId('coverageOverviewPanelRuleStats')).not.toBeInTheDocument(); + }); + + test('it renders panel with expanded view', () => { + (useCoverageOverviewDashboardContext as jest.Mock).mockReturnValue({ + state: { showExpandedCells: true }, + actions: { enableAllDisabled: mockEnableAllDisabled }, + }); + const wrapper = renderTechniquePanelPopover(); + + expect(wrapper.getByTestId('coverageOverviewTechniquePanel')).toBeInTheDocument(); + expect(wrapper.getByTestId('coverageOverviewPanelRuleStats')).toBeInTheDocument(); + }); + test('it renders all rules in correct areas', () => { const wrapper = renderTechniquePanelPopover(); @@ -64,7 +88,7 @@ describe('CoverageOverviewMitreTechniquePanelPopover', () => { fireEvent.click(wrapper.getByTestId('enableAllDisabledButton')); }); - expect(mockExecuteBulkAction).toHaveBeenCalledWith({ ids: ['rule-id'], type: 'enable' }); + expect(mockEnableAllDisabled).toHaveBeenCalledWith(['rule-id']); }); test('"Enable all disabled" button is disabled when there are no disabled rules', async () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx index 2a7ca6f6a22f3..9beae73a21c4c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx @@ -21,48 +21,49 @@ import { } from '@elastic/eui'; import { css, cx } from '@emotion/css'; import React, { memo, useCallback, useMemo, useState } from 'react'; -import { BulkActionType } from '../../../../../common/api/detection_engine'; -import { useExecuteBulkAction } from '../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; import { getNumOfCoveredSubtechniques } from './helpers'; import { CoverageOverviewRuleListHeader } from './shared_components/popover_list_header'; import { CoverageOverviewMitreTechniquePanel } from './technique_panel'; import * as i18n from './translations'; import { RuleLink } from '../../components/rules_table/use_columns'; +import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context'; export interface CoverageOverviewMitreTechniquePanelPopoverProps { technique: CoverageOverviewMitreTechnique; - isExpanded: boolean; } const CoverageOverviewMitreTechniquePanelPopoverComponent = ({ technique, - isExpanded, }: CoverageOverviewMitreTechniquePanelPopoverProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isEnableButtonLoading, setIsDisableButtonLoading] = useState(false); const closePopover = useCallback(() => setIsPopoverOpen(false), []); const coveredSubtechniques = useMemo(() => getNumOfCoveredSubtechniques(technique), [technique]); - const { executeBulkAction } = useExecuteBulkAction(); const isEnableButtonDisabled = useMemo( () => technique.disabledRules.length === 0, [technique.disabledRules.length] ); + const { + state: { showExpandedCells }, + actions: { enableAllDisabled }, + } = useCoverageOverviewDashboardContext(); + const handleEnableAllDisabled = useCallback(async () => { setIsDisableButtonLoading(true); const ruleIds = technique.disabledRules.map((rule) => rule.id); - await executeBulkAction({ type: BulkActionType.enable, ids: ruleIds }); + await enableAllDisabled(ruleIds); setIsDisableButtonLoading(false); closePopover(); - }, [closePopover, executeBulkAction, technique.disabledRules]); + }, [closePopover, enableAllDisabled, technique.disabledRules]); const TechniquePanel = ( ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/translations.ts index ce4587fb01aea..b4aa93f2bcc02 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/translations.ts @@ -105,3 +105,67 @@ export const CoverageOverviewLegendRulesLabel = i18n.translate( defaultMessage: 'rules', } ); + +export const CoverageOverviewEnabledRuleActivity = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.enabledRuleActivity', + { + defaultMessage: 'Enabled rules', + } +); + +export const CoverageOverviewDisabledRuleActivity = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.disabledRuleActivity', + { + defaultMessage: 'Disabled rules', + } +); + +export const CoverageOverviewElasticRuleSource = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.elasticRuleSource', + { + defaultMessage: 'Elastic rules', + } +); + +export const CoverageOverviewCustomRuleSource = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.customRuleSource', + { + defaultMessage: 'Custom rules', + } +); + +export const CoverageOverviewRuleActivityFilterLabel = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.ruleActivityFilterLabel', + { + defaultMessage: 'Installed rule status', + } +); + +export const CoverageOverviewRuleSourceFilterLabel = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.ruleSourceFilterLabel', + { + defaultMessage: 'Installed rule type', + } +); + +export const CoverageOverviewSearchBarPlaceholder = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.searchBarPlaceholder', + { + defaultMessage: + 'Search for the tactic, technique (e.g.,"defence evasion" or "TA0005") or rule name, index pattern (e.g.,"filebeat-*")', + } +); + +export const CoverageOverviewFilterPopoverTitle = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.filterPopoverTitle', + { + defaultMessage: 'Select to view on framework', + } +); + +export const CoverageOverviewFilterPopoverClearAll = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.filterPopoverClearAll', + { + defaultMessage: 'Clear all', + } +);