From 9105f66c6cb9808e246b11ca9aea5779adc65221 Mon Sep 17 00:00:00 2001 From: Sean Rathier Date: Mon, 28 Oct 2024 21:51:40 -0400 Subject: [PATCH 1/9] using reducer broken --- .../public/pages/rules/index.tsx | 5 +- .../public/pages/rules/rules_container.tsx | 78 +-------- .../public/pages/rules/rules_context.tsx | 163 ++++++++++++++++++ .../public/pages/rules/rules_table.tsx | 16 +- .../public/pages/rules/rules_table_header.tsx | 37 ++-- 5 files changed, 204 insertions(+), 95 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx index f404b6a82d215..c1377b6193a34 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx @@ -19,6 +19,7 @@ import { useSecuritySolutionContext } from '../../application/security_solution_ import { useCspBenchmarkIntegrationsV2 } from '../benchmarks/use_csp_benchmark_integrations'; import { CISBenchmarkIcon } from '../../components/cis_benchmark_icon'; import { getBenchmarkCisName } from '../../../common/utils/helpers'; +import { RulesProvider } from './rules_context'; export const Rules = ({ match: { params } }: RouteComponentProps) => { const benchmarksInfo = useCspBenchmarkIntegrationsV2(); @@ -63,7 +64,9 @@ export const Rules = ({ match: { params } }: RouteComponentProps) } /> - + + + {SpyRoute && ( { const params = useParams(); const history = useHistory(); const [enabledDisabledItemsFilter, setEnabledDisabledItemsFilter] = useState('no-filter'); - const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); + const { allRules, rulesQuery, pageSize } = useRules(); const navToRuleFlyout = (ruleId: string) => { history.push( @@ -91,52 +90,6 @@ export const RulesContainer = () => { ); }; - // We need to make this call without filters. this way the section list is always full - const allRules = useFindCspBenchmarkRule( - { - page: 1, - perPage: MAX_ITEMS_PER_PAGE, - sortField: 'metadata.benchmark.rule_number', - sortOrder: 'asc', - }, - params.benchmarkId, - params.benchmarkVersion - ); - - const [rulesQuery, setRulesQuery] = useState({ - section: undefined, - ruleNumber: undefined, - search: '', - page: 0, - perPage: pageSize || 10, - sortField: 'metadata.benchmark.rule_number', - sortOrder: 'asc', - }); - - // This useEffect is in charge of auto paginating to the correct page of a rule from the url params - useEffect(() => { - const getPageByRuleId = () => { - if (params.ruleId && allRules.data?.items) { - const ruleIndex = allRules.data.items.findIndex( - (rule) => rule.metadata.id === params.ruleId - ); - - if (ruleIndex !== -1) { - // Calculate the page based on the rule index and page size - const rulePage = Math.floor(ruleIndex / pageSize); - return rulePage; - } - } - return 0; - }; - - setRulesQuery({ - ...rulesQuery, - page: getPageByRuleId(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allRules.data?.items]); - const { data, status, error } = useFindCspBenchmarkRule( { section: rulesQuery.section, @@ -185,13 +138,13 @@ export const RulesContainer = () => { }, [rulesWithStates, enabledDisabledItemsFilter]); const sectionList = useMemo( - () => allRules.data?.items.map((rule) => rule.metadata.section), - [allRules.data] + () => allRules?.items.map((rule) => rule.metadata.section), + [allRules] ); const ruleNumberList = useMemo( - () => allRules.data?.items.map((rule) => rule.metadata.benchmark.rule_number || ''), - [allRules.data] + () => allRules?.items.map((rule) => rule.metadata.benchmark.rule_number || ''), + [allRules] ); const cleanedSectionList = [...new Set(sectionList)].sort((a, b) => { @@ -222,7 +175,7 @@ export const RulesContainer = () => { : 'unmuted', }, ...{ - metadata: allRules.data?.items.find((rule) => rule.metadata.id === params.ruleId)?.metadata!, + metadata: allRules?.items.find((rule) => rule.metadata.id === params.ruleId)?.metadata!, }, }; @@ -234,15 +187,8 @@ export const RulesContainer = () => { /> - setRulesQuery((currentQuery) => ({ ...currentQuery, section: value })) - } - onRuleNumberChange={(value) => - setRulesQuery((currentQuery) => ({ ...currentQuery, ruleNumber: value })) - } sectionSelectOptions={cleanedSectionList} ruleNumberSelectOptions={cleanedRuleNumberList} - search={(value) => setRulesQuery((currentQuery) => ({ ...currentQuery, search: value }))} searchValue={rulesQuery.search || ''} totalRulesCount={rulesPageData.all_rules.length} pageSize={rulesPageData.rules_page.length} @@ -255,19 +201,11 @@ export const RulesContainer = () => { /> - setRulesQuery((currentQuery) => ({ ...currentQuery, sortOrder: value })) - } rules_page={rulesPageData.rules_page} total={rulesPageData.total} error={rulesPageData.error} loading={rulesPageData.loading} perPage={pageSize || rulesQuery.perPage} - page={rulesQuery.page} - setPagination={(paginationQuery) => { - setPageSize(paginationQuery.perPage); - setRulesQuery((currentQuery) => ({ ...currentQuery, ...paginationQuery })); - }} selectedRuleId={params.ruleId} onRuleClick={navToRuleFlyout} selectedRules={selectedRules} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx new file mode 100644 index 0000000000000..382e3ad04754a --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -0,0 +1,163 @@ +/* + * Copyright 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, useContext, useMemo, useEffect, useCallback } from 'react'; +import { useParams } from 'react-router-dom'; + +import { + FindCspBenchmarkRuleResponse, + PageUrlParams, +} from '@kbn/cloud-security-posture-common/schema/rules/latest'; +import { RulesQuery, useFindCspBenchmarkRule } from './use_csp_benchmark_rules'; +import { usePageSize } from '../../common/hooks/use_page_size'; +import { LOCAL_STORAGE_PAGE_SIZE_RULES_KEY } from '../../common/constants'; + +// type Dispatch = (action: Action) => void; +interface RulesProviderProps { + children: React.ReactNode; +} +interface RulesContextValue { + allRules: FindCspBenchmarkRuleResponse | undefined; + rulesQuery: RulesQuery; + setRulesQuery: (query: Partial) => void; + pageSize: number; + setPageSize: (pageSize: number) => void; + page: number; + setPage: (page: number) => void; +} + +const RulesContext = createContext(undefined); +const MAX_ITEMS_PER_PAGE = 10000; + +interface State { + rulesQuery: RulesQuery; + pageSize?: number; + page: number; +} + +type Action = + | { + type: 'setRulesQuery'; + value: Partial; + } + | { type: 'setPageSize'; value: number } + | { type: 'setPage'; value: number }; + +function rulesReducer(state: State, action: Action) { + switch (action.type) { + case 'setRulesQuery': { + return { ...state, rulesQuery: { ...state.rulesQuery, ...action.value } }; + } + case 'setPage': { + return { ...state, page: action.value }; + } + case 'setPageSize': { + return { ...state, pageSize: action.value }; + } + default: { + throw new Error(`Unhandled action type: ${action}`); + } + } +} + +const initialState: State = { + page: 1, + pageSize: 10, + rulesQuery: { + section: undefined, + ruleNumber: undefined, + search: '', + page: 0, + perPage: 10, + sortField: 'metadata.benchmark.rule_number', + sortOrder: 'asc', + }, +}; + +export function RulesProvider({ children }: RulesProviderProps) { + const params = useParams(); + const { pageSize: localStoragePageSize, setPageSize: setLocalStoragePageSize } = usePageSize( + LOCAL_STORAGE_PAGE_SIZE_RULES_KEY + ); + const allRules = useFindCspBenchmarkRule( + { + page: 1, + perPage: MAX_ITEMS_PER_PAGE, + sortField: 'metadata.benchmark.rule_number', + sortOrder: 'asc', + }, + params.benchmarkId, + params.benchmarkVersion + ); + + const [state, dispatch] = React.useReducer(rulesReducer, initialState); + + // This useEffect is in charge of auto paginating to the correct page of a rule from the url params + useEffect(() => { + const getPageByRuleId = () => { + if (params.ruleId && allRules?.data?.items) { + const ruleIndex = allRules?.data?.items.findIndex( + (rule) => rule.metadata.id === params.ruleId + ); + + if (ruleIndex !== -1) { + // Calculate the page based on the rule index and page size + const rulePage = Math.floor(ruleIndex / localStoragePageSize); + return rulePage; + } + } + return 0; + }; + + dispatch({ type: 'setPageSize', value: getPageByRuleId() }); + }, [allRules?.data?.items, params.ruleId, localStoragePageSize]); + + const setRulesQuery = useCallback( + () => (query: Partial) => { + dispatch({ type: 'setRulesQuery', value: query }); + }, + [] + ); + + const setPageSize = useCallback( + () => (value: number) => { + setLocalStoragePageSize(value); + dispatch({ type: 'setPageSize', value }); + }, + [setLocalStoragePageSize] + ); + + const initialContextValue = useMemo( + () => ({ + allRules: allRules.data, + rulesQuery: state.rulesQuery, + setRulesQuery, + pageSize: state.pageSize || localStoragePageSize, + setPageSize, + page: state.page, + setPage: (value: number) => dispatch({ type: 'setPage', value }), + }), + [ + allRules, + state.rulesQuery, + state.pageSize, + state.page, + localStoragePageSize, + setRulesQuery, + setPageSize, + ] + ); + + return {children}; +} + +export function useRules() { + const context = useContext(RulesContext); + if (context === undefined) { + throw new Error('useRules must be used within a RulesProvider'); + } + return context; +} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index a30e009c85198..31c7ce45c9773 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -29,20 +29,19 @@ import { ColumnNameWithTooltip } from '../../components/column_name_with_tooltip import type { CspBenchmarkRulesWithStates, RulesState } from './rules_container'; import * as TEST_SUBJECTS from './test_subjects'; import { useChangeCspRuleState } from './use_change_csp_rule_state'; +import { useRules } from './rules_context'; export const RULES_ROWS_ENABLE_SWITCH_BUTTON = 'rules-row-enable-switch-button'; export const RULES_ROW_SELECT_ALL_CURRENT_PAGE = 'cloud-security-fields-selector-item-all'; type RulesTableProps = Pick< RulesState, - 'loading' | 'error' | 'rules_page' | 'total' | 'perPage' | 'page' + 'loading' | 'error' | 'rules_page' | 'total' | 'perPage' > & { - setPagination(pagination: Pick): void; onRuleClick: (ruleID: string) => void; selectedRuleId?: string; selectedRules: CspBenchmarkRulesWithStates[]; setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; - onSortChange: (value: 'asc' | 'desc') => void; }; type GetColumnProps = Pick< @@ -59,10 +58,8 @@ type GetColumnProps = Pick< }; export const RulesTable = ({ - setPagination, perPage: pageSize, rules_page: items, - page, total, loading, error, @@ -70,9 +67,9 @@ export const RulesTable = ({ selectedRules, setSelectedRules, onRuleClick, - onSortChange, }: RulesTableProps) => { const { euiTheme } = useEuiTheme(); + const { setRulesQuery, setPageSize, page } = useRules(); const euiPagination: EuiBasicTableProps['pagination'] = { pageIndex: page, pageSize, @@ -91,10 +88,13 @@ export const RulesTable = ({ sort: sortOrder, }: Criteria) => { if (!pagination) return; - if (pagination) setPagination({ page: pagination.index, perPage: pagination.size }); + if (pagination) { + setPageSize(pagination.size); + setRulesQuery({ page: pagination.index, perPage: pagination.size }); + } if (sortOrder) { setSortDirection(sortOrder.direction); - onSortChange(sortOrder.direction); + setRulesQuery({ sortOrder: sortOrder.direction }); } }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx index a7c69326d3964..939ad06c4f31b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx @@ -29,6 +29,7 @@ import { } from './use_change_csp_rule_state'; import { CspBenchmarkRulesWithStates } from './rules_container'; import { MultiSelectFilter } from '../../common/component/multi_select_filter'; +import { useRules } from './rules_context'; export const RULES_BULK_ACTION_BUTTON = 'bulk-action-button'; export const RULES_BULK_ACTION_OPTION_ENABLE = 'bulk-action-option-enable'; @@ -39,9 +40,6 @@ export const RULES_DISABLED_FILTER = 'rules-disabled-filter'; export const RULES_ENABLED_FILTER = 'rules-enabled-filter'; interface RulesTableToolbarProps { - search: (value: string) => void; - onSectionChange: (value: string[] | undefined) => void; - onRuleNumberChange: (value: string[] | undefined) => void; sectionSelectOptions: string[]; ruleNumberSelectOptions: string[]; totalRulesCount: number; @@ -64,13 +62,10 @@ interface RuleTableCount { } export const RulesTableHeader = ({ - search, searchValue, isSearching, totalRulesCount, pageSize, - onSectionChange, - onRuleNumberChange, sectionSelectOptions, ruleNumberSelectOptions, selectedRules, @@ -79,6 +74,7 @@ export const RulesTableHeader = ({ setSelectAllRules, setSelectedRules, }: RulesTableToolbarProps) => { + const { setRulesQuery } = useRules(); const [selectedSection, setSelectedSection] = useState([]); const [selectedRuleNumber, setSelectedRuleNumber] = useState([]); const sectionOptions = sectionSelectOptions.map((option) => ({ @@ -104,7 +100,7 @@ export const RulesTableHeader = ({ - + @@ -123,9 +119,9 @@ export const RulesTableHeader = ({ id={'cis-section-multi-select-filter'} onChange={(section) => { setSelectedSection([...section?.selectedOptionKeys]); - onSectionChange( - section?.selectedOptionKeys ? section?.selectedOptionKeys : undefined - ); + setRulesQuery({ + section: section?.selectedOptionKeys ? section?.selectedOptionKeys : undefined, + }); }} options={sectionOptions} selectedOptionKeys={selectedSection} @@ -146,9 +142,11 @@ export const RulesTableHeader = ({ id={'rule-number-multi-select-filter'} onChange={(ruleNumber) => { setSelectedRuleNumber([...ruleNumber?.selectedOptionKeys]); - onRuleNumberChange( - ruleNumber?.selectedOptionKeys ? ruleNumber?.selectedOptionKeys : undefined - ); + setRulesQuery({ + ruleNumber: ruleNumber?.selectedOptionKeys + ? ruleNumber?.selectedOptionKeys + : undefined, + }); }} options={ruleNumberOptions} selectedOptionKeys={selectedRuleNumber} @@ -202,13 +200,20 @@ export const RulesTableHeader = ({ const SEARCH_DEBOUNCE_MS = 300; const SearchField = ({ - search, isSearching, searchValue, -}: Pick) => { +}: Pick) => { const [localValue, setLocalValue] = useState(searchValue); + const { setRulesQuery } = useRules(); - useDebounce(() => search(localValue), SEARCH_DEBOUNCE_MS, [localValue]); + useDebounce( + () => + setRulesQuery({ + search: localValue, + }), + SEARCH_DEBOUNCE_MS, + [localValue] + ); return (
From 936d654375f1c694774da45002ab50e158de41b1 Mon Sep 17 00:00:00 2001 From: Sean Rathier Date: Mon, 28 Oct 2024 22:41:07 -0400 Subject: [PATCH 2/9] working paging --- .../public/pages/rules/rules_context.tsx | 156 ++++++++++-------- 1 file changed, 88 insertions(+), 68 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx index 382e3ad04754a..6e58b82ebcd81 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { createContext, useContext, useMemo, useEffect, useCallback } from 'react'; +import React, { createContext, useContext, useMemo, useEffect, useCallback, useState } from 'react'; import { useParams } from 'react-router-dom'; import { @@ -32,56 +32,65 @@ interface RulesContextValue { const RulesContext = createContext(undefined); const MAX_ITEMS_PER_PAGE = 10000; -interface State { - rulesQuery: RulesQuery; - pageSize?: number; - page: number; -} - -type Action = - | { - type: 'setRulesQuery'; - value: Partial; - } - | { type: 'setPageSize'; value: number } - | { type: 'setPage'; value: number }; - -function rulesReducer(state: State, action: Action) { - switch (action.type) { - case 'setRulesQuery': { - return { ...state, rulesQuery: { ...state.rulesQuery, ...action.value } }; - } - case 'setPage': { - return { ...state, page: action.value }; - } - case 'setPageSize': { - return { ...state, pageSize: action.value }; - } - default: { - throw new Error(`Unhandled action type: ${action}`); - } - } -} +// interface State { +// rulesQuery: RulesQuery; +// pageSize?: number; +// page: number; +// } + +// type Action = +// | { +// type: 'setRulesQuery'; +// value: Partial; +// } +// | { type: 'setPageSize'; value: number } +// | { type: 'setPage'; value: number }; + +// function rulesReducer(state: State, action: Action) { +// switch (action.type) { +// case 'setRulesQuery': { +// return { ...state, rulesQuery: { ...state.rulesQuery, ...action.value } }; +// } +// case 'setPage': { +// return { ...state, page: action.value }; +// } +// case 'setPageSize': { +// return { ...state, pageSize: action.value }; +// } +// default: { +// throw new Error(`Unhandled action type: ${action}`); +// } +// } +// } + +// const initialState: State = { +// page: 1, +// pageSize: 10, +// rulesQuery: { +// section: undefined, +// ruleNumber: undefined, +// search: '', +// page: 0, +// perPage: 10, +// sortField: 'metadata.benchmark.rule_number', +// sortOrder: 'asc', +// }, +// }; -const initialState: State = { - page: 1, - pageSize: 10, - rulesQuery: { +export function RulesProvider({ children }: RulesProviderProps) { + const params = useParams(); + const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); + const [page, setPage] = useState(1); + const [rulesQuery, setRulesQuery] = useState({ section: undefined, ruleNumber: undefined, search: '', page: 0, - perPage: 10, + perPage: pageSize || 10, sortField: 'metadata.benchmark.rule_number', sortOrder: 'asc', - }, -}; + }); -export function RulesProvider({ children }: RulesProviderProps) { - const params = useParams(); - const { pageSize: localStoragePageSize, setPageSize: setLocalStoragePageSize } = usePageSize( - LOCAL_STORAGE_PAGE_SIZE_RULES_KEY - ); const allRules = useFindCspBenchmarkRule( { page: 1, @@ -93,8 +102,6 @@ export function RulesProvider({ children }: RulesProviderProps) { params.benchmarkVersion ); - const [state, dispatch] = React.useReducer(rulesReducer, initialState); - // This useEffect is in charge of auto paginating to the correct page of a rule from the url params useEffect(() => { const getPageByRuleId = () => { @@ -105,49 +112,62 @@ export function RulesProvider({ children }: RulesProviderProps) { if (ruleIndex !== -1) { // Calculate the page based on the rule index and page size - const rulePage = Math.floor(ruleIndex / localStoragePageSize); + const rulePage = Math.floor(ruleIndex / pageSize); return rulePage; } } return 0; }; - dispatch({ type: 'setPageSize', value: getPageByRuleId() }); - }, [allRules?.data?.items, params.ruleId, localStoragePageSize]); + setRulesQuery({ + ...rulesQuery, + page: getPageByRuleId(), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allRules?.data?.items]); - const setRulesQuery = useCallback( + const setRulesQueryCallback = useCallback( () => (query: Partial) => { - dispatch({ type: 'setRulesQuery', value: query }); + setRulesQuery({ ...rulesQuery, ...query }); + }, + [rulesQuery, setRulesQuery] + ); + + const setPageSizeCallback = useCallback( + (value: number) => { + if (value === pageSize) return; + + setPageSize(value); + setRulesQuery({ ...rulesQuery, ...{ perPage: value } }); }, - [] + [rulesQuery, setPageSize, pageSize] ); - const setPageSize = useCallback( - () => (value: number) => { - setLocalStoragePageSize(value); - dispatch({ type: 'setPageSize', value }); + const setPageCallback = useCallback( + (value: number) => { + setPage(value); }, - [setLocalStoragePageSize] + [setPage] ); const initialContextValue = useMemo( () => ({ allRules: allRules.data, - rulesQuery: state.rulesQuery, - setRulesQuery, - pageSize: state.pageSize || localStoragePageSize, - setPageSize, - page: state.page, - setPage: (value: number) => dispatch({ type: 'setPage', value }), + rulesQuery, + setRulesQuery: setRulesQueryCallback, + pageSize, + setPageSize: setPageSizeCallback, + page, + setPage: setPageCallback, }), [ allRules, - state.rulesQuery, - state.pageSize, - state.page, - localStoragePageSize, - setRulesQuery, - setPageSize, + rulesQuery, + setRulesQueryCallback, + pageSize, + setPageSizeCallback, + page, + setPageCallback, ] ); From 113e993118061217f1a6452aaede396c9246d747 Mon Sep 17 00:00:00 2001 From: Sean Rathier Date: Tue, 29 Oct 2024 16:06:43 -0400 Subject: [PATCH 3/9] Added all state --- .../public/pages/rules/rules_container.tsx | 176 +------------- .../public/pages/rules/rules_context.tsx | 218 ++++++++++++++---- .../public/pages/rules/rules_counters.tsx | 10 +- .../public/pages/rules/rules_table.tsx | 39 ++-- .../public/pages/rules/rules_table_header.tsx | 82 ++----- 5 files changed, 213 insertions(+), 312 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx index 02e44d1ae0259..ef6835738ba56 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx @@ -4,72 +4,22 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useMemo } from 'react'; +import React from 'react'; import { EuiSpacer } from '@elastic/eui'; import { useParams, useHistory, generatePath } from 'react-router-dom'; -import type { - CspBenchmarkRule, - PageUrlParams, - RuleStateAttributes, -} from '@kbn/cloud-security-posture-common/schema/rules/latest'; -import { extractErrorMessage } from '@kbn/cloud-security-posture-common'; -import semVerCompare from 'semver/functions/compare'; -import semVerCoerce from 'semver/functions/coerce'; +import type { PageUrlParams } from '@kbn/cloud-security-posture-common/schema/rules/latest'; import { benchmarksNavigation } from '../../common/navigation/constants'; -import { buildRuleKey } from '../../../common/utils/rules_states'; import { RulesTable } from './rules_table'; import { RulesTableHeader } from './rules_table_header'; -import { useFindCspBenchmarkRule, type RulesQuery } from './use_csp_benchmark_rules'; import * as TEST_SUBJECTS from './test_subjects'; import { RuleFlyout } from './rules_flyout'; -import { useCspGetRulesStates } from './use_csp_rules_state'; import { RulesCounters } from './rules_counters'; import { useRules } from './rules_context'; -export interface CspBenchmarkRulesWithStates { - metadata: CspBenchmarkRule['metadata']; - state: 'muted' | 'unmuted'; -} - -interface RulesPageData { - rules_page: CspBenchmarkRulesWithStates[]; - all_rules: CspBenchmarkRulesWithStates[]; - rules_map: Map; - total: number; - error?: string; - loading: boolean; -} - -export type RulesState = RulesPageData & RulesQuery; - -const getPage = (data: CspBenchmarkRulesWithStates[], { page, perPage }: RulesQuery) => - data.slice(page * perPage, (page + 1) * perPage); - -const getRulesPageData = ( - data: CspBenchmarkRulesWithStates[], - status: string, - error: unknown, - query: RulesQuery -): RulesPageData => { - const page = getPage(data, query); - - return { - loading: status === 'loading', - error: error ? extractErrorMessage(error) : undefined, - all_rules: data, - rules_map: new Map(data.map((rule) => [rule.metadata.id, rule])), - rules_page: page, - total: data?.length || 0, - }; -}; - -const MAX_ITEMS_PER_PAGE = 10000; - export const RulesContainer = () => { const params = useParams(); const history = useHistory(); - const [enabledDisabledItemsFilter, setEnabledDisabledItemsFilter] = useState('no-filter'); - const { allRules, rulesQuery, pageSize } = useRules(); + const { rulesFlyoutData } = useRules(); const navToRuleFlyout = (ruleId: string) => { history.push( @@ -90,127 +40,13 @@ export const RulesContainer = () => { ); }; - const { data, status, error } = useFindCspBenchmarkRule( - { - section: rulesQuery.section, - ruleNumber: rulesQuery.ruleNumber, - search: rulesQuery.search, - page: 1, - perPage: MAX_ITEMS_PER_PAGE, - sortField: 'metadata.benchmark.rule_number', - sortOrder: rulesQuery.sortOrder, - }, - params.benchmarkId, - params.benchmarkVersion - ); - - const rulesStates = useCspGetRulesStates(); - const arrayRulesStates: RuleStateAttributes[] = Object.values(rulesStates.data || {}); - - const rulesWithStates: CspBenchmarkRulesWithStates[] = useMemo(() => { - if (!data) return []; - - return data.items - .filter((rule: CspBenchmarkRule) => rule.metadata.benchmark.rule_number !== undefined) - .map((rule: CspBenchmarkRule) => { - const rulesKey = buildRuleKey( - rule.metadata.benchmark.id, - rule.metadata.benchmark.version, - /* Rule number always exists* from 8.7 */ - rule.metadata.benchmark.rule_number! - ); - - const match = rulesStates?.data?.[rulesKey]; - const rulesState = match?.muted ? 'muted' : 'unmuted'; - - return { ...rule, state: rulesState || 'unmuted' }; - }); - }, [data, rulesStates?.data]); - - const mutedRulesCount = rulesWithStates.filter((rule) => rule.state === 'muted').length; - - const filteredRulesWithStates: CspBenchmarkRulesWithStates[] = useMemo(() => { - if (enabledDisabledItemsFilter === 'disabled') - return rulesWithStates?.filter((rule) => rule?.state === 'muted'); - else if (enabledDisabledItemsFilter === 'enabled') - return rulesWithStates?.filter((rule) => rule?.state === 'unmuted'); - else return rulesWithStates; - }, [rulesWithStates, enabledDisabledItemsFilter]); - - const sectionList = useMemo( - () => allRules?.items.map((rule) => rule.metadata.section), - [allRules] - ); - - const ruleNumberList = useMemo( - () => allRules?.items.map((rule) => rule.metadata.benchmark.rule_number || ''), - [allRules] - ); - - const cleanedSectionList = [...new Set(sectionList)].sort((a, b) => { - return a.localeCompare(b, 'en', { sensitivity: 'base' }); - }); - - const cleanedRuleNumberList = [...new Set(ruleNumberList)].sort((a, b) => - semVerCompare(semVerCoerce(a) ?? '', semVerCoerce(b) ?? '') - ); - - const rulesPageData = useMemo( - () => getRulesPageData(filteredRulesWithStates, status, error, rulesQuery), - [filteredRulesWithStates, status, error, rulesQuery] - ); - - const [selectedRules, setSelectedRules] = useState([]); - - const setSelectAllRules = () => { - setSelectedRules(rulesPageData.all_rules); - }; - - const rulesFlyoutData: CspBenchmarkRulesWithStates = { - ...{ - state: - arrayRulesStates.find((filteredRuleState) => filteredRuleState.rule_id === params.ruleId) - ?.muted === true - ? 'muted' - : 'unmuted', - }, - ...{ - metadata: allRules?.items.find((rule) => rule.metadata.id === params.ruleId)?.metadata!, - }, - }; - return (
- + - + - + {params.ruleId && rulesFlyoutData.metadata && ( )} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx index 6e58b82ebcd81..6e8db09a5865a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -8,74 +8,79 @@ import React, { createContext, useContext, useMemo, useEffect, useCallback, useS import { useParams } from 'react-router-dom'; import { - FindCspBenchmarkRuleResponse, + CspBenchmarkRule, PageUrlParams, + RuleStateAttributes, } from '@kbn/cloud-security-posture-common/schema/rules/latest'; +import { extractErrorMessage } from '@kbn/cloud-security-posture-common'; +import { buildRuleKey } from '../../../common/utils/rules_states'; import { RulesQuery, useFindCspBenchmarkRule } from './use_csp_benchmark_rules'; import { usePageSize } from '../../common/hooks/use_page_size'; import { LOCAL_STORAGE_PAGE_SIZE_RULES_KEY } from '../../common/constants'; +import { useCspGetRulesStates } from './use_csp_rules_state'; + +export interface CspBenchmarkRulesWithStates { + metadata: CspBenchmarkRule['metadata']; + state: 'muted' | 'unmuted'; +} + +interface RulesPageData { + rules_page: CspBenchmarkRulesWithStates[]; + all_rules: CspBenchmarkRulesWithStates[]; + rules_map: Map; + total: number; + error?: string; + loading: boolean; +} + +export type RulesState = RulesPageData & RulesQuery; -// type Dispatch = (action: Action) => void; interface RulesProviderProps { children: React.ReactNode; } interface RulesContextValue { - allRules: FindCspBenchmarkRuleResponse | undefined; rulesQuery: RulesQuery; + rulesPageData: RulesPageData; setRulesQuery: (query: Partial) => void; pageSize: number; setPageSize: (pageSize: number) => void; page: number; setPage: (page: number) => void; + sectionList: string[] | undefined; + ruleNumberSelectOptions: string[]; + sectionSelectOptions: string[]; + selectedRules: CspBenchmarkRulesWithStates[]; + setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; + setSelectAllRules: () => void; + setEnabledDisabledItemsFilter: (filter: string) => void; + enabledDisabledItemsFilter: string; + mutedRulesCount?: number; + rulesFlyoutData: CspBenchmarkRulesWithStates; } const RulesContext = createContext(undefined); const MAX_ITEMS_PER_PAGE = 10000; -// interface State { -// rulesQuery: RulesQuery; -// pageSize?: number; -// page: number; -// } - -// type Action = -// | { -// type: 'setRulesQuery'; -// value: Partial; -// } -// | { type: 'setPageSize'; value: number } -// | { type: 'setPage'; value: number }; - -// function rulesReducer(state: State, action: Action) { -// switch (action.type) { -// case 'setRulesQuery': { -// return { ...state, rulesQuery: { ...state.rulesQuery, ...action.value } }; -// } -// case 'setPage': { -// return { ...state, page: action.value }; -// } -// case 'setPageSize': { -// return { ...state, pageSize: action.value }; -// } -// default: { -// throw new Error(`Unhandled action type: ${action}`); -// } -// } -// } - -// const initialState: State = { -// page: 1, -// pageSize: 10, -// rulesQuery: { -// section: undefined, -// ruleNumber: undefined, -// search: '', -// page: 0, -// perPage: 10, -// sortField: 'metadata.benchmark.rule_number', -// sortOrder: 'asc', -// }, -// }; +const getPage = (data: CspBenchmarkRulesWithStates[], { page, perPage }: RulesQuery) => + data.slice(page * perPage, (page + 1) * perPage); + +const getRulesPageData = ( + data: CspBenchmarkRulesWithStates[], + status: string, + error: unknown, + query: RulesQuery +): RulesPageData => { + const page = getPage(data, query); + + return { + loading: status === 'loading', + error: error ? extractErrorMessage(error) : undefined, + all_rules: data, + rules_map: new Map(data.map((rule) => [rule.metadata.id, rule])), + rules_page: page, + total: data?.length || 0, + }; +}; export function RulesProvider({ children }: RulesProviderProps) { const params = useParams(); @@ -90,6 +95,8 @@ export function RulesProvider({ children }: RulesProviderProps) { sortField: 'metadata.benchmark.rule_number', sortOrder: 'asc', }); + const [selectedRules, setSelectedRules] = useState([]); + const [enabledDisabledItemsFilter, setEnabledDisabledItemsFilter] = useState('no-filter'); const allRules = useFindCspBenchmarkRule( { @@ -150,28 +157,137 @@ export function RulesProvider({ children }: RulesProviderProps) { [setPage] ); - const initialContextValue = useMemo( + const sectionList = useMemo( + () => allRules?.data?.items.map((rule) => rule.metadata.section), + [allRules] + ); + + const ruleNumberSelectOptions = useMemo( + () => + allRules.data + ? allRules.data.items.map((rule) => rule.metadata.benchmark.rule_number || '') + : [], + [allRules] + ); + + const sectionSelectOptions = [...new Set(sectionList)].sort((a, b) => { + return a.localeCompare(b, 'en', { sensitivity: 'base' }); + }); + + const { data, status, error } = useFindCspBenchmarkRule( + { + section: rulesQuery.section, + ruleNumber: rulesQuery.ruleNumber, + search: rulesQuery.search, + page: 1, + perPage: MAX_ITEMS_PER_PAGE, + sortField: 'metadata.benchmark.rule_number', + sortOrder: rulesQuery.sortOrder, + }, + params.benchmarkId, + params.benchmarkVersion + ); + + const rulesStates = useCspGetRulesStates(); + + const rulesWithStates: CspBenchmarkRulesWithStates[] = useMemo(() => { + if (!data) return []; + + return data.items + .filter((rule: CspBenchmarkRule) => rule.metadata.benchmark.rule_number !== undefined) + .map((rule: CspBenchmarkRule) => { + const rulesKey = buildRuleKey( + rule.metadata.benchmark.id, + rule.metadata.benchmark.version, + /* Rule number always exists* from 8.7 */ + rule.metadata.benchmark.rule_number! + ); + + const match = rulesStates?.data?.[rulesKey]; + const rulesState = match?.muted ? 'muted' : 'unmuted'; + + return { ...rule, state: rulesState || 'unmuted' }; + }); + }, [data, rulesStates?.data]); + + const mutedRulesCount = rulesWithStates.filter((rule) => rule.state === 'muted').length; + + const filteredRulesWithStates: CspBenchmarkRulesWithStates[] = useMemo(() => { + if (enabledDisabledItemsFilter === 'disabled') + return rulesWithStates?.filter((rule) => rule?.state === 'muted'); + else if (enabledDisabledItemsFilter === 'enabled') + return rulesWithStates?.filter((rule) => rule?.state === 'unmuted'); + else return rulesWithStates; + }, [rulesWithStates, enabledDisabledItemsFilter]); + + const rulesPageData = useMemo( + () => getRulesPageData(filteredRulesWithStates, status, error, rulesQuery), + [filteredRulesWithStates, status, error, rulesQuery] + ); + + const setSelectAllRules = useCallback(() => { + setSelectedRules(rulesPageData.all_rules); + }, [rulesPageData.all_rules]); + + const rulesFlyoutData: CspBenchmarkRulesWithStates = useMemo(() => { + const arrayRulesStates: RuleStateAttributes[] = Object.values(rulesStates.data || {}); + return { + ...{ + state: + arrayRulesStates.find((filteredRuleState) => filteredRuleState.rule_id === params.ruleId) + ?.muted === true + ? 'muted' + : 'unmuted', + }, + ...{ + metadata: allRules?.data?.items.find((rule) => rule.metadata.id === params.ruleId) + ?.metadata!, + }, + }; + }, [rulesStates, params.ruleId, allRules]); + + const contextValue: RulesContextValue = useMemo( () => ({ - allRules: allRules.data, rulesQuery, + rulesPageData, setRulesQuery: setRulesQueryCallback, pageSize, setPageSize: setPageSizeCallback, page, setPage: setPageCallback, + sectionList, + ruleNumberSelectOptions, + sectionSelectOptions, + selectedRules, + setSelectedRules, + setSelectAllRules, + setEnabledDisabledItemsFilter, + enabledDisabledItemsFilter, + mutedRulesCount, + rulesFlyoutData, }), [ - allRules, rulesQuery, + rulesPageData, setRulesQueryCallback, pageSize, setPageSizeCallback, page, setPageCallback, + sectionList, + ruleNumberSelectOptions, + sectionSelectOptions, + selectedRules, + setSelectedRules, + setSelectAllRules, + setEnabledDisabledItemsFilter, + enabledDisabledItemsFilter, + mutedRulesCount, + rulesFlyoutData, ] ); - return {children}; + return {children}; } export function useRules() { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_counters.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_counters.tsx index 15dca8e9b76ff..0509511162148 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_counters.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_counters.tsx @@ -29,6 +29,7 @@ import { RULE_FAILED, RULE_PASSED } from '../../../common/constants'; import { useCspBenchmarkIntegrationsV2 } from '../benchmarks/use_csp_benchmark_integrations'; import { CspCounterCard } from '../../components/csp_counter_card'; import { useKibana } from '../../common/hooks/use_kibana'; +import { useRules } from './rules_context'; const EvaluationPieChart = ({ failed, passed }: { failed: number; passed: number }) => { const { @@ -84,18 +85,13 @@ const EvaluationPieChart = ({ failed, passed }: { failed: number; passed: number ); }; -export const RulesCounters = ({ - mutedRulesCount, - setEnabledDisabledItemsFilter, -}: { - mutedRulesCount: number; - setEnabledDisabledItemsFilter: (filterState: string) => void; -}) => { +export const RulesCounters = () => { const { http } = useKibana().services; const { getBenchmarkDynamicValues } = useBenchmarkDynamicValues(); const rulesPageParams = useParams<{ benchmarkId: string; benchmarkVersion: string }>(); const getBenchmarks = useCspBenchmarkIntegrationsV2(); const navToFindings = useNavigateFindings(); + const { setEnabledDisabledItemsFilter, mutedRulesCount } = useRules(); const benchmarkRulesStats = getBenchmarks.data?.items.find( (benchmark) => diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index 31c7ce45c9773..4e3f021830608 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -26,28 +26,21 @@ import { } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { uniqBy } from 'lodash'; import { ColumnNameWithTooltip } from '../../components/column_name_with_tooltip'; -import type { CspBenchmarkRulesWithStates, RulesState } from './rules_container'; import * as TEST_SUBJECTS from './test_subjects'; import { useChangeCspRuleState } from './use_change_csp_rule_state'; -import { useRules } from './rules_context'; +import { CspBenchmarkRulesWithStates, useRules } from './rules_context'; export const RULES_ROWS_ENABLE_SWITCH_BUTTON = 'rules-row-enable-switch-button'; export const RULES_ROW_SELECT_ALL_CURRENT_PAGE = 'cloud-security-fields-selector-item-all'; -type RulesTableProps = Pick< - RulesState, - 'loading' | 'error' | 'rules_page' | 'total' | 'perPage' -> & { +interface RulesTableProps { onRuleClick: (ruleID: string) => void; selectedRuleId?: string; +} + +type GetColumnProps = Pick & { selectedRules: CspBenchmarkRulesWithStates[]; setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; -}; - -type GetColumnProps = Pick< - RulesTableProps, - 'onRuleClick' | 'selectedRules' | 'setSelectedRules' -> & { items: CspBenchmarkRulesWithStates[]; setIsAllRulesSelectedThisPage: (isAllRulesSelected: boolean) => void; isAllRulesSelectedThisPage: boolean; @@ -57,22 +50,18 @@ type GetColumnProps = Pick< ) => boolean; }; -export const RulesTable = ({ - perPage: pageSize, - rules_page: items, - total, - loading, - error, - selectedRuleId, - selectedRules, - setSelectedRules, - onRuleClick, -}: RulesTableProps) => { +export const RulesTable = ({ selectedRuleId, onRuleClick }: RulesTableProps) => { const { euiTheme } = useEuiTheme(); - const { setRulesQuery, setPageSize, page } = useRules(); + const { setRulesQuery, setPageSize, page, setSelectedRules, selectedRules } = useRules(); + const { pageSize, rulesQuery, rulesPageData } = useRules(); + const items = rulesPageData.rules_page; + const total = rulesPageData.total; + const error = rulesPageData.error; + const loading = rulesPageData.loading; + const euiPagination: EuiBasicTableProps['pagination'] = { pageIndex: page, - pageSize, + pageSize: pageSize || rulesQuery.perPage, totalItemCount: total, pageSizeOptions: [10, 25, 100], }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx index 939ad06c4f31b..731932593348e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx @@ -27,9 +27,8 @@ import { RuleStateAttributesWithoutStates, useChangeCspRuleState, } from './use_change_csp_rule_state'; -import { CspBenchmarkRulesWithStates } from './rules_container'; import { MultiSelectFilter } from '../../common/component/multi_select_filter'; -import { useRules } from './rules_context'; +import { CspBenchmarkRulesWithStates, useRules } from './rules_context'; export const RULES_BULK_ACTION_BUTTON = 'bulk-action-button'; export const RULES_BULK_ACTION_OPTION_ENABLE = 'bulk-action-option-enable'; @@ -40,41 +39,16 @@ export const RULES_DISABLED_FILTER = 'rules-disabled-filter'; export const RULES_ENABLED_FILTER = 'rules-enabled-filter'; interface RulesTableToolbarProps { - sectionSelectOptions: string[]; - ruleNumberSelectOptions: string[]; - totalRulesCount: number; - searchValue: string; isSearching: boolean; - pageSize: number; - selectedRules: CspBenchmarkRulesWithStates[]; - setEnabledDisabledItemsFilter: (filterState: string) => void; - enabledDisabledItemsFilterState: string; - setSelectAllRules: () => void; - setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; } - -interface RuleTableCount { - pageSize: number; - total: number; - selectedRules: CspBenchmarkRulesWithStates[]; - setSelectAllRules: () => void; - setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; -} - -export const RulesTableHeader = ({ - searchValue, - isSearching, - totalRulesCount, - pageSize, - sectionSelectOptions, - ruleNumberSelectOptions, - selectedRules, - setEnabledDisabledItemsFilter, - enabledDisabledItemsFilterState, - setSelectAllRules, - setSelectedRules, -}: RulesTableToolbarProps) => { - const { setRulesQuery } = useRules(); +export const RulesTableHeader = ({ isSearching }: RulesTableToolbarProps) => { + const { + setRulesQuery, + sectionSelectOptions, + ruleNumberSelectOptions, + setEnabledDisabledItemsFilter, + enabledDisabledItemsFilter, + } = useRules(); const [selectedSection, setSelectedSection] = useState([]); const [selectedRuleNumber, setSelectedRuleNumber] = useState([]); const sectionOptions = sectionSelectOptions.map((option) => ({ @@ -87,12 +61,12 @@ export const RulesTableHeader = ({ })); const toggleEnabledRulesFilter = () => { - if (enabledDisabledItemsFilterState === 'enabled') setEnabledDisabledItemsFilter('no-filter'); + if (enabledDisabledItemsFilter === 'enabled') setEnabledDisabledItemsFilter('no-filter'); else setEnabledDisabledItemsFilter('enabled'); }; const toggleDisabledRulesFilter = () => { - if (enabledDisabledItemsFilterState === 'disabled') setEnabledDisabledItemsFilter('no-filter'); + if (enabledDisabledItemsFilter === 'disabled') setEnabledDisabledItemsFilter('no-filter'); else setEnabledDisabledItemsFilter('disabled'); }; @@ -100,7 +74,7 @@ export const RulesTableHeader = ({ - + @@ -160,7 +134,7 @@ export const RulesTableHeader = ({ @@ -170,7 +144,7 @@ export const RulesTableHeader = ({ /> @@ -185,13 +159,7 @@ export const RulesTableHeader = ({ - + ); @@ -199,11 +167,9 @@ export const RulesTableHeader = ({ const SEARCH_DEBOUNCE_MS = 300; -const SearchField = ({ - isSearching, - searchValue, -}: Pick) => { - const [localValue, setLocalValue] = useState(searchValue); +const SearchField = ({ isSearching }: Pick) => { + const { rulesQuery } = useRules(); + const [localValue, setLocalValue] = useState(rulesQuery.search); const { setRulesQuery } = useRules(); useDebounce( @@ -233,13 +199,8 @@ const SearchField = ({ ); }; -const CurrentPageOfTotal = ({ - pageSize, - total, - selectedRules, - setSelectAllRules, - setSelectedRules, -}: RuleTableCount) => { +const CurrentPageOfTotal = () => { + const { selectedRules, setSelectedRules, rulesPageData, setSelectAllRules } = useRules(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onPopoverClick = () => { setIsPopoverOpen((e) => !e); @@ -309,6 +270,9 @@ const CurrentPageOfTotal = ({ , ]; + const pageSize = rulesPageData.rules_page.length; + const total = rulesPageData.all_rules.length; + return ( From f56f039662e2add3f3db1674e31ff1c3c7b61b69 Mon Sep 17 00:00:00 2001 From: Sean Rathier Date: Wed, 30 Oct 2024 10:10:09 -0400 Subject: [PATCH 4/9] Fixed paging --- .../public/pages/rules/rules_context.tsx | 184 +++++++++--------- .../public/pages/rules/rules_table.tsx | 19 +- 2 files changed, 103 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx index 6e8db09a5865a..5816130737b88 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -9,6 +9,7 @@ import { useParams } from 'react-router-dom'; import { CspBenchmarkRule, + FindCspBenchmarkRuleResponse, PageUrlParams, RuleStateAttributes, } from '@kbn/cloud-security-posture-common/schema/rules/latest'; @@ -42,8 +43,6 @@ interface RulesContextValue { rulesQuery: RulesQuery; rulesPageData: RulesPageData; setRulesQuery: (query: Partial) => void; - pageSize: number; - setPageSize: (pageSize: number) => void; page: number; setPage: (page: number) => void; sectionList: string[] | undefined; @@ -61,27 +60,6 @@ interface RulesContextValue { const RulesContext = createContext(undefined); const MAX_ITEMS_PER_PAGE = 10000; -const getPage = (data: CspBenchmarkRulesWithStates[], { page, perPage }: RulesQuery) => - data.slice(page * perPage, (page + 1) * perPage); - -const getRulesPageData = ( - data: CspBenchmarkRulesWithStates[], - status: string, - error: unknown, - query: RulesQuery -): RulesPageData => { - const page = getPage(data, query); - - return { - loading: status === 'loading', - error: error ? extractErrorMessage(error) : undefined, - all_rules: data, - rules_map: new Map(data.map((rule) => [rule.metadata.id, rule])), - rules_page: page, - total: data?.length || 0, - }; -}; - export function RulesProvider({ children }: RulesProviderProps) { const params = useParams(); const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); @@ -90,7 +68,7 @@ export function RulesProvider({ children }: RulesProviderProps) { section: undefined, ruleNumber: undefined, search: '', - page: 0, + page: 1, perPage: pageSize || 10, sortField: 'metadata.benchmark.rule_number', sortOrder: 'asc', @@ -109,45 +87,15 @@ export function RulesProvider({ children }: RulesProviderProps) { params.benchmarkVersion ); - // This useEffect is in charge of auto paginating to the correct page of a rule from the url params - useEffect(() => { - const getPageByRuleId = () => { - if (params.ruleId && allRules?.data?.items) { - const ruleIndex = allRules?.data?.items.findIndex( - (rule) => rule.metadata.id === params.ruleId - ); - - if (ruleIndex !== -1) { - // Calculate the page based on the rule index and page size - const rulePage = Math.floor(ruleIndex / pageSize); - return rulePage; - } - } - return 0; - }; - - setRulesQuery({ - ...rulesQuery, - page: getPageByRuleId(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allRules?.data?.items]); - const setRulesQueryCallback = useCallback( - () => (query: Partial) => { + (query: Partial) => { setRulesQuery({ ...rulesQuery, ...query }); + if (query.perPage) { + // set the local storage page size + setPageSize(query.perPage); + } }, - [rulesQuery, setRulesQuery] - ); - - const setPageSizeCallback = useCallback( - (value: number) => { - if (value === pageSize) return; - - setPageSize(value); - setRulesQuery({ ...rulesQuery, ...{ perPage: value } }); - }, - [rulesQuery, setPageSize, pageSize] + [rulesQuery, setRulesQuery, setPageSize] ); const setPageCallback = useCallback( @@ -190,35 +138,14 @@ export function RulesProvider({ children }: RulesProviderProps) { const rulesStates = useCspGetRulesStates(); - const rulesWithStates: CspBenchmarkRulesWithStates[] = useMemo(() => { - if (!data) return []; - - return data.items - .filter((rule: CspBenchmarkRule) => rule.metadata.benchmark.rule_number !== undefined) - .map((rule: CspBenchmarkRule) => { - const rulesKey = buildRuleKey( - rule.metadata.benchmark.id, - rule.metadata.benchmark.version, - /* Rule number always exists* from 8.7 */ - rule.metadata.benchmark.rule_number! - ); - - const match = rulesStates?.data?.[rulesKey]; - const rulesState = match?.muted ? 'muted' : 'unmuted'; - - return { ...rule, state: rulesState || 'unmuted' }; - }); - }, [data, rulesStates?.data]); + const rulesWithStates = getRulesWithStates(data, rulesStates); const mutedRulesCount = rulesWithStates.filter((rule) => rule.state === 'muted').length; - const filteredRulesWithStates: CspBenchmarkRulesWithStates[] = useMemo(() => { - if (enabledDisabledItemsFilter === 'disabled') - return rulesWithStates?.filter((rule) => rule?.state === 'muted'); - else if (enabledDisabledItemsFilter === 'enabled') - return rulesWithStates?.filter((rule) => rule?.state === 'unmuted'); - else return rulesWithStates; - }, [rulesWithStates, enabledDisabledItemsFilter]); + const filteredRulesWithStates = getFilteredRulesWithStates( + rulesWithStates, + enabledDisabledItemsFilter + ); const rulesPageData = useMemo( () => getRulesPageData(filteredRulesWithStates, status, error, rulesQuery), @@ -246,13 +173,35 @@ export function RulesProvider({ children }: RulesProviderProps) { }; }, [rulesStates, params.ruleId, allRules]); - const contextValue: RulesContextValue = useMemo( + // This useEffect is in charge of auto paginating to the correct page of a rule from the url params + useEffect(() => { + const getPageByRuleId = () => { + if (params.ruleId && allRules?.data?.items) { + const ruleIndex = allRules?.data?.items.findIndex( + (rule) => rule.metadata.id === params.ruleId + ); + + if (ruleIndex !== -1) { + // Calculate the page based on the rule index and page size + const rulePage = Math.floor(ruleIndex / pageSize); + return rulePage; + } + } + return 0; + }; + + setRulesQuery({ + ...rulesQuery, + page: getPageByRuleId(), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allRules?.data?.items]); + + const contextValue = useMemo( () => ({ rulesQuery, rulesPageData, setRulesQuery: setRulesQueryCallback, - pageSize, - setPageSize: setPageSizeCallback, page, setPage: setPageCallback, sectionList, @@ -270,8 +219,6 @@ export function RulesProvider({ children }: RulesProviderProps) { rulesQuery, rulesPageData, setRulesQueryCallback, - pageSize, - setPageSizeCallback, page, setPageCallback, sectionList, @@ -290,6 +237,38 @@ export function RulesProvider({ children }: RulesProviderProps) { return {children}; } +const getFilteredRulesWithStates = ( + rulesWithStates: CspBenchmarkRulesWithStates[], + enabledDisabledItemsFilter: string +) => { + if (enabledDisabledItemsFilter === 'disabled') + return rulesWithStates?.filter((rule) => rule?.state === 'muted'); + else if (enabledDisabledItemsFilter === 'enabled') + return rulesWithStates?.filter((rule) => rule?.state === 'unmuted'); + else return rulesWithStates; +}; + +const getPage = (data: CspBenchmarkRulesWithStates[], { page, perPage }: RulesQuery) => + data.slice(page * perPage, (page + 1) * perPage); + +const getRulesPageData = ( + data: CspBenchmarkRulesWithStates[], + status: string, + error: unknown, + query: RulesQuery +): RulesPageData => { + const page = getPage(data, query); + + return { + loading: status === 'loading', + error: error ? extractErrorMessage(error) : undefined, + all_rules: data, + rules_map: new Map(data.map((rule) => [rule.metadata.id, rule])), + rules_page: page, + total: data?.length || 0, + }; +}; + export function useRules() { const context = useContext(RulesContext); if (context === undefined) { @@ -297,3 +276,26 @@ export function useRules() { } return context; } + +const getRulesWithStates = ( + data: FindCspBenchmarkRuleResponse | undefined, + rulesStates: ReturnType +): CspBenchmarkRulesWithStates[] => { + if (!data) return []; + + return data.items + .filter((rule: CspBenchmarkRule) => rule.metadata.benchmark.rule_number !== undefined) + .map((rule: CspBenchmarkRule) => { + const rulesKey = buildRuleKey( + rule.metadata.benchmark.id, + rule.metadata.benchmark.version, + /* Rule number always exists* from 8.7 */ + rule.metadata.benchmark.rule_number! + ); + + const match = rulesStates?.data?.[rulesKey]; + const rulesState = match?.muted ? 'muted' : 'unmuted'; + + return { ...rule, state: rulesState || 'unmuted' }; + }); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index 4e3f021830608..27451f3eeb6ad 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -52,8 +52,8 @@ type GetColumnProps = Pick & { export const RulesTable = ({ selectedRuleId, onRuleClick }: RulesTableProps) => { const { euiTheme } = useEuiTheme(); - const { setRulesQuery, setPageSize, page, setSelectedRules, selectedRules } = useRules(); - const { pageSize, rulesQuery, rulesPageData } = useRules(); + const { setRulesQuery, page, setSelectedRules, selectedRules, rulesQuery, rulesPageData } = + useRules(); const items = rulesPageData.rules_page; const total = rulesPageData.total; const error = rulesPageData.error; @@ -61,15 +61,15 @@ export const RulesTable = ({ selectedRuleId, onRuleClick }: RulesTableProps) => const euiPagination: EuiBasicTableProps['pagination'] = { pageIndex: page, - pageSize: pageSize || rulesQuery.perPage, + pageSize: rulesQuery.perPage, totalItemCount: total, pageSizeOptions: [10, 25, 100], }; - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); + const sorting: EuiTableSortingType = { sort: { field: 'metadata.benchmark.rule_number' as keyof CspBenchmarkRulesWithStates, - direction: sortDirection, + direction: rulesQuery.sortOrder, }, }; const onTableChange = ({ @@ -77,12 +77,13 @@ export const RulesTable = ({ selectedRuleId, onRuleClick }: RulesTableProps) => sort: sortOrder, }: Criteria) => { if (!pagination) return; - if (pagination) { - setPageSize(pagination.size); + if ( + pagination && + (pagination.index !== rulesQuery.page || pagination.size !== rulesQuery.perPage) + ) { setRulesQuery({ page: pagination.index, perPage: pagination.size }); } - if (sortOrder) { - setSortDirection(sortOrder.direction); + if (sortOrder && sortOrder.direction !== rulesQuery.sortOrder) { setRulesQuery({ sortOrder: sortOrder.direction }); } }; From c62b038af22d1266353ae2e5ee76f45a92f37c3f Mon Sep 17 00:00:00 2001 From: Sean Rathier Date: Wed, 30 Oct 2024 18:07:58 -0400 Subject: [PATCH 5/9] Mutation function for muting and enabling moved to context --- .../common/utils/ui_metrics.ts | 10 +-- .../public/components/take_action.tsx | 8 +-- .../public/pages/rules/rules_context.tsx | 60 ++++++++++++++++++ .../public/pages/rules/rules_flyout.tsx | 61 +++++++------------ .../public/pages/rules/rules_table.tsx | 28 +-------- .../public/pages/rules/rules_table_header.tsx | 40 +++--------- 6 files changed, 103 insertions(+), 104 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts index c7baeb47bc214..e38af0a901393 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts @@ -27,9 +27,9 @@ export const ENTITY_FLYOUT_EXPAND_MISCONFIGURATION_VIEW_VISITS = 'entity-flyout-expand-misconfiguration-view-visits'; export const ENTITY_FLYOUT_EXPAND_VULNERABILITY_VIEW_VISITS = 'entity-flyout-expand-vulnerability-view-visits'; -export const NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT = +export const NAV_TO_FINDINGS_BY_HOST_NAME_FROM_ENTITY_FLYOUT = 'nav-to-findings-by-host-name-from-entity-flyout'; -export const NAV_TO_FINDINGS_BY_RULE_NAME_FRPOM_ENTITY_FLYOUT = +export const NAV_TO_FINDINGS_BY_RULE_NAME_FROM_ENTITY_FLYOUT = 'nav-to-findings-by-rule-name-from-entity-flyout'; export const CREATE_DETECTION_RULE_FROM_FLYOUT = 'create-detection-rule-from-flyout'; export const CREATE_DETECTION_FROM_TABLE_ROW_ACTION = 'create-detection-from-table-row-action'; @@ -37,20 +37,22 @@ export const VULNERABILITIES_FLYOUT_VISITS = 'vulnerabilities-flyout-visits'; export const OPEN_FINDINGS_FLYOUT = 'open-findings-flyout'; export const GROUP_BY_CLICK = 'group-by-click'; export const CHANGE_RULE_STATE = 'change-rule-state'; +export const CHANGE_MULTIPLE_RULE_STATE = 'change-multiple-rule-state'; type CloudSecurityUiCounters = | typeof ENTITY_FLYOUT_WITH_MISCONFIGURATION_VISIT | typeof ENTITY_FLYOUT_WITH_VULNERABILITY_PREVIEW | typeof ENTITY_FLYOUT_EXPAND_MISCONFIGURATION_VIEW_VISITS | typeof ENTITY_FLYOUT_EXPAND_VULNERABILITY_VIEW_VISITS - | typeof NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT - | typeof NAV_TO_FINDINGS_BY_RULE_NAME_FRPOM_ENTITY_FLYOUT + | typeof NAV_TO_FINDINGS_BY_HOST_NAME_FROM_ENTITY_FLYOUT + | typeof NAV_TO_FINDINGS_BY_RULE_NAME_FROM_ENTITY_FLYOUT | typeof VULNERABILITIES_FLYOUT_VISITS | typeof OPEN_FINDINGS_FLYOUT | typeof CREATE_DETECTION_RULE_FROM_FLYOUT | typeof CREATE_DETECTION_FROM_TABLE_ROW_ACTION | typeof GROUP_BY_CLICK | typeof CHANGE_RULE_STATE + | typeof CHANGE_MULTIPLE_RULE_STATE | typeof MISCONFIGURATION_INSIGHT_HOST_DETAILS | typeof MISCONFIGURATION_INSIGHT_USER_DETAILS | typeof MISCONFIGURATION_INSIGHT_HOST_ENTITY_OVERVIEW diff --git a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx index 8c7bcb5886d8d..7b61eadc67ae6 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx @@ -37,8 +37,8 @@ const RULE_PAGE_PATH = '/app/security/rules/id/'; interface TakeActionProps { createRuleFn?: (http: HttpSetup) => Promise; - enableBenchmarkRuleFn?: () => Promise; - disableBenchmarkRuleFn?: () => Promise; + enableBenchmarkRuleFn?: () => void; + disableBenchmarkRuleFn?: () => void; isCreateDetectionRuleDisabled?: boolean; isDataGridControlColumn?: boolean; } @@ -263,7 +263,7 @@ const EnableBenchmarkRule = ({ setIsLoading, closePopover, }: { - enableBenchmarkRuleFn: () => Promise; + enableBenchmarkRuleFn: () => void; setIsLoading: (isLoading: boolean) => void; closePopover: () => void; }) => { @@ -288,7 +288,7 @@ const DisableBenchmarkRule = ({ setIsLoading, closePopover, }: { - disableBenchmarkRuleFn: () => Promise; + disableBenchmarkRuleFn: () => void; setIsLoading: (isLoading: boolean) => void; closePopover: () => void; }) => { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx index 5816130737b88..3cc4e91112f12 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -14,8 +14,18 @@ import { RuleStateAttributes, } from '@kbn/cloud-security-posture-common/schema/rules/latest'; import { extractErrorMessage } from '@kbn/cloud-security-posture-common'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { + CHANGE_MULTIPLE_RULE_STATE, + CHANGE_RULE_STATE, + uiMetricService, +} from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { buildRuleKey } from '../../../common/utils/rules_states'; import { RulesQuery, useFindCspBenchmarkRule } from './use_csp_benchmark_rules'; +import { + RuleStateAttributesWithoutStates, + useChangeCspRuleState, +} from './use_change_csp_rule_state'; import { usePageSize } from '../../common/hooks/use_page_size'; import { LOCAL_STORAGE_PAGE_SIZE_RULES_KEY } from '../../common/constants'; import { useCspGetRulesStates } from './use_csp_rules_state'; @@ -52,6 +62,8 @@ interface RulesContextValue { setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; setSelectAllRules: () => void; setEnabledDisabledItemsFilter: (filter: string) => void; + toggleRuleState: (rule: CspBenchmarkRulesWithStates) => void; + toggleSelectedRulesStates: (state: 'mute' | 'unmute') => void; enabledDisabledItemsFilter: string; mutedRulesCount?: number; rulesFlyoutData: CspBenchmarkRulesWithStates; @@ -63,6 +75,7 @@ const MAX_ITEMS_PER_PAGE = 10000; export function RulesProvider({ children }: RulesProviderProps) { const params = useParams(); const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); + const { mutate: mutateRuleState } = useChangeCspRuleState(); const [page, setPage] = useState(1); const [rulesQuery, setRulesQuery] = useState({ section: undefined, @@ -147,6 +160,49 @@ export function RulesProvider({ children }: RulesProviderProps) { enabledDisabledItemsFilter ); + const toggleRuleState = useCallback( + (rule: CspBenchmarkRulesWithStates) => { + if (rule.metadata.benchmark.rule_number) { + uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_RULE_STATE); + const nextRuleStates = rule.state === 'muted' ? 'unmute' : 'mute'; + const rulesObjectRequest: RuleStateAttributesWithoutStates = { + benchmark_id: rule.metadata.benchmark.id, + benchmark_version: rule.metadata.benchmark.version, + rule_number: rule.metadata.benchmark.rule_number, + rule_id: rule.metadata.id, + }; + mutateRuleState({ + newState: nextRuleStates, + ruleIds: [rulesObjectRequest], + }); + } + }, + [mutateRuleState] + ); + + const toggleSelectedRulesStates = useCallback( + (state: 'mute' | 'unmute') => { + const bulkSelectedRules: RuleStateAttributesWithoutStates[] = selectedRules.map( + (e: CspBenchmarkRulesWithStates) => ({ + benchmark_id: e?.metadata.benchmark.id, + benchmark_version: e?.metadata.benchmark.version, + rule_number: e?.metadata.benchmark.rule_number!, + rule_id: e?.metadata.id, + }) + ); + // Only do the API Call IF there are no undefined value for rule number in the selected rules + if (!bulkSelectedRules.some((rule) => rule.rule_number === undefined)) { + uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_MULTIPLE_RULE_STATE); + mutateRuleState({ + newState: state, + ruleIds: bulkSelectedRules, + }); + } + setSelectedRules([]); + }, + [selectedRules, mutateRuleState] + ); + const rulesPageData = useMemo( () => getRulesPageData(filteredRulesWithStates, status, error, rulesQuery), [filteredRulesWithStates, status, error, rulesQuery] @@ -211,6 +267,8 @@ export function RulesProvider({ children }: RulesProviderProps) { setSelectedRules, setSelectAllRules, setEnabledDisabledItemsFilter, + toggleRuleState, + toggleSelectedRulesStates, enabledDisabledItemsFilter, mutedRulesCount, rulesFlyoutData, @@ -229,6 +287,8 @@ export function RulesProvider({ children }: RulesProviderProps) { setSelectAllRules, setEnabledDisabledItemsFilter, enabledDisabledItemsFilter, + toggleRuleState, + toggleSelectedRulesStates, mutedRulesCount, rulesFlyoutData, ] diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx index 7447d82d251ee..2bc0b0af42739 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx @@ -27,10 +27,9 @@ import type { CspBenchmarkRuleMetadata } from '@kbn/cloud-security-posture-commo import { getRuleList } from '../configurations/findings_flyout/rule_tab'; import { getRemediationList } from '../configurations/findings_flyout/overview_tab'; import * as TEST_SUBJECTS from './test_subjects'; -import { useChangeCspRuleState } from './use_change_csp_rule_state'; -import { CspBenchmarkRulesWithStates } from './rules_container'; import { TakeAction } from '../../components/take_action'; import { createDetectionRuleFromBenchmarkRule } from '../configurations/utils/create_detection_rule_from_benchmark'; +import { CspBenchmarkRulesWithStates, useRules } from './rules_context'; export const RULES_FLYOUT_SWITCH_BUTTON = 'rule-flyout-switch-button'; @@ -60,25 +59,9 @@ type RuleTab = (typeof tabs)[number]['id']; export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => { const [tab, setTab] = useState('overview'); + const { toggleRuleState } = useRules(); const isRuleMuted = rule?.state === 'muted'; - const { mutate: mutateRuleState } = useChangeCspRuleState(); - - const switchRuleStates = async () => { - if (rule.metadata.benchmark.rule_number) { - const rulesObjectRequest = { - benchmark_id: rule.metadata.benchmark.id, - benchmark_version: rule.metadata.benchmark.version, - rule_number: rule.metadata.benchmark.rule_number, - rule_id: rule.metadata.id, - }; - const nextRuleStates = isRuleMuted ? 'unmute' : 'mute'; - mutateRuleState({ - newState: nextRuleStates, - ruleIds: [rulesObjectRequest], - }); - } - }; const createMisconfigurationRuleFn = async (http: HttpSetup) => await createDetectionRuleFromBenchmarkRule(http, rule.metadata); @@ -107,13 +90,7 @@ export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => { - {tab === 'overview' && ( - - )} + {tab === 'overview' && } {tab === 'remediation' && ( )} @@ -123,13 +100,13 @@ export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => { {isRuleMuted ? ( toggleRuleState(rule)} createRuleFn={createMisconfigurationRuleFn} isCreateDetectionRuleDisabled={true} /> ) : ( toggleRuleState(rule)} createRuleFn={createMisconfigurationRuleFn} isCreateDetectionRuleDisabled={false} /> @@ -144,22 +121,26 @@ export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => { const RuleOverviewTab = ({ rule, ruleData, - switchRuleStates, }: { rule: CspBenchmarkRuleMetadata; ruleData: CspBenchmarkRulesWithStates; - switchRuleStates: () => Promise; -}) => ( - - - - - -); +}) => { + const { toggleRuleState } = useRules(); + return ( + + + toggleRuleState(ruleData)), + ...getRuleList(rule, ruleData.state), + ]} + /> + + + ); +}; -const ruleState = (rule: CspBenchmarkRulesWithStates, switchRuleStates: () => Promise) => [ +const ruleState = (rule: CspBenchmarkRulesWithStates, switchRuleStates: () => void) => [ { title: ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index 27451f3eeb6ad..66a392a17d083 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -19,15 +19,9 @@ import { EuiTableSortingType, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { METRIC_TYPE } from '@kbn/analytics'; -import { - CHANGE_RULE_STATE, - uiMetricService, -} from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { uniqBy } from 'lodash'; import { ColumnNameWithTooltip } from '../../components/column_name_with_tooltip'; import * as TEST_SUBJECTS from './test_subjects'; -import { useChangeCspRuleState } from './use_change_csp_rule_state'; import { CspBenchmarkRulesWithStates, useRules } from './rules_context'; export const RULES_ROWS_ENABLE_SWITCH_BUTTON = 'rules-row-enable-switch-button'; @@ -262,34 +256,16 @@ const getColumns = ({ ]; const RuleStateSwitch = ({ rule }: { rule: CspBenchmarkRulesWithStates }) => { + const { toggleRuleState } = useRules(); const isRuleMuted = rule?.state === 'muted'; - const nextRuleState = isRuleMuted ? 'unmute' : 'mute'; - - const { mutate: mutateRulesStates } = useChangeCspRuleState(); - const rulesObjectRequest = { - benchmark_id: rule?.metadata.benchmark.id, - benchmark_version: rule?.metadata.benchmark.version, - /* Rule number always exists from 8.7 */ - rule_number: rule?.metadata.benchmark.rule_number!, - rule_id: rule?.metadata.id, - }; - const changeCspRuleStateFn = async () => { - if (rule?.metadata.benchmark.rule_number) { - uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_RULE_STATE); - mutateRulesStates({ - newState: nextRuleState, - ruleIds: [rulesObjectRequest], - }); - } - }; return ( toggleRuleState(rule)} data-test-subj={RULES_ROWS_ENABLE_SWITCH_BUTTON} label="" compressed={true} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx index 731932593348e..33a2c7756bf0f 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx @@ -23,12 +23,8 @@ import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; -import { - RuleStateAttributesWithoutStates, - useChangeCspRuleState, -} from './use_change_csp_rule_state'; import { MultiSelectFilter } from '../../common/component/multi_select_filter'; -import { CspBenchmarkRulesWithStates, useRules } from './rules_context'; +import { useRules } from './rules_context'; export const RULES_BULK_ACTION_BUTTON = 'bulk-action-button'; export const RULES_BULK_ACTION_OPTION_ENABLE = 'bulk-action-option-enable'; @@ -200,39 +196,23 @@ const SearchField = ({ isSearching }: Pick { - const { selectedRules, setSelectedRules, rulesPageData, setSelectAllRules } = useRules(); + const { + selectedRules, + setSelectedRules, + rulesPageData, + setSelectAllRules, + toggleSelectedRulesStates, + } = useRules(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onPopoverClick = () => { setIsPopoverOpen((e) => !e); }; - const { mutate: mutateRulesStates } = useChangeCspRuleState(); - - const changeCspRuleState = (state: 'mute' | 'unmute') => { - const bulkSelectedRules: RuleStateAttributesWithoutStates[] = selectedRules.map( - (e: CspBenchmarkRulesWithStates) => ({ - benchmark_id: e?.metadata.benchmark.id, - benchmark_version: e?.metadata.benchmark.version, - rule_number: e?.metadata.benchmark.rule_number!, - rule_id: e?.metadata.id, - }) - ); - // Only do the API Call IF there are no undefined value for rule number in the selected rules - if (!bulkSelectedRules.some((rule) => rule.rule_number === undefined)) { - mutateRulesStates({ - newState: state, - ruleIds: bulkSelectedRules, - }); - setIsPopoverOpen(false); - } - setSelectedRules([]); - }; - const changeCspRuleStateMute = () => { - changeCspRuleState('mute'); + toggleSelectedRulesStates('mute'); }; const changeCspRuleStateUnmute = () => { - changeCspRuleState('unmute'); + toggleSelectedRulesStates('unmute'); }; const areAllSelectedRulesMuted = selectedRules.every((rule) => rule?.state === 'muted'); From 65f72b1b8b44fcc556db8a667b85e8bc08418e51 Mon Sep 17 00:00:00 2001 From: Sean Rathier Date: Thu, 31 Oct 2024 12:39:29 -0400 Subject: [PATCH 6/9] Moved useRules --- .../public/pages/rules/rules_context.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx index 3cc4e91112f12..3f15f3cd3dc90 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -72,6 +72,14 @@ interface RulesContextValue { const RulesContext = createContext(undefined); const MAX_ITEMS_PER_PAGE = 10000; +export function useRules() { + const context = useContext(RulesContext); + if (context === undefined) { + throw new Error('useRules must be used within a RulesProvider'); + } + return context; +} + export function RulesProvider({ children }: RulesProviderProps) { const params = useParams(); const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); @@ -329,14 +337,6 @@ const getRulesPageData = ( }; }; -export function useRules() { - const context = useContext(RulesContext); - if (context === undefined) { - throw new Error('useRules must be used within a RulesProvider'); - } - return context; -} - const getRulesWithStates = ( data: FindCspBenchmarkRuleResponse | undefined, rulesStates: ReturnType From b0f149103531451aab93d4cfba793491c073ee64 Mon Sep 17 00:00:00 2001 From: Sean Rathier Date: Tue, 5 Nov 2024 18:16:18 -0500 Subject: [PATCH 7/9] Removed RulesQuery --- .../public/pages/rules/rules_context.tsx | 163 +++++++++--------- .../public/pages/rules/rules_table.tsx | 35 ++-- .../public/pages/rules/rules_table_header.tsx | 49 +++--- 3 files changed, 125 insertions(+), 122 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx index 3f15f3cd3dc90..14ef4dcf534da 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { createContext, useContext, useMemo, useEffect, useCallback, useState } from 'react'; +import React, { createContext, useContext, useMemo, useCallback, useState } from 'react'; import { useParams } from 'react-router-dom'; import { @@ -21,7 +21,7 @@ import { uiMetricService, } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { buildRuleKey } from '../../../common/utils/rules_states'; -import { RulesQuery, useFindCspBenchmarkRule } from './use_csp_benchmark_rules'; +import { useFindCspBenchmarkRule } from './use_csp_benchmark_rules'; import { RuleStateAttributesWithoutStates, useChangeCspRuleState, @@ -44,17 +44,27 @@ interface RulesPageData { loading: boolean; } -export type RulesState = RulesPageData & RulesQuery; - interface RulesProviderProps { children: React.ReactNode; } interface RulesContextValue { - rulesQuery: RulesQuery; - rulesPageData: RulesPageData; - setRulesQuery: (query: Partial) => void; + section: string[] | undefined; + setSection: (section: string[] | undefined) => void; page: number; setPage: (page: number) => void; + pageSize: number; + setPageSize: (pageSize: number) => void; + sortField: string; + setSortField: (sortField: string) => void; + sortOrder: 'asc' | 'desc'; + setSortOrder: (sortOrder: 'asc' | 'desc') => void; + ruleNumber: string[] | undefined; + setRuleNumber: (ruleNumber: string[] | undefined) => void; + search: string; + setSearch: (search: string) => void; + rulesPageData: RulesPageData; + // setRulesQuery: (query: Partial) => void; + sectionList: string[] | undefined; ruleNumberSelectOptions: string[]; sectionSelectOptions: string[]; @@ -83,17 +93,13 @@ export function useRules() { export function RulesProvider({ children }: RulesProviderProps) { const params = useParams(); const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); - const { mutate: mutateRuleState } = useChangeCspRuleState(); const [page, setPage] = useState(1); - const [rulesQuery, setRulesQuery] = useState({ - section: undefined, - ruleNumber: undefined, - search: '', - page: 1, - perPage: pageSize || 10, - sortField: 'metadata.benchmark.rule_number', - sortOrder: 'asc', - }); + const [search, setSearch] = useState(''); + const [sortField, setSortField] = useState('metadata.benchmark.rule_number'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + const [section, setSection] = useState(undefined); + const [ruleNumber, setRuleNumber] = useState(undefined); + const { mutate: mutateRuleState } = useChangeCspRuleState(); const [selectedRules, setSelectedRules] = useState([]); const [enabledDisabledItemsFilter, setEnabledDisabledItemsFilter] = useState('no-filter'); @@ -108,24 +114,6 @@ export function RulesProvider({ children }: RulesProviderProps) { params.benchmarkVersion ); - const setRulesQueryCallback = useCallback( - (query: Partial) => { - setRulesQuery({ ...rulesQuery, ...query }); - if (query.perPage) { - // set the local storage page size - setPageSize(query.perPage); - } - }, - [rulesQuery, setRulesQuery, setPageSize] - ); - - const setPageCallback = useCallback( - (value: number) => { - setPage(value); - }, - [setPage] - ); - const sectionList = useMemo( () => allRules?.data?.items.map((rule) => rule.metadata.section), [allRules] @@ -145,13 +133,13 @@ export function RulesProvider({ children }: RulesProviderProps) { const { data, status, error } = useFindCspBenchmarkRule( { - section: rulesQuery.section, - ruleNumber: rulesQuery.ruleNumber, - search: rulesQuery.search, - page: 1, + section, + ruleNumber, + search, + page, perPage: MAX_ITEMS_PER_PAGE, sortField: 'metadata.benchmark.rule_number', - sortOrder: rulesQuery.sortOrder, + sortOrder, }, params.benchmarkId, params.benchmarkVersion @@ -212,8 +200,8 @@ export function RulesProvider({ children }: RulesProviderProps) { ); const rulesPageData = useMemo( - () => getRulesPageData(filteredRulesWithStates, status, error, rulesQuery), - [filteredRulesWithStates, status, error, rulesQuery] + () => getRulesPageData(filteredRulesWithStates, status, error, page, pageSize), + [filteredRulesWithStates, status, error, page, pageSize] ); const setSelectAllRules = useCallback(() => { @@ -238,36 +226,42 @@ export function RulesProvider({ children }: RulesProviderProps) { }, [rulesStates, params.ruleId, allRules]); // This useEffect is in charge of auto paginating to the correct page of a rule from the url params - useEffect(() => { - const getPageByRuleId = () => { - if (params.ruleId && allRules?.data?.items) { - const ruleIndex = allRules?.data?.items.findIndex( - (rule) => rule.metadata.id === params.ruleId - ); - - if (ruleIndex !== -1) { - // Calculate the page based on the rule index and page size - const rulePage = Math.floor(ruleIndex / pageSize); - return rulePage; - } - } - return 0; - }; - - setRulesQuery({ - ...rulesQuery, - page: getPageByRuleId(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allRules?.data?.items]); + // useEffect(() => { + // const getPageByRuleId = () => { + // if (params.ruleId && allRules?.data?.items) { + // const ruleIndex = allRules?.data?.items.findIndex( + // (rule) => rule.metadata.id === params.ruleId + // ); + + // if (ruleIndex !== -1) { + // // Calculate the page based on the rule index and page size + // const rulePage = Math.floor(ruleIndex / pageSize); + // return rulePage; + // } + // } + // return 1; + // }; + + // setPage(getPageByRuleId()); + // }, [allRules?.data?.items, params.ruleId, pageSize]); const contextValue = useMemo( () => ({ - rulesQuery, - rulesPageData, - setRulesQuery: setRulesQueryCallback, + section, + setSection, page, - setPage: setPageCallback, + pageSize, + setPageSize, + sortField, + setSortField, + sortOrder, + setSortOrder, + ruleNumber, + setRuleNumber, + search, + setSearch, + rulesPageData, + setPage, sectionList, ruleNumberSelectOptions, sectionSelectOptions, @@ -282,12 +276,22 @@ export function RulesProvider({ children }: RulesProviderProps) { rulesFlyoutData, }), [ - rulesQuery, rulesPageData, - setRulesQueryCallback, + sortField, + setSortField, + sortOrder, + setSortOrder, + pageSize, + setPageSize, page, - setPageCallback, + setPage, + section, + setSection, + search, + setSearch, sectionList, + ruleNumber, + setRuleNumber, ruleNumberSelectOptions, sectionSelectOptions, selectedRules, @@ -309,30 +313,33 @@ const getFilteredRulesWithStates = ( rulesWithStates: CspBenchmarkRulesWithStates[], enabledDisabledItemsFilter: string ) => { + let rulesWithStatesFiltered = rulesWithStates; if (enabledDisabledItemsFilter === 'disabled') - return rulesWithStates?.filter((rule) => rule?.state === 'muted'); + rulesWithStatesFiltered = rulesWithStates?.filter((rule) => rule?.state === 'muted'); else if (enabledDisabledItemsFilter === 'enabled') - return rulesWithStates?.filter((rule) => rule?.state === 'unmuted'); - else return rulesWithStates; + rulesWithStatesFiltered = rulesWithStates?.filter((rule) => rule?.state === 'unmuted'); + + return rulesWithStatesFiltered; }; -const getPage = (data: CspBenchmarkRulesWithStates[], { page, perPage }: RulesQuery) => +const getPage = (data: CspBenchmarkRulesWithStates[], page: number, perPage: number) => data.slice(page * perPage, (page + 1) * perPage); const getRulesPageData = ( data: CspBenchmarkRulesWithStates[], status: string, error: unknown, - query: RulesQuery + page: number, + perPage: number ): RulesPageData => { - const page = getPage(data, query); + const pageIndex = getPage(data, page, perPage); return { loading: status === 'loading', error: error ? extractErrorMessage(error) : undefined, all_rules: data, rules_map: new Map(data.map((rule) => [rule.metadata.id, rule])), - rules_page: page, + rules_page: pageIndex, total: data?.length || 0, }; }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index 66a392a17d083..a5d14b1ec787b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -46,16 +46,24 @@ type GetColumnProps = Pick & { export const RulesTable = ({ selectedRuleId, onRuleClick }: RulesTableProps) => { const { euiTheme } = useEuiTheme(); - const { setRulesQuery, page, setSelectedRules, selectedRules, rulesQuery, rulesPageData } = - useRules(); - const items = rulesPageData.rules_page; + const { + page, + setSelectedRules, + selectedRules, + rulesPageData, + pageSize, + setPageSize, + sortOrder, + setSortOrder, + } = useRules(); + const items = rulesPageData.all_rules; const total = rulesPageData.total; const error = rulesPageData.error; const loading = rulesPageData.loading; const euiPagination: EuiBasicTableProps['pagination'] = { pageIndex: page, - pageSize: rulesQuery.perPage, + pageSize, totalItemCount: total, pageSizeOptions: [10, 25, 100], }; @@ -63,22 +71,17 @@ export const RulesTable = ({ selectedRuleId, onRuleClick }: RulesTableProps) => const sorting: EuiTableSortingType = { sort: { field: 'metadata.benchmark.rule_number' as keyof CspBenchmarkRulesWithStates, - direction: rulesQuery.sortOrder, + direction: sortOrder, }, }; - const onTableChange = ({ - page: pagination, - sort: sortOrder, - }: Criteria) => { + const onTableChange = ({ page: pagination, sort }: Criteria) => { if (!pagination) return; - if ( - pagination && - (pagination.index !== rulesQuery.page || pagination.size !== rulesQuery.perPage) - ) { - setRulesQuery({ page: pagination.index, perPage: pagination.size }); + if (pagination && (pagination.index !== page || pagination.size !== pageSize)) { + setPageSize(pagination.size); + setPageSize(pagination.index); } - if (sortOrder && sortOrder.direction !== rulesQuery.sortOrder) { - setRulesQuery({ sortOrder: sortOrder.direction }); + if (sort && sort.direction !== sortOrder) { + setSortOrder(sort.direction); } }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx index 33a2c7756bf0f..35474851a0b9a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx @@ -39,14 +39,15 @@ interface RulesTableToolbarProps { } export const RulesTableHeader = ({ isSearching }: RulesTableToolbarProps) => { const { - setRulesQuery, + section, + setSection, + ruleNumber, + setRuleNumber, sectionSelectOptions, ruleNumberSelectOptions, setEnabledDisabledItemsFilter, enabledDisabledItemsFilter, } = useRules(); - const [selectedSection, setSelectedSection] = useState([]); - const [selectedRuleNumber, setSelectedRuleNumber] = useState([]); const sectionOptions = sectionSelectOptions.map((option) => ({ key: option, label: option, @@ -87,14 +88,15 @@ export const RulesTableHeader = ({ isSearching }: RulesTableToolbarProps) => { } )} id={'cis-section-multi-select-filter'} - onChange={(section) => { - setSelectedSection([...section?.selectedOptionKeys]); - setRulesQuery({ - section: section?.selectedOptionKeys ? section?.selectedOptionKeys : undefined, - }); + onChange={(changedSections) => { + setSection( + changedSections?.selectedOptionKeys + ? changedSections?.selectedOptionKeys + : undefined + ); }} options={sectionOptions} - selectedOptionKeys={selectedSection} + selectedOptionKeys={section} /> { } )} id={'rule-number-multi-select-filter'} - onChange={(ruleNumber) => { - setSelectedRuleNumber([...ruleNumber?.selectedOptionKeys]); - setRulesQuery({ - ruleNumber: ruleNumber?.selectedOptionKeys - ? ruleNumber?.selectedOptionKeys - : undefined, - }); + onChange={(changedRuleNumbers) => { + setRuleNumber( + changedRuleNumbers?.selectedOptionKeys + ? changedRuleNumbers?.selectedOptionKeys + : undefined + ); }} options={ruleNumberOptions} - selectedOptionKeys={selectedRuleNumber} + selectedOptionKeys={ruleNumber} /> { const SEARCH_DEBOUNCE_MS = 300; const SearchField = ({ isSearching }: Pick) => { - const { rulesQuery } = useRules(); - const [localValue, setLocalValue] = useState(rulesQuery.search); - const { setRulesQuery } = useRules(); + const { search, setSearch } = useRules(); + const [localValue, setLocalValue] = useState(search); - useDebounce( - () => - setRulesQuery({ - search: localValue, - }), - SEARCH_DEBOUNCE_MS, - [localValue] - ); + useDebounce(() => setSearch(localValue), SEARCH_DEBOUNCE_MS, [localValue]); return (
From ffc8807a3fc4b8282ce490d77292acceb0f8bff4 Mon Sep 17 00:00:00 2001 From: Sean Rathier Date: Thu, 7 Nov 2024 16:51:29 -0500 Subject: [PATCH 8/9] Removed RulesPage and streamlined --- .../public/pages/rules/rules_context.tsx | 119 +++++++++--------- .../public/pages/rules/rules_table.tsx | 12 +- .../public/pages/rules/rules_table_header.tsx | 10 +- 3 files changed, 68 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx index 14ef4dcf534da..4ddf050c56d11 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { createContext, useContext, useMemo, useCallback, useState } from 'react'; +import React, { createContext, useContext, useMemo, useCallback, useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { @@ -35,14 +35,14 @@ export interface CspBenchmarkRulesWithStates { state: 'muted' | 'unmuted'; } -interface RulesPageData { - rules_page: CspBenchmarkRulesWithStates[]; - all_rules: CspBenchmarkRulesWithStates[]; - rules_map: Map; - total: number; - error?: string; - loading: boolean; -} +// interface RulesPageData { +// rules_page: CspBenchmarkRulesWithStates[]; +// all_rules: CspBenchmarkRulesWithStates[]; +// rules_map: Map; +// total: number; +// error?: string; +// loading: boolean; +// } interface RulesProviderProps { children: React.ReactNode; @@ -62,7 +62,13 @@ interface RulesContextValue { setRuleNumber: (ruleNumber: string[] | undefined) => void; search: string; setSearch: (search: string) => void; - rulesPageData: RulesPageData; + // rulesPageData: RulesPageData; + loading: boolean; + error?: string; + total: number; + rules: CspBenchmarkRulesWithStates[]; + rulesShown: number; + // setRulesQuery: (query: Partial) => void; sectionList: string[] | undefined; @@ -93,7 +99,7 @@ export function useRules() { export function RulesProvider({ children }: RulesProviderProps) { const params = useParams(); const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); - const [page, setPage] = useState(1); + const [page, setPage] = useState(0); const [search, setSearch] = useState(''); const [sortField, setSortField] = useState('metadata.benchmark.rule_number'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); @@ -136,7 +142,7 @@ export function RulesProvider({ children }: RulesProviderProps) { section, ruleNumber, search, - page, + page: 1, perPage: MAX_ITEMS_PER_PAGE, sortField: 'metadata.benchmark.rule_number', sortOrder, @@ -156,6 +162,15 @@ export function RulesProvider({ children }: RulesProviderProps) { enabledDisabledItemsFilter ); + const rules = + filteredRulesWithStates.length > pageSize + ? filteredRulesWithStates.slice(page * pageSize, (page + 1) * pageSize) + : filteredRulesWithStates; + + const rulesShown = rules.length; + const total = filteredRulesWithStates.length; + const loading = status === 'loading'; + const toggleRuleState = useCallback( (rule: CspBenchmarkRulesWithStates) => { if (rule.metadata.benchmark.rule_number) { @@ -199,14 +214,9 @@ export function RulesProvider({ children }: RulesProviderProps) { [selectedRules, mutateRuleState] ); - const rulesPageData = useMemo( - () => getRulesPageData(filteredRulesWithStates, status, error, page, pageSize), - [filteredRulesWithStates, status, error, page, pageSize] - ); - const setSelectAllRules = useCallback(() => { - setSelectedRules(rulesPageData.all_rules); - }, [rulesPageData.all_rules]); + setSelectedRules(filteredRulesWithStates); + }, [filteredRulesWithStates]); const rulesFlyoutData: CspBenchmarkRulesWithStates = useMemo(() => { const arrayRulesStates: RuleStateAttributes[] = Object.values(rulesStates.data || {}); @@ -226,24 +236,24 @@ export function RulesProvider({ children }: RulesProviderProps) { }, [rulesStates, params.ruleId, allRules]); // This useEffect is in charge of auto paginating to the correct page of a rule from the url params - // useEffect(() => { - // const getPageByRuleId = () => { - // if (params.ruleId && allRules?.data?.items) { - // const ruleIndex = allRules?.data?.items.findIndex( - // (rule) => rule.metadata.id === params.ruleId - // ); - - // if (ruleIndex !== -1) { - // // Calculate the page based on the rule index and page size - // const rulePage = Math.floor(ruleIndex / pageSize); - // return rulePage; - // } - // } - // return 1; - // }; - - // setPage(getPageByRuleId()); - // }, [allRules?.data?.items, params.ruleId, pageSize]); + useEffect(() => { + const getPageByRuleId = () => { + if (params.ruleId && allRules?.data?.items) { + const ruleIndex = allRules?.data?.items.findIndex( + (rule) => rule.metadata.id === params.ruleId + ); + + if (ruleIndex !== -1) { + // Calculate the page based on the rule index and page size + const rulePage = Math.floor(ruleIndex / pageSize); + return rulePage; + } + } + return 0; + }; + + setPage(getPageByRuleId()); + }, [allRules?.data?.items, params.ruleId, pageSize]); const contextValue = useMemo( () => ({ @@ -260,7 +270,12 @@ export function RulesProvider({ children }: RulesProviderProps) { setRuleNumber, search, setSearch, - rulesPageData, + // rulesPageData, + loading, + error: error ? extractErrorMessage(error) : undefined, + total, + rules, + rulesShown, setPage, sectionList, ruleNumberSelectOptions, @@ -276,7 +291,11 @@ export function RulesProvider({ children }: RulesProviderProps) { rulesFlyoutData, }), [ - rulesPageData, + rules, + error, + loading, + rulesShown, + total, sortField, setSortField, sortOrder, @@ -322,28 +341,6 @@ const getFilteredRulesWithStates = ( return rulesWithStatesFiltered; }; -const getPage = (data: CspBenchmarkRulesWithStates[], page: number, perPage: number) => - data.slice(page * perPage, (page + 1) * perPage); - -const getRulesPageData = ( - data: CspBenchmarkRulesWithStates[], - status: string, - error: unknown, - page: number, - perPage: number -): RulesPageData => { - const pageIndex = getPage(data, page, perPage); - - return { - loading: status === 'loading', - error: error ? extractErrorMessage(error) : undefined, - all_rules: data, - rules_map: new Map(data.map((rule) => [rule.metadata.id, rule])), - rules_page: pageIndex, - total: data?.length || 0, - }; -}; - const getRulesWithStates = ( data: FindCspBenchmarkRuleResponse | undefined, rulesStates: ReturnType diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index a5d14b1ec787b..2cbe5c2d4732b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -48,18 +48,18 @@ export const RulesTable = ({ selectedRuleId, onRuleClick }: RulesTableProps) => const { euiTheme } = useEuiTheme(); const { page, + setPage, setSelectedRules, selectedRules, - rulesPageData, + rules: items, + total, + error, + loading, pageSize, setPageSize, sortOrder, setSortOrder, } = useRules(); - const items = rulesPageData.all_rules; - const total = rulesPageData.total; - const error = rulesPageData.error; - const loading = rulesPageData.loading; const euiPagination: EuiBasicTableProps['pagination'] = { pageIndex: page, @@ -78,7 +78,7 @@ export const RulesTable = ({ selectedRuleId, onRuleClick }: RulesTableProps) => if (!pagination) return; if (pagination && (pagination.index !== page || pagination.size !== pageSize)) { setPageSize(pagination.size); - setPageSize(pagination.index); + setPage(pagination.index); } if (sort && sort.direction !== sortOrder) { setSortOrder(sort.direction); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx index 35474851a0b9a..7324222c12483 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx @@ -192,7 +192,8 @@ const CurrentPageOfTotal = () => { const { selectedRules, setSelectedRules, - rulesPageData, + rulesShown, + total, setSelectAllRules, toggleSelectedRulesStates, } = useRules(); @@ -243,9 +244,6 @@ const CurrentPageOfTotal = () => { , ]; - const pageSize = rulesPageData.rules_page.length; - const total = rulesPageData.all_rules.length; - return ( @@ -254,9 +252,9 @@ const CurrentPageOfTotal = () => { Date: Fri, 8 Nov 2024 14:02:10 -0500 Subject: [PATCH 9/9] Code review changes --- .../public/pages/rules/index.tsx | 5 +- .../public/pages/rules/rules_container.tsx | 19 +- .../public/pages/rules/rules_context.tsx | 240 +++++++----------- .../public/pages/rules/rules_flyout.tsx | 9 +- .../public/pages/rules/rules_table_header.tsx | 13 +- 5 files changed, 117 insertions(+), 169 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx index c1377b6193a34..f404b6a82d215 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx @@ -19,7 +19,6 @@ import { useSecuritySolutionContext } from '../../application/security_solution_ import { useCspBenchmarkIntegrationsV2 } from '../benchmarks/use_csp_benchmark_integrations'; import { CISBenchmarkIcon } from '../../components/cis_benchmark_icon'; import { getBenchmarkCisName } from '../../../common/utils/helpers'; -import { RulesProvider } from './rules_context'; export const Rules = ({ match: { params } }: RouteComponentProps) => { const benchmarksInfo = useCspBenchmarkIntegrationsV2(); @@ -64,9 +63,7 @@ export const Rules = ({ match: { params } }: RouteComponentProps) } /> - - - + {SpyRoute && ( { const params = useParams(); const history = useHistory(); - const { rulesFlyoutData } = useRules(); const navToRuleFlyout = (ruleId: string) => { history.push( @@ -42,14 +41,14 @@ export const RulesContainer = () => { return (
- - - - - - {params.ruleId && rulesFlyoutData.metadata && ( - - )} + + + + + + + +
); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx index 4ddf050c56d11..f6e28a38611e8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { createContext, useContext, useMemo, useCallback, useState, useEffect } from 'react'; +import React, { createContext, useContext, useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { @@ -82,7 +82,7 @@ interface RulesContextValue { toggleSelectedRulesStates: (state: 'mute' | 'unmute') => void; enabledDisabledItemsFilter: string; mutedRulesCount?: number; - rulesFlyoutData: CspBenchmarkRulesWithStates; + rulesFlyoutData: CspBenchmarkRulesWithStates | undefined; } const RulesContext = createContext(undefined); @@ -120,18 +120,10 @@ export function RulesProvider({ children }: RulesProviderProps) { params.benchmarkVersion ); - const sectionList = useMemo( - () => allRules?.data?.items.map((rule) => rule.metadata.section), - [allRules] - ); + const sectionList = allRules?.data?.items.map((rule) => rule.metadata.section) || []; - const ruleNumberSelectOptions = useMemo( - () => - allRules.data - ? allRules.data.items.map((rule) => rule.metadata.benchmark.rule_number || '') - : [], - [allRules] - ); + const ruleNumberSelectOptions = + allRules?.data?.items.map((rule) => rule.metadata.benchmark.rule_number || '') || []; const sectionSelectOptions = [...new Set(sectionList)].sort((a, b) => { return a.localeCompare(b, 'en', { sensitivity: 'base' }); @@ -171,69 +163,65 @@ export function RulesProvider({ children }: RulesProviderProps) { const total = filteredRulesWithStates.length; const loading = status === 'loading'; - const toggleRuleState = useCallback( - (rule: CspBenchmarkRulesWithStates) => { - if (rule.metadata.benchmark.rule_number) { - uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_RULE_STATE); - const nextRuleStates = rule.state === 'muted' ? 'unmute' : 'mute'; - const rulesObjectRequest: RuleStateAttributesWithoutStates = { - benchmark_id: rule.metadata.benchmark.id, - benchmark_version: rule.metadata.benchmark.version, - rule_number: rule.metadata.benchmark.rule_number, - rule_id: rule.metadata.id, - }; - mutateRuleState({ - newState: nextRuleStates, - ruleIds: [rulesObjectRequest], - }); - } - }, - [mutateRuleState] - ); - - const toggleSelectedRulesStates = useCallback( - (state: 'mute' | 'unmute') => { - const bulkSelectedRules: RuleStateAttributesWithoutStates[] = selectedRules.map( - (e: CspBenchmarkRulesWithStates) => ({ - benchmark_id: e?.metadata.benchmark.id, - benchmark_version: e?.metadata.benchmark.version, - rule_number: e?.metadata.benchmark.rule_number!, - rule_id: e?.metadata.id, - }) - ); - // Only do the API Call IF there are no undefined value for rule number in the selected rules - if (!bulkSelectedRules.some((rule) => rule.rule_number === undefined)) { - uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_MULTIPLE_RULE_STATE); - mutateRuleState({ - newState: state, - ruleIds: bulkSelectedRules, - }); - } - setSelectedRules([]); - }, - [selectedRules, mutateRuleState] - ); - - const setSelectAllRules = useCallback(() => { + const toggleRuleState = (rule: CspBenchmarkRulesWithStates) => { + if (rule.metadata.benchmark.rule_number) { + uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_RULE_STATE); + const nextRuleStates = rule.state === 'muted' ? 'unmute' : 'mute'; + const rulesObjectRequest: RuleStateAttributesWithoutStates = { + benchmark_id: rule.metadata.benchmark.id, + benchmark_version: rule.metadata.benchmark.version, + rule_number: rule.metadata.benchmark.rule_number, + rule_id: rule.metadata.id, + }; + mutateRuleState({ + newState: nextRuleStates, + ruleIds: [rulesObjectRequest], + }); + } + }; + + const toggleSelectedRulesStates = (state: 'mute' | 'unmute') => { + const bulkSelectedRules: RuleStateAttributesWithoutStates[] = selectedRules.map( + (e: CspBenchmarkRulesWithStates) => ({ + benchmark_id: e?.metadata.benchmark.id, + benchmark_version: e?.metadata.benchmark.version, + rule_number: e?.metadata.benchmark.rule_number!, + rule_id: e?.metadata.id, + }) + ); + // Only do the API Call IF there are no undefined value for rule number in the selected rules + if (!bulkSelectedRules.some((rule) => rule.rule_number === undefined)) { + uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_MULTIPLE_RULE_STATE); + mutateRuleState({ + newState: state, + ruleIds: bulkSelectedRules, + }); + } + setSelectedRules([]); + }; + + const setSelectAllRules = () => { setSelectedRules(filteredRulesWithStates); - }, [filteredRulesWithStates]); - - const rulesFlyoutData: CspBenchmarkRulesWithStates = useMemo(() => { - const arrayRulesStates: RuleStateAttributes[] = Object.values(rulesStates.data || {}); - return { - ...{ - state: - arrayRulesStates.find((filteredRuleState) => filteredRuleState.rule_id === params.ruleId) - ?.muted === true - ? 'muted' - : 'unmuted', - }, - ...{ - metadata: allRules?.data?.items.find((rule) => rule.metadata.id === params.ruleId) - ?.metadata!, - }, - }; - }, [rulesStates, params.ruleId, allRules]); + }; + + const arrayRulesStates: RuleStateAttributes[] = Object.values(rulesStates.data || {}); + const rulesFlyoutData: CspBenchmarkRulesWithStates | undefined = + !arrayRulesStates || !params.ruleId + ? undefined + : { + ...{ + state: + arrayRulesStates.find( + (filteredRuleState) => filteredRuleState.rule_id === params.ruleId + )?.muted === true + ? 'muted' + : 'unmuted', + }, + ...{ + metadata: allRules?.data?.items.find((rule) => rule.metadata.id === params.ruleId) + ?.metadata!, + }, + }; // This useEffect is in charge of auto paginating to the correct page of a rule from the url params useEffect(() => { @@ -255,75 +243,39 @@ export function RulesProvider({ children }: RulesProviderProps) { setPage(getPageByRuleId()); }, [allRules?.data?.items, params.ruleId, pageSize]); - const contextValue = useMemo( - () => ({ - section, - setSection, - page, - pageSize, - setPageSize, - sortField, - setSortField, - sortOrder, - setSortOrder, - ruleNumber, - setRuleNumber, - search, - setSearch, - // rulesPageData, - loading, - error: error ? extractErrorMessage(error) : undefined, - total, - rules, - rulesShown, - setPage, - sectionList, - ruleNumberSelectOptions, - sectionSelectOptions, - selectedRules, - setSelectedRules, - setSelectAllRules, - setEnabledDisabledItemsFilter, - toggleRuleState, - toggleSelectedRulesStates, - enabledDisabledItemsFilter, - mutedRulesCount, - rulesFlyoutData, - }), - [ - rules, - error, - loading, - rulesShown, - total, - sortField, - setSortField, - sortOrder, - setSortOrder, - pageSize, - setPageSize, - page, - setPage, - section, - setSection, - search, - setSearch, - sectionList, - ruleNumber, - setRuleNumber, - ruleNumberSelectOptions, - sectionSelectOptions, - selectedRules, - setSelectedRules, - setSelectAllRules, - setEnabledDisabledItemsFilter, - enabledDisabledItemsFilter, - toggleRuleState, - toggleSelectedRulesStates, - mutedRulesCount, - rulesFlyoutData, - ] - ); + const contextValue = { + section, + setSection, + page, + pageSize, + setPageSize, + sortField, + setSortField, + sortOrder, + setSortOrder, + ruleNumber, + setRuleNumber, + search, + setSearch, + loading, + error: error ? extractErrorMessage(error) : undefined, + total, + rules, + rulesShown, + setPage, + sectionList, + ruleNumberSelectOptions, + sectionSelectOptions, + selectedRules, + setSelectedRules, + setSelectAllRules, + setEnabledDisabledItemsFilter, + toggleRuleState, + toggleSelectedRulesStates, + enabledDisabledItemsFilter, + mutedRulesCount, + rulesFlyoutData, + }; return {children}; } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx index 2bc0b0af42739..d04ce342171d7 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx @@ -35,7 +35,6 @@ export const RULES_FLYOUT_SWITCH_BUTTON = 'rule-flyout-switch-button'; interface RuleFlyoutProps { onClose(): void; - rule: CspBenchmarkRulesWithStates; } const tabs = [ @@ -57,9 +56,13 @@ const tabs = [ type RuleTab = (typeof tabs)[number]['id']; -export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => { +export const RuleFlyout = ({ onClose }: RuleFlyoutProps) => { const [tab, setTab] = useState('overview'); - const { toggleRuleState } = useRules(); + const { toggleRuleState, rulesFlyoutData: rule } = useRules(); + + if (!rule) { + return null; + } const isRuleMuted = rule?.state === 'muted'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx index 7324222c12483..b7fca82c235b5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx @@ -34,10 +34,7 @@ export const RULES_CLEAR_ALL_RULES_SELECTION = 'clear-rules-selection-button'; export const RULES_DISABLED_FILTER = 'rules-disabled-filter'; export const RULES_ENABLED_FILTER = 'rules-enabled-filter'; -interface RulesTableToolbarProps { - isSearching: boolean; -} -export const RulesTableHeader = ({ isSearching }: RulesTableToolbarProps) => { +export const RulesTableHeader = () => { const { section, setSection, @@ -71,7 +68,7 @@ export const RulesTableHeader = ({ isSearching }: RulesTableToolbarProps) => { - + @@ -164,8 +161,8 @@ export const RulesTableHeader = ({ isSearching }: RulesTableToolbarProps) => { const SEARCH_DEBOUNCE_MS = 300; -const SearchField = ({ isSearching }: Pick) => { - const { search, setSearch } = useRules(); +const SearchField = () => { + const { search, setSearch, loading } = useRules(); const [localValue, setLocalValue] = useState(search); useDebounce(() => setSearch(localValue), SEARCH_DEBOUNCE_MS, [localValue]); @@ -174,7 +171,7 @@ const SearchField = ({ isSearching }: Pick