From 2c8edb384af302d97016883023d119a13c2be483 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Thu, 23 May 2024 11:10:18 -0300 Subject: [PATCH 01/12] Fix cannot perform a React state update on an unmounted component --- .../common/search-bar/search-bar-service.ts | 17 ++-- .../last-alerts-stat/last-alerts-service.ts | 7 +- .../last-alerts-stat/last-alerts-stat.tsx | 84 +++++++++++-------- 3 files changed, 67 insertions(+), 41 deletions(-) diff --git a/plugins/main/public/components/common/search-bar/search-bar-service.ts b/plugins/main/public/components/common/search-bar/search-bar-service.ts index d3a3e6543f..dd0cc1f0c7 100644 --- a/plugins/main/public/components/common/search-bar/search-bar-service.ts +++ b/plugins/main/public/components/common/search-bar/search-bar-service.ts @@ -51,8 +51,13 @@ export function getForceNow() { //////////////////////////////////////////////////////////////////////////////////// +interface Options { + signal?: AbortSignal; +} + export const search = async ( params: SearchParams, + options: Options = {}, ): Promise => { const { indexPattern, @@ -123,7 +128,7 @@ export const search = async ( searchSource.setField('aggs', aggs); } try { - return await searchParams.fetch(); + return await searchParams.fetch({ signal: options.signal }); } catch (error) { if (error.body) { throw error.body; @@ -133,10 +138,10 @@ export const search = async ( }; const getValueDisplayedOnFilter = (filter: tFilter) => { - return filter.query?.bool?.minimum_should_match === 1 ? - `is one of ${filter.meta?.value}` : - filter.meta?.params?.query || filter.meta?.value; -} + return filter.query?.bool?.minimum_should_match === 1 + ? `is one of ${filter.meta?.value}` + : filter.meta?.params?.query || filter.meta?.value; +}; export const hideCloseButtonOnFixedFilters = ( filters: tFilter[], @@ -152,7 +157,7 @@ export const hideCloseButtonOnFixedFilters = ( index, filter, field: filter.meta?.key, - value: getValueDisplayedOnFilter(filter) + value: getValueDisplayedOnFilter(filter), }; } }) diff --git a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-service.ts b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-service.ts index 11b0822a9d..0a54533519 100644 --- a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-service.ts +++ b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-service.ts @@ -12,12 +12,17 @@ interface Last24HoursAlerts { indexPatternName: string; } +interface Options { + signal?: AbortSignal; +} + /** * This fetch the last 24 hours alerts from the selected cluster * TODO: The search function should be moved to a common place */ export const getLast24HoursAlerts = async ( ruleLevelRange, + options: Options = {}, ): Promise => { try { const currentIndexPattern = await getDataPlugin().indexPatterns.get( @@ -36,7 +41,7 @@ export const getLast24HoursAlerts = async ( ruleLevelRange, ); - const result = await search(lastAlertsQuery); + const result = await search(lastAlertsQuery, options); const count = result?.hits?.total; return { count, diff --git a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx index b8576905bc..a6fd122720 100644 --- a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx +++ b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { EuiStat, EuiFlexItem, @@ -19,6 +19,8 @@ import { export function LastAlertsStat({ severity }: { severity: string }) { const [countLastAlerts, setCountLastAlerts] = useState(null); const [discoverLocation, setDiscoverLocation] = useState(''); + const isMounted = useRef(true); + const severityLabel = { low: { label: 'Low', @@ -54,50 +56,64 @@ export function LastAlertsStat({ severity }: { severity: string }) { }; useEffect(() => { + const controller = new AbortController(); + const signal = controller.signal; const getCountLastAlerts = async () => { try { const { indexPatternName, cluster, count } = await getLast24HoursAlerts( severityLabel[severity].ruleLevelRange, + { signal }, ); - setCountLastAlerts(count); - const core = getCore(); - let discoverLocation = { - app: 'data-explorer', - basePath: 'discover', - }; + if (isMounted.current) { + setCountLastAlerts(count); + const core = getCore(); + + let discoverLocation = { + app: 'data-explorer', + basePath: 'discover', + }; - // TODO: find a better way to get the query discover URL - const destURL = core.application.getUrlForApp(discoverLocation.app, { - path: `${ - discoverLocation.basePath - }#?_a=(discover:(columns:!(_source),isDirty:!f,sort:!()),metadata:(indexPattern:'${indexPatternName}',view:discover))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'${indexPatternName}',key:${ - cluster.field - },negate:!f,params:(query:${ - cluster.name - }),type:phrase),query:(match_phrase:(${cluster.field}:${ - cluster.name - }))),('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'wazuh-alerts-*',key:rule.level,negate:!f,params:(gte:${ - severityLabel[severity].ruleLevelRange.minRuleLevel - },lte:${ - severityLabel[severity].ruleLevelRange.maxRuleLevel || '!n' - }),type:range),range:(rule.level:(gte:${ - severityLabel[severity].ruleLevelRange.minRuleLevel - },lte:${ - severityLabel[severity].ruleLevelRange.maxRuleLevel || '!n' - })))),refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_q=(filters:!(),query:(language:kuery,query:''))`, - }); - setDiscoverLocation(destURL); + // TODO: find a better way to get the query discover URL + const destURL = core.application.getUrlForApp(discoverLocation.app, { + path: `${ + discoverLocation.basePath + }#?_a=(discover:(columns:!(_source),isDirty:!f,sort:!()),metadata:(indexPattern:'${indexPatternName}',view:discover))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'${indexPatternName}',key:${ + cluster.field + },negate:!f,params:(query:${ + cluster.name + }),type:phrase),query:(match_phrase:(${cluster.field}:${ + cluster.name + }))),('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'wazuh-alerts-*',key:rule.level,negate:!f,params:(gte:${ + severityLabel[severity].ruleLevelRange.minRuleLevel + },lte:${ + severityLabel[severity].ruleLevelRange.maxRuleLevel || '!n' + }),type:range),range:(rule.level:(gte:${ + severityLabel[severity].ruleLevelRange.minRuleLevel + },lte:${ + severityLabel[severity].ruleLevelRange.maxRuleLevel || '!n' + })))),refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_q=(filters:!(),query:(language:kuery,query:''))`, + }); + setDiscoverLocation(destURL); + } } catch (error) { - const searchError = ErrorFactory.create(HttpError, { - error, - message: 'Error fetching last alerts', - }); - ErrorHandler.handleError(searchError); + if (error.name !== 'AbortError') { + const searchError = ErrorFactory.create(HttpError, { + error, + message: 'Error fetching last alerts', + }); + ErrorHandler.handleError(searchError); + } } }; + getCountLastAlerts(); - }, []); + + return () => { + isMounted.current = false; + controller.abort(); + }; + }, [severity]); return ( From 5f5ea316529c84055bb2c803de067c36320fc2bb Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Thu, 23 May 2024 11:26:52 -0300 Subject: [PATCH 02/12] Fix cannot perform a React state update on an unmounted component in AgentStats --- .../components/agents/stats/agent-stats.tsx | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/plugins/main/public/components/agents/stats/agent-stats.tsx b/plugins/main/public/components/agents/stats/agent-stats.tsx index a77b125b56..63c49a9eec 100644 --- a/plugins/main/public/components/agents/stats/agent-stats.tsx +++ b/plugins/main/public/components/agents/stats/agent-stats.tsx @@ -9,7 +9,7 @@ * * Find more information about this on the LICENSE file. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -148,8 +148,12 @@ function AgentStats({ agent }) { const [loading, setLoading] = useState(); const [dataStatLogcollector, setDataStatLogcollector] = useState({}); const [dataStatAgent, setDataStatAgent] = useState(); + const isMounted = useRef(false); + useEffect(() => { - (async function () { + isMounted.current = true; + + const fetchData = async () => { setLoading(true); try { const responseDataStatLogcollector = await WzRequest.apiReq( @@ -162,29 +166,43 @@ function AgentStats({ agent }) { `/agents/${agent.id}/stats/agent`, {}, ); - setDataStatLogcollector( - responseDataStatLogcollector?.data?.data?.affected_items?.[0] || {}, - ); - setDataStatAgent( - responseDataStatAgent?.data?.data?.affected_items?.[0] || undefined, - ); + + if (isMounted.current) { + setDataStatLogcollector( + responseDataStatLogcollector?.data?.data?.affected_items?.[0] || {}, + ); + setDataStatAgent( + responseDataStatAgent?.data?.data?.affected_items?.[0] || undefined, + ); + } } catch (error) { - const options: UIErrorLog = { - context: `${AgentStats.name}.useEffect`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); + if (isMounted.current) { + const options: UIErrorLog = { + context: `${AgentStats.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } } finally { - setLoading(false); + if (isMounted.current) { + setLoading(false); + } } - })(); - }, []); + }; + + fetchData(); + + return () => { + isMounted.current = false; + }; + }, [agent.id]); + return ( From 10d0655bb9a3db9af3021f625ec48549f73efd0b Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Thu, 23 May 2024 11:57:24 -0300 Subject: [PATCH 03/12] Fix cannot perform a React state update on an unmounted component in Endpoint summary inventory data --- .../components/common/modules/main-agent.tsx | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/plugins/main/public/components/common/modules/main-agent.tsx b/plugins/main/public/components/common/modules/main-agent.tsx index 3d27e26405..6225ccca90 100644 --- a/plugins/main/public/components/common/modules/main-agent.tsx +++ b/plugins/main/public/components/common/modules/main-agent.tsx @@ -39,20 +39,31 @@ import { } from '../data-source'; import { useAsyncAction } from '../hooks'; -export class MainModuleAgent extends Component { - props!: { - [key: string]: any; - }; - state: { - selectView: Boolean; - loadingReport: Boolean; - switchModule: Boolean; - showAgentInfo: Boolean; - }; +interface MainModuleAgentProps { + agent: any; + section: string; + selectView: boolean; + tabs?: any[]; + renderTabs: () => React.ReactNode; +} + +interface MainModuleAgentState { + selectView: boolean; + loadingReport: boolean; + switchModule: boolean; + showAgentInfo: boolean; +} + +export class MainModuleAgent extends Component< + MainModuleAgentProps, + MainModuleAgentState +> { + isMounted: boolean; reportingService: ReportingService; filterHandler: FilterHandler; + router: any; - constructor(props) { + constructor(props: MainModuleAgentProps) { super(props); this.reportingService = new ReportingService(); this.filterHandler = new FilterHandler(AppState.getCurrentPattern()); @@ -62,13 +73,19 @@ export class MainModuleAgent extends Component { switchModule: false, showAgentInfo: false, }; + this.isMounted = false; } async componentDidMount() { + this.isMounted = true; const $injector = getAngularModule().$injector; this.router = $injector.get('$route'); } + componentWillUnmount() { + this.isMounted = false; + } + renderTitle() { return ( From 9dce1d2234029225fc98493c1f58e6b02d55ec14 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Fri, 24 May 2024 11:41:38 -0300 Subject: [PATCH 04/12] fix cannot perform a react status update on an unmounted component in the TableWithSearchBar file --- .../common/tables/table-with-search-bar.tsx | 93 ++++++++++--------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/plugins/main/public/components/common/tables/table-with-search-bar.tsx b/plugins/main/public/components/common/tables/table-with-search-bar.tsx index 2c551192aa..1141faa0e1 100644 --- a/plugins/main/public/components/common/tables/table-with-search-bar.tsx +++ b/plugins/main/public/components/common/tables/table-with-search-bar.tsx @@ -126,6 +126,34 @@ export function TableWithSearchBar({ const isMounted = useRef(false); const tableRef = useRef(); + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + useEffect(() => { + // This effect is triggered when the component is mounted because of how to the useEffect hook works. + // We don't want to set the pagination state because there is another effect that has this dependency + // and will cause the effect is triggered (redoing the onSearch function). + if (isMounted.current) { + // Reset the page index when the endpoint or reload changes. + // This will cause that onSearch function is triggered because to changes in pagination in the another effect. + updateRefresh(); + } + }, [endpoint, reload]); + + useEffect(() => { + // This effect is triggered when the component is mounted because of how to the useEffect hook works. + // We don't want to set the filters state because there is another effect that has this dependency + // and will cause the effect is triggered (redoing the onSearch function). + if (isMounted.current && !_.isEqual(rest.filters, filters)) { + setFilters(rest.filters || {}); + updateRefresh(); + } + }, [rest.filters]); + const searchBarWQLOptions = useMemo( () => ({ searchTermFields: tableColumns @@ -163,34 +191,25 @@ export function TableWithSearchBar({ } useEffect(() => { - // This effect is triggered when the component is mounted because of how to the useEffect hook works. - // We don't want to set the pagination state because there is another effect that has this dependency - // and will cause the effect is triggered (redoing the onSearch function). - if (isMounted.current) { - // Reset the page index when the endpoint or reload changes. - // This will cause that onSearch function is triggered because to changes in pagination in the another effect. - updateRefresh(); - } - }, [endpoint, reload]); - - useEffect( - function () { - (async () => { - try { - setLoading(true); + (async () => { + try { + setLoading(true); - //Reset the table selection in case is enabled - tableRef.current.setSelection([]); + //Reset the table selection in case is enabled + tableRef.current.setSelection([]); - const { items, totalItems } = await onSearch( - endpoint, - filters, - pagination, - sorting, - ); + const { items, totalItems } = await onSearch( + endpoint, + filters, + pagination, + sorting, + ); + if (isMounted.current) { setItems(items); setTotalItems(totalItems); - } catch (error) { + } + } catch (error) { + if (isMounted.current) { setItems([]); setTotalItems(0); const options = { @@ -205,28 +224,12 @@ export function TableWithSearchBar({ }; getErrorOrchestrator().handleError(options); } + } + if (isMounted.current) { setLoading(false); - })(); - }, - [filters, pagination, sorting, refresh], - ); - - useEffect(() => { - // This effect is triggered when the component is mounted because of how to the useEffect hook works. - // We don't want to set the filters state because there is another effect that has this dependency - // and will cause the effect is triggered (redoing the onSearch function). - if (isMounted.current && !_.isEqual(rest.filters, filters)) { - setFilters(rest.filters || {}); - updateRefresh(); - } - }, [rest.filters]); - - // It is required that this effect runs after other effects that use isMounted - // to avoid that these effects run when the component is mounted, only running - // when one of its dependencies changes. - useEffect(() => { - isMounted.current = true; - }, []); + } + })(); + }, [filters, pagination, sorting, refresh]); const tablePagination = { ...pagination, From 011ee29eb4c8843dee51f60b88328a694c4b15d5 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Tue, 28 May 2024 15:39:20 -0300 Subject: [PATCH 05/12] Fix cannot perform a React state update on an unmounted component in TableWzAPI --- .../components/common/tables/table-wz-api.tsx | 110 +++++++++++------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/plugins/main/public/components/common/tables/table-wz-api.tsx b/plugins/main/public/components/common/tables/table-wz-api.tsx index 50ac453b8d..51e4ed499c 100644 --- a/plugins/main/public/components/common/tables/table-wz-api.tsx +++ b/plugins/main/public/components/common/tables/table-wz-api.tsx @@ -10,7 +10,13 @@ * Find more information about this on the LICENSE file. */ -import React, { ReactNode, useCallback, useEffect, useState } from 'react'; +import React, { + ReactNode, + useCallback, + useEffect, + useState, + useRef, +} from 'react'; import { EuiTitle, EuiLoadingSpinner, @@ -78,6 +84,8 @@ export function TableWzAPI({ const [filters, setFilters] = useState({}); const [isLoading, setIsLoading] = useState(false); + const isMounted = useRef(false); + const onFiltersChange = filters => typeof rest.onFiltersChange === 'function' ? rest.onFiltersChange(filters) @@ -102,56 +110,68 @@ export function TableWzAPI({ ); const [isOpenFieldSelector, setIsOpenFieldSelector] = useState(false); - const onSearch = useCallback(async function ( - endpoint, - filters, - pagination, - sorting, - ) { - try { - const { pageIndex, pageSize } = pagination; - const { field, direction } = sorting.sort; - setIsLoading(true); - setFilters(filters); - onFiltersChange(filters); - const params = { - ...getFilters(filters), - offset: pageIndex * pageSize, - limit: pageSize, - sort: `${direction === 'asc' ? '+' : '-'}${field}`, - }; + const controller = new AbortController(); + const signal = controller.signal; + + const onSearch = useCallback( + async function (endpoint, filters, pagination, sorting) { + try { + if (isMounted.current == false) { + const error = new Error('Abort error onSearch callback'); + error.name = 'AbortError'; + throw error; + } + const { pageIndex, pageSize } = pagination; + const { field, direction } = sorting.sort; + setIsLoading(true); + setFilters(filters); + onFiltersChange(filters); + const params = { + ...getFilters(filters), + offset: pageIndex * pageSize, + limit: pageSize, + sort: `${direction === 'asc' ? '+' : '-'}${field}`, + signal, + }; - const response = await WzRequest.apiReq('GET', endpoint, { params }); + const response = await WzRequest.apiReq('GET', endpoint, { params }); - const { affected_items: items, total_affected_items: totalItems } = ( - (response || {}).data || {} - ).data; - setIsLoading(false); - setTotalItems(totalItems); + const { affected_items: items, total_affected_items: totalItems } = ( + (response || {}).data || {} + ).data; + setIsLoading(false); + setTotalItems(totalItems); - const result = { - items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, - totalItems, - }; + const result = { + items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, + totalItems, + }; - onDataChange(result); + onDataChange(result); - return result; - } catch (error) { - setIsLoading(false); - setTotalItems(0); - if (error?.name) { - /* This replaces the error name. The intention is that an AxiosError + return result; + } catch (error) { + setIsLoading(false); + setTotalItems(0); + if (error.name !== 'AbortError') { + if (error?.name) { + /* This replaces the error name. The intention is that an AxiosError doesn't appear in the toast message. TODO: This should be managed by the service that does the request instead of only changing the name in this case. */ - error.name = 'RequestError'; + error.name = 'RequestError'; + } + throw error; + } + return { + items: [], + totalItems, + }; } - throw error; - } - }, - []); + }, + [isMounted], + ); const renderActionButtons = (actionButtons, filters) => { if (Array.isArray(actionButtons)) { @@ -182,7 +202,13 @@ export function TableWzAPI({ }; useEffect(() => { - if (rest.reload) triggerReload(); + isMounted.current = true; + if (rest.reload && isMounted.current == true) triggerReload(); + + return () => { + isMounted.current = false; + controller.abort(); + }; }, [rest.reload]); const ReloadButton = ( From 55a4cd7be464f33b61c7c01161825868521bb7e7 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Tue, 28 May 2024 15:51:21 -0300 Subject: [PATCH 06/12] Fix cannot perform a React state update on an unmounted component in useGenericRequest --- .../common/hooks/useGenericRequest.ts | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/plugins/main/public/components/common/hooks/useGenericRequest.ts b/plugins/main/public/components/common/hooks/useGenericRequest.ts index d8fd733cff..071cddeee7 100644 --- a/plugins/main/public/components/common/hooks/useGenericRequest.ts +++ b/plugins/main/public/components/common/hooks/useGenericRequest.ts @@ -12,33 +12,44 @@ import { export function useGenericRequest(method, path, params, formatFunction) { const [items, setItems] = useState({}); const [isLoading, setisLoading] = useState(true); - const [error, setError] = useState(""); + const [error, setError] = useState(''); - useEffect( () => { - try{ - setisLoading(true); - const fetchData = async() => { - const response = await GenericRequest.request(method, path, params); + useEffect(() => { + let isMounted = true; + + const fetchData = async () => { + try { + setisLoading(true); + const response = await GenericRequest.request(method, path, params); + if (isMounted) { setItems(response); setisLoading(false); } - fetchData(); - } catch(error) { - setError(error); - setisLoading(false); - const options: UIErrorLog = { - context: `${useGenericRequest.name}.fetchData`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - }, [params]); + } catch (error) { + if (isMounted) { + setError(error); + setisLoading(false); + const options: UIErrorLog = { + context: `${useGenericRequest.name}.fetchData`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + } + }; + + fetchData(); + + return () => { + isMounted = false; + }; + }, [method, path, params]); - return {isLoading, data: formatFunction(items), error}; + return { isLoading, data: formatFunction(items), error }; } From 62d8275ea700ac2597eb32cb6ff9e11d96cf7598 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Tue, 28 May 2024 16:07:39 -0300 Subject: [PATCH 07/12] Fix cannot perform a React state update on an unmounted component in index searchBar --- .../public/components/search-bar/index.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/main/public/components/search-bar/index.tsx b/plugins/main/public/components/search-bar/index.tsx index d0538739de..d7a3f80989 100644 --- a/plugins/main/public/components/search-bar/index.tsx +++ b/plugins/main/public/components/search-bar/index.tsx @@ -77,7 +77,6 @@ export const SearchBar = ({ const onChangeInput = (event: React.ChangeEvent) => setInput(event.target.value); - // Handler when pressing a key const onKeyPressHandler = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { _onSearch(queryLanguageOutputRun.output); @@ -89,6 +88,7 @@ export const SearchBar = ({ ); useEffect(() => { + let isMounted = true; // React to external changes and set the internal input text. Use the `transformInput` of // the query language in use rest.input && @@ -102,15 +102,14 @@ export const SearchBar = ({ }, ), ); - }, [rest.input]); - useEffect(() => { (async () => { // Set the query language output debounceUpdateSearchBarTimer.current && clearTimeout(debounceUpdateSearchBarTimer.current); // Debounce the updating of the search bar state debounceUpdateSearchBarTimer.current = setTimeout(async () => { + if (!isMounted) return; const queryLanguageOutput = await searchBarQueryLanguages[ queryLanguage.id ].run(input, { @@ -137,16 +136,26 @@ export const SearchBar = ({ setQueryLanguageOutputRun(queryLanguageOutput); }, SEARCH_BAR_DEBOUNCE_UPDATE_TIME); })(); + + return () => { + isMounted = false; + debounceUpdateSearchBarTimer.current && + clearTimeout(debounceUpdateSearchBarTimer.current); + }; }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); useEffect(() => { - onChange && + if (!onChange) return; + + if ( // Ensure the previous output is different to the new one !_.isEqual( queryLanguageOutputRun.output, queryLanguageOutputRunPreviousOutput.current, - ) && + ) + ) { onChange(queryLanguageOutputRun.output); + } }, [queryLanguageOutputRun.output]); const onQueryLanguagePopoverSwitch = () => From 63b284fcfac61efb2d9adcb4f48a09e95b5680ed Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Tue, 28 May 2024 16:12:10 -0300 Subject: [PATCH 08/12] Fix cannot perform a React state update on an unmounted component in useDataSource --- .../data-source/hooks/use-data-source.ts | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/plugins/main/public/components/common/data-source/hooks/use-data-source.ts b/plugins/main/public/components/common/data-source/hooks/use-data-source.ts index 9581980195..dc00da34be 100644 --- a/plugins/main/public/components/common/data-source/hooks/use-data-source.ts +++ b/plugins/main/public/components/common/data-source/hooks/use-data-source.ts @@ -89,6 +89,7 @@ export function useDataSource< }; useEffect(() => { + let isMounted = true; let subscription; (async () => { setIsLoading(true); @@ -103,31 +104,38 @@ export function useDataSource< if (!dataSource) { throw new Error('No valid data source found'); } - setDataSource(dataSource); - const dataSourceFilterManager = new PatternDataSourceFilterManager( - dataSource, - initialFilters, - injectedFilterManager, - initialFetchFilters, - ); - // what the filters update - subscription = dataSourceFilterManager.getUpdates$().subscribe({ - next: () => { - // this is necessary to remove the hidden filters from the filter manager and not show them in the search bar - dataSourceFilterManager.setFilters( - dataSourceFilterManager.getFilters(), - ); - setAllFilters(dataSourceFilterManager.getFilters()); - setFetchFilters(dataSourceFilterManager.getFetchFilters()); - }, - }); - setAllFilters(dataSourceFilterManager.getFilters()); - setFetchFilters(dataSourceFilterManager.getFetchFilters()); - setDataSourceFilterManager(dataSourceFilterManager); - setIsLoading(false); + if (isMounted) { + setDataSource(dataSource); + const dataSourceFilterManager = new PatternDataSourceFilterManager( + dataSource, + initialFilters, + injectedFilterManager, + initialFetchFilters, + ); + // what the filters update + subscription = dataSourceFilterManager.getUpdates$().subscribe({ + next: () => { + // this is necessary to remove the hidden filters from the filter manager and not show them in the search bar + if (isMounted) { + dataSourceFilterManager.setFilters( + dataSourceFilterManager.getFilters(), + ); + setAllFilters(dataSourceFilterManager.getFilters()); + setFetchFilters(dataSourceFilterManager.getFetchFilters()); + } + }, + }); + setAllFilters(dataSourceFilterManager.getFilters()); + setFetchFilters(dataSourceFilterManager.getFetchFilters()); + setDataSourceFilterManager(dataSourceFilterManager); + setIsLoading(false); + } })(); - - return () => subscription && subscription.unsubscribe(); + + return () => { + isMounted = false; + if (subscription) subscription.unsubscribe(); + }; }, []); useEffect(() => { From 4a9b93b1c36393986d226eb56794adb3e78a9aaa Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Mon, 3 Jun 2024 14:13:22 -0300 Subject: [PATCH 09/12] create and implement useIsMounted --- .../common/hooks/use-is-mounted.test.tsx | 24 ++++++++ .../components/common/hooks/use-is-mounted.ts | 15 +++++ .../last-alerts-stat/last-alerts-stat.tsx | 57 +++++++++---------- 3 files changed, 65 insertions(+), 31 deletions(-) create mode 100644 plugins/main/public/components/common/hooks/use-is-mounted.test.tsx create mode 100644 plugins/main/public/components/common/hooks/use-is-mounted.ts diff --git a/plugins/main/public/components/common/hooks/use-is-mounted.test.tsx b/plugins/main/public/components/common/hooks/use-is-mounted.test.tsx new file mode 100644 index 0000000000..4443dd6c38 --- /dev/null +++ b/plugins/main/public/components/common/hooks/use-is-mounted.test.tsx @@ -0,0 +1,24 @@ +import { renderHook } from '@testing-library/react'; + +import { useIsMounted } from './use-is-mounted'; + +describe('useIsMounted()', () => { + it('should return true when component is mounted', () => { + const { + result: { current: isMounted }, + } = renderHook(() => useIsMounted()); + + expect(isMounted()).toBe(true); + }); + + it('should return false when component is unmounted', () => { + const { + result: { current: isMounted }, + unmount, + } = renderHook(() => useIsMounted()); + + unmount(); + + expect(isMounted()).toBe(false); + }); +}); diff --git a/plugins/main/public/components/common/hooks/use-is-mounted.ts b/plugins/main/public/components/common/hooks/use-is-mounted.ts new file mode 100644 index 0000000000..51243a5ba2 --- /dev/null +++ b/plugins/main/public/components/common/hooks/use-is-mounted.ts @@ -0,0 +1,15 @@ +import { useCallback, useEffect, useRef } from 'react'; + +export function useIsMounted(): () => boolean { + const isMounted = useRef(false); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + + return useCallback(() => isMounted.current, []); +} diff --git a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx index c506e1871d..9fb6202fef 100644 --- a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx +++ b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx @@ -15,12 +15,12 @@ import { ErrorFactory, HttpError, } from '../../../../react-services/error-management'; +import { useIsMounted } from '../../../../components/common/hooks/use-is-mounted'; export function LastAlertsStat({ severity }: { severity: string }) { const [countLastAlerts, setCountLastAlerts] = useState(null); const [discoverLocation, setDiscoverLocation] = useState(''); - const isMounted = useRef(true); - + const isMounted = useIsMounted(); const severityLabel = { low: { label: 'Low', @@ -64,8 +64,7 @@ export function LastAlertsStat({ severity }: { severity: string }) { severityLabel[severity].ruleLevelRange, { signal }, ); - - if (isMounted.current) { + if (isMounted()) { setCountLastAlerts(count); const core = getCore(); @@ -74,27 +73,28 @@ export function LastAlertsStat({ severity }: { severity: string }) { basePath: 'discover', }; - // TODO: find a better way to get the query discover URL - const destURL = core.application.getUrlForApp(discoverLocation.app, { - path: `${ - discoverLocation.basePath - }#?_a=(discover:(columns:!(_source),isDirty:!f,sort:!()),metadata:(indexPattern:'${indexPatternName}',view:discover))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'${indexPatternName}',key:${ - cluster.field - },negate:!f,params:(query:${ - cluster.name - }),type:phrase),query:(match_phrase:(${cluster.field}:${ - cluster.name - }))),('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'${indexPatternName}',key:rule.level,negate:!f,params:(gte:${ - severityLabel[severity].ruleLevelRange.minRuleLevel - },lte:${ - severityLabel[severity].ruleLevelRange.maxRuleLevel || '!n' - }),type:range),range:(rule.level:(gte:${ - severityLabel[severity].ruleLevelRange.minRuleLevel - },lte:${ - severityLabel[severity].ruleLevelRange.maxRuleLevel || '!n' - })))),refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_q=(filters:!(),query:(language:kuery,query:''))`, - }); - setDiscoverLocation(destURL); + // TODO: find a better way to get the query discover URL + const destURL = core.application.getUrlForApp(discoverLocation.app, { + path: `${ + discoverLocation.basePath + }#?_a=(discover:(columns:!(_source),isDirty:!f,sort:!()),metadata:(indexPattern:'${indexPatternName}',view:discover))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'${indexPatternName}',key:${ + cluster.field + },negate:!f,params:(query:${ + cluster.name + }),type:phrase),query:(match_phrase:(${cluster.field}:${ + cluster.name + }))),('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'${indexPatternName}',key:rule.level,negate:!f,params:(gte:${ + severityLabel[severity].ruleLevelRange.minRuleLevel + },lte:${ + severityLabel[severity].ruleLevelRange.maxRuleLevel || '!n' + }),type:range),range:(rule.level:(gte:${ + severityLabel[severity].ruleLevelRange.minRuleLevel + },lte:${ + severityLabel[severity].ruleLevelRange.maxRuleLevel || '!n' + })))),refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_q=(filters:!(),query:(language:kuery,query:''))`, + }); + setDiscoverLocation(destURL); + } } catch (error) { if (error.name !== 'AbortError') { const searchError = ErrorFactory.create(HttpError, { @@ -107,12 +107,7 @@ export function LastAlertsStat({ severity }: { severity: string }) { }; getCountLastAlerts(); - - return () => { - isMounted.current = false; - controller.abort(); - }; - }, [severity]); + }, [isMounted]); return ( From 5d284969e887b917a719187fd2de5451017b30e2 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Wed, 5 Jun 2024 14:41:35 -0300 Subject: [PATCH 10/12] Update hook useIsMounted --- .../components/common/hooks/use-is-mounted.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/plugins/main/public/components/common/hooks/use-is-mounted.ts b/plugins/main/public/components/common/hooks/use-is-mounted.ts index 51243a5ba2..4931b31754 100644 --- a/plugins/main/public/components/common/hooks/use-is-mounted.ts +++ b/plugins/main/public/components/common/hooks/use-is-mounted.ts @@ -1,15 +1,26 @@ import { useCallback, useEffect, useRef } from 'react'; -export function useIsMounted(): () => boolean { +export const useIsMounted = () => { const isMounted = useRef(false); + const abortControllerRef = useRef(new AbortController()); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; + abortControllerRef.current.abort(); }; }, []); - return useCallback(() => isMounted.current, []); -} + const getAbortController = useCallback(() => { + if (!isMounted.current) { + abortControllerRef.current = new AbortController(); + } + return abortControllerRef.current; + }, []); + + const isComponentMounted = useCallback(() => isMounted.current, []); + + return { isComponentMounted, getAbortController }; +}; From 57cee284ace7bc6a04330e8c090ef1b05cd051e8 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Wed, 5 Jun 2024 15:00:57 -0300 Subject: [PATCH 11/12] Update unit test of hook useIsMounted --- .../common/hooks/use-is-mounted.test.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/plugins/main/public/components/common/hooks/use-is-mounted.test.tsx b/plugins/main/public/components/common/hooks/use-is-mounted.test.tsx index 4443dd6c38..a2cbae8827 100644 --- a/plugins/main/public/components/common/hooks/use-is-mounted.test.tsx +++ b/plugins/main/public/components/common/hooks/use-is-mounted.test.tsx @@ -1,24 +1,19 @@ -import { renderHook } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; import { useIsMounted } from './use-is-mounted'; describe('useIsMounted()', () => { it('should return true when component is mounted', () => { - const { - result: { current: isMounted }, - } = renderHook(() => useIsMounted()); + const { result } = renderHook(() => useIsMounted()); - expect(isMounted()).toBe(true); + expect(result.current.isComponentMounted()).toBe(true); }); it('should return false when component is unmounted', () => { - const { - result: { current: isMounted }, - unmount, - } = renderHook(() => useIsMounted()); + const { result, unmount } = renderHook(() => useIsMounted()); unmount(); - expect(isMounted()).toBe(false); + expect(result.current.isComponentMounted()).toBe(false); }); }); From e7311c12d5994e57d44776d9149c7f3778afd494 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Thu, 6 Jun 2024 12:21:13 -0300 Subject: [PATCH 12/12] Troubleshoot status update issues on disassembled components in various application files --- .../components/agents/stats/agent-stats.tsx | 28 ++-- .../data-source/hooks/use-data-source.ts | 13 +- .../common/tables/table-default.tsx | 68 ++++---- .../common/tables/table-with-search-bar.tsx | 38 ++--- .../components/common/tables/table-wz-api.tsx | 28 +--- .../components/techniques/techniques.tsx | 158 +++++++++++------- .../last-alerts-stat/last-alerts-stat.tsx | 17 +- 7 files changed, 188 insertions(+), 162 deletions(-) diff --git a/plugins/main/public/components/agents/stats/agent-stats.tsx b/plugins/main/public/components/agents/stats/agent-stats.tsx index 63c49a9eec..268a08a723 100644 --- a/plugins/main/public/components/agents/stats/agent-stats.tsx +++ b/plugins/main/public/components/agents/stats/agent-stats.tsx @@ -9,7 +9,7 @@ * * Find more information about this on the LICENSE file. */ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -48,6 +48,7 @@ import { import { getErrorOrchestrator } from '../../../react-services/common-services'; import { endpointSummary } from '../../../utils/applications'; import { getCore } from '../../../kibana-services'; +import { useIsMounted } from '../../common/hooks/use-is-mounted'; const tableColumns = [ { @@ -145,29 +146,30 @@ export const MainAgentStats = compose( )(AgentStats); function AgentStats({ agent }) { - const [loading, setLoading] = useState(); + const [loading, setLoading] = useState(false); const [dataStatLogcollector, setDataStatLogcollector] = useState({}); const [dataStatAgent, setDataStatAgent] = useState(); - const isMounted = useRef(false); - useEffect(() => { - isMounted.current = true; + const { isComponentMounted, getAbortController } = useIsMounted(); + useEffect(() => { const fetchData = async () => { setLoading(true); try { + const signal = getAbortController().signal; + const responseDataStatLogcollector = await WzRequest.apiReq( 'GET', `/agents/${agent.id}/stats/logcollector`, - {}, + { signal }, ); const responseDataStatAgent = await WzRequest.apiReq( 'GET', `/agents/${agent.id}/stats/agent`, - {}, + { signal }, ); - if (isMounted.current) { + if (isComponentMounted()) { setDataStatLogcollector( responseDataStatLogcollector?.data?.data?.affected_items?.[0] || {}, ); @@ -176,8 +178,8 @@ function AgentStats({ agent }) { ); } } catch (error) { - if (isMounted.current) { - const options: UIErrorLog = { + if (isComponentMounted()) { + const options = { context: `${AgentStats.name}.useEffect`, level: UI_LOGGER_LEVELS.ERROR as UILogLevel, severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, @@ -190,17 +192,13 @@ function AgentStats({ agent }) { getErrorOrchestrator().handleError(options); } } finally { - if (isMounted.current) { + if (isComponentMounted()) { setLoading(false); } } }; fetchData(); - - return () => { - isMounted.current = false; - }; }, [agent.id]); return ( diff --git a/plugins/main/public/components/common/data-source/hooks/use-data-source.ts b/plugins/main/public/components/common/data-source/hooks/use-data-source.ts index 3f2ff0e80e..614f93e95d 100644 --- a/plugins/main/public/components/common/data-source/hooks/use-data-source.ts +++ b/plugins/main/public/components/common/data-source/hooks/use-data-source.ts @@ -12,6 +12,7 @@ import { tFilterManager, } from '../index'; import { PinnedAgentManager } from '../../../wz-agent-selector/wz-agent-selector-service'; +import { useIsMounted } from '../../../common/hooks/use-is-mounted'; type tUseDataSourceProps = { DataSource: IDataSourceFactoryConstructor; @@ -70,6 +71,8 @@ export function useDataSource< const pinnedAgentManager = new PinnedAgentManager(); const pinnedAgent = pinnedAgentManager.getPinnedAgent(); + const { isComponentMounted, getAbortController } = useIsMounted(); + const setFilters = (filters: tFilter[]) => { if (!dataSourceFilterManager) { return; @@ -83,11 +86,11 @@ export function useDataSource< if (!dataSourceFilterManager) { return; } - return await dataSourceFilterManager?.fetch(params); + const paramsWithSignal = { ...params, signal: getAbortController().signal }; + return await dataSourceFilterManager.fetch(paramsWithSignal); }; useEffect(() => { - let isMounted = true; let subscription; (async () => { setIsLoading(true); @@ -102,7 +105,7 @@ export function useDataSource< if (!dataSource) { throw new Error('No valid data source found'); } - if (isMounted) { + if (isComponentMounted()) { setDataSource(dataSource); const dataSourceFilterManager = new PatternDataSourceFilterManager( dataSource, @@ -110,11 +113,9 @@ export function useDataSource< injectedFilterManager, initialFetchFilters, ); - // what the filters update subscription = dataSourceFilterManager.getUpdates$().subscribe({ next: () => { - // this is necessary to remove the hidden filters from the filter manager and not show them in the search bar - if (isMounted) { + if (isComponentMounted()) { dataSourceFilterManager.setFilters( dataSourceFilterManager.getFilters(), ); diff --git a/plugins/main/public/components/common/tables/table-default.tsx b/plugins/main/public/components/common/tables/table-default.tsx index ed068e3d23..9be741855a 100644 --- a/plugins/main/public/components/common/tables/table-default.tsx +++ b/plugins/main/public/components/common/tables/table-default.tsx @@ -15,6 +15,7 @@ import { EuiBasicTable } from '@elastic/eui'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { useIsMounted } from '../hooks/use-is-mounted'; export function TableDefault({ onSearch, @@ -43,7 +44,7 @@ export function TableDefault({ }, }); - const isMounted = useRef(false); + const { isComponentMounted, getAbortController } = useIsMounted(); function tableOnChange({ page = {}, sort = {} }) { const { index: pageIndex, size: pageSize } = page; @@ -61,58 +62,59 @@ export function TableDefault({ } useEffect(() => { - // This effect is triggered when the component is mounted because of how to the useEffect hook works. - // We don't want to set the pagination state because there is another effect that has this dependency - // and will cause the effect is triggered (redoing the onSearch function). - if (isMounted.current) { - // Reset the page index when the endpoint changes. - // This will cause that onSearch function is triggered because to changes in pagination in the another effect. + if (isComponentMounted()) { setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); } }, [endpoint]); useEffect(() => { (async function () { + const abortController = getAbortController(); try { setLoading(true); - const { items, totalItems } = await onSearch(endpoint, [], pagination, sorting); - setItems(items); - setTotalItems(totalItems); + const { items, totalItems } = await onSearch( + endpoint, + [], + pagination, + sorting, + ); + if (isComponentMounted()) { + setItems(items); + setTotalItems(totalItems); + } } catch (error) { - setItems([]); - setTotalItems(0); - const options = { - context: `${TableDefault.name}.useEffect`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error fetching items`, - }, - }; - getErrorOrchestrator().handleError(options); + if (isComponentMounted()) { + setItems([]); + setTotalItems(0); + const options = { + context: `${TableDefault.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error fetching items`, + }, + }; + getErrorOrchestrator().handleError(options); + } + } + if (isComponentMounted()) { + setLoading(false); } - setLoading(false); })(); }, [endpoint, pagination, sorting, reload]); - // It is required that this effect runs after other effects that use isMounted - // to avoid that these effects run when the component is mounted, only running - // when one of its dependencies changes. - useEffect(() => { - isMounted.current = true; - }, []); - const tablePagination = { ...pagination, totalItemCount: totalItems, pageSizeOptions: tablePageSizeOptions, - hidePerPageOptions + hidePerPageOptions, }; + return ( ({...rest}))} + columns={tableColumns.map(({ show, ...rest }) => ({ ...rest }))} items={items} loading={loading} pagination={tablePagination} diff --git a/plugins/main/public/components/common/tables/table-with-search-bar.tsx b/plugins/main/public/components/common/tables/table-with-search-bar.tsx index 1141faa0e1..75f4dd34fd 100644 --- a/plugins/main/public/components/common/tables/table-with-search-bar.tsx +++ b/plugins/main/public/components/common/tables/table-with-search-bar.tsx @@ -17,7 +17,7 @@ import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; import { SearchBar, SearchBarProps } from '../../search-bar'; - +import { useIsMounted } from '../hooks/use-is-mounted'; export interface ITableWithSearcHBarProps { /** * Function to fetch the data @@ -123,32 +123,17 @@ export function TableWithSearchBar({ }); const [refresh, setRefresh] = useState(Date.now()); - const isMounted = useRef(false); + const { isComponentMounted, getAbortController } = useIsMounted(); const tableRef = useRef(); useEffect(() => { - isMounted.current = true; - return () => { - isMounted.current = false; - }; - }, []); - - useEffect(() => { - // This effect is triggered when the component is mounted because of how to the useEffect hook works. - // We don't want to set the pagination state because there is another effect that has this dependency - // and will cause the effect is triggered (redoing the onSearch function). - if (isMounted.current) { - // Reset the page index when the endpoint or reload changes. - // This will cause that onSearch function is triggered because to changes in pagination in the another effect. + if (isComponentMounted()) { updateRefresh(); } }, [endpoint, reload]); useEffect(() => { - // This effect is triggered when the component is mounted because of how to the useEffect hook works. - // We don't want to set the filters state because there is another effect that has this dependency - // and will cause the effect is triggered (redoing the onSearch function). - if (isMounted.current && !_.isEqual(rest.filters, filters)) { + if (isComponentMounted() && !_.isEqual(rest.filters, filters)) { setFilters(rest.filters || {}); updateRefresh(); } @@ -174,7 +159,7 @@ export function TableWithSearchBar({ } function tableOnChange({ page = {}, sort = {} }) { - if (isMounted.current) { + if (isComponentMounted()) { const { index: pageIndex, size: pageSize } = page; const { field, direction } = sort; setPagination({ @@ -195,8 +180,10 @@ export function TableWithSearchBar({ try { setLoading(true); - //Reset the table selection in case is enabled - tableRef.current.setSelection([]); + // Reset the table selection in case is enabled + if (tableRef.current) { + tableRef.current.setSelection([]); + } const { items, totalItems } = await onSearch( endpoint, @@ -204,12 +191,12 @@ export function TableWithSearchBar({ pagination, sorting, ); - if (isMounted.current) { + if (isComponentMounted()) { setItems(items); setTotalItems(totalItems); } } catch (error) { - if (isMounted.current) { + if (isComponentMounted()) { setItems([]); setTotalItems(0); const options = { @@ -225,7 +212,7 @@ export function TableWithSearchBar({ getErrorOrchestrator().handleError(options); } } - if (isMounted.current) { + if (isComponentMounted()) { setLoading(false); } })(); @@ -236,6 +223,7 @@ export function TableWithSearchBar({ totalItemCount: totalItems, pageSizeOptions: tablePageSizeOptions, }; + return ( <> typeof rest.onFiltersChange === 'function' @@ -110,13 +104,11 @@ export function TableWzAPI({ ); const [isOpenFieldSelector, setIsOpenFieldSelector] = useState(false); - const controller = new AbortController(); - const signal = controller.signal; - const onSearch = useCallback( async function (endpoint, filters, pagination, sorting) { + const abortController = getAbortController(); try { - if (isMounted.current == false) { + if (!isComponentMounted()) { const error = new Error('Abort error onSearch callback'); error.name = 'AbortError'; throw error; @@ -131,7 +123,7 @@ export function TableWzAPI({ offset: pageIndex * pageSize, limit: pageSize, sort: `${direction === 'asc' ? '+' : '-'}${field}`, - signal, + signal: abortController.signal, }; const response = await WzRequest.apiReq('GET', endpoint, { params }); @@ -170,7 +162,7 @@ export function TableWzAPI({ }; } }, - [isMounted], + [isComponentMounted, getAbortController], ); const renderActionButtons = (actionButtons, filters) => { @@ -202,12 +194,10 @@ export function TableWzAPI({ }; useEffect(() => { - isMounted.current = true; - if (rest.reload && isMounted.current == true) triggerReload(); + if (rest.reload && isComponentMounted()) triggerReload(); return () => { - isMounted.current = false; - controller.abort(); + getAbortController().abort(); }; }, [rest.reload]); diff --git a/plugins/main/public/components/overview/mitre/framework/components/techniques/techniques.tsx b/plugins/main/public/components/overview/mitre/framework/components/techniques/techniques.tsx index 2533d1643d..4200b11620 100644 --- a/plugins/main/public/components/overview/mitre/framework/components/techniques/techniques.tsx +++ b/plugins/main/public/components/overview/mitre/framework/components/techniques/techniques.tsx @@ -40,6 +40,7 @@ import { getErrorOrchestrator } from '../../../../../../react-services/common-se import { tFilter, tSearchParams } from '../../../../../common/data-source'; import { tFilterParams } from '../../mitre'; import { getDataPlugin } from '../../../../../../kibana-services'; +import { useIsMounted } from '../../../../../common/hooks/use-is-mounted'; const MITRE_ATTACK = 'mitre-attack'; @@ -90,6 +91,7 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { const [loadingAlerts, setLoadingAlerts] = useState(false); const { isFlyoutVisible, techniquesCount, currentTechnique } = state; + const { isComponentMounted, getAbortController } = useIsMounted(); const getMitreRuleIdFilter = (value: string) => { const GROUP_KEY = 'rule.mitre.id'; @@ -130,12 +132,12 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { return; } buildMitreTechniquesFromApi(); - }, [isLoading]); + }, [isLoading, isComponentMounted]); useEffect(() => { - if (isLoading || isSearching) return; + if (isLoading || isSearching || !isComponentMounted()) return; getTechniquesCount(); - }, [tacticsObject, isLoading, filterParams, isSearching]); + }, [tacticsObject, isLoading, filterParams, isSearching, isComponentMounted]); const getTechniquesCount = async () => { try { @@ -183,7 +185,7 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { } }; - const buildPanel = (techniqueID) => { + const buildPanel = techniqueID => { return [ { id: 0, @@ -191,21 +193,21 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { items: [ { name: 'Filter for value', - icon: , + icon: , onClick: () => { closeActionsMenu(); }, }, { name: 'Filter out value', - icon: , + icon: , onClick: () => { closeActionsMenu(); }, }, { name: 'View technique details', - icon: , + icon: , onClick: () => { closeActionsMenu(); showFlyout(techniqueID); @@ -218,13 +220,17 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { const techniqueColumnsResponsive = () => { if (props && props?.windowSize) { - return props.windowSize.width < 930 ? 2 : props.windowSize.width < 1200 ? 3 : 4; + return props.windowSize.width < 930 + ? 2 + : props.windowSize.width < 1200 + ? 3 + : 4; } else { return 4; } }; - const getMitreTechniques = async (params) => { + const getMitreTechniques = async params => { try { return await WzRequest.apiReq('GET', '/mitre/techniques', { params }); } catch (error) { @@ -250,10 +256,16 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { const params = { limit: limitResults }; setIsSearching(true); const output = await getMitreTechniques(params); - const totalItems = (((output || {}).data || {}).data || {}).total_affected_items; + const totalItems = (((output || {}).data || {}).data || {}) + .total_affected_items; let mitreTechniques = []; mitreTechniques.push(...output.data.data.affected_items); - if (totalItems && output.data && output.data.data && totalItems > limitResults) { + if ( + totalItems && + output.data && + output.data.data && + totalItems > limitResults + ) { const extraResults = await Promise.all( Array(Math.ceil((totalItems - params.limit) / params.limit)) .fill() @@ -263,7 +275,7 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { offset: limitResults * (1 + index), }); return response.data.data.affected_items; - }) + }), ); mitreTechniques.push(...extraResults.flat()); } @@ -271,16 +283,17 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { setIsSearching(false); }; - const buildObjTechniques = (techniques) => { + const buildObjTechniques = techniques => { const techniquesObj = []; - techniques.forEach((element) => { - const mitreObj = state.mitreTechniques.find((item) => item.id === element); + techniques.forEach(element => { + const mitreObj = state.mitreTechniques.find(item => item.id === element); if (mitreObj) { const mitreTechniqueName = mitreObj.name; const mitreTechniqueID = mitreObj.source === MITRE_ATTACK ? mitreObj.external_id - : mitreObj.references.find((item) => item.source === MITRE_ATTACK).external_id; + : mitreObj.references.find(item => item.source === MITRE_ATTACK) + .external_id; mitreTechniqueID ? techniquesObj.push({ id: mitreTechniqueID, @@ -292,7 +305,7 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { return techniquesObj; }; - const addFilter = (filter) => { + const addFilter = filter => { const { filterManager } = getDataPlugin().query; const matchPhrase = {}; matchPhrase[filter.key] = filter.value; @@ -333,37 +346,43 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { let hash = {}; let tacticsToRender: Array = []; const currentTechniques = Object.keys(tacticsObject) - .map((tacticsKey) => ({ + .map(tacticsKey => ({ tactic: tacticsKey, techniques: buildObjTechniques(tacticsObject[tacticsKey].techniques), })) - .filter((tactic) => selectedTactics[tactic.tactic]) - .map((tactic) => tactic.techniques) + .filter(tactic => selectedTactics[tactic.tactic]) + .map(tactic => tactic.techniques) .flat() - .filter((techniqueID, index, array) => array.indexOf(techniqueID) === index); + .filter( + (techniqueID, index, array) => array.indexOf(techniqueID) === index, + ); tacticsToRender = currentTechniques - .filter((technique) => + .filter(technique => state.filteredTechniques ? state.filteredTechniques.includes(technique.id) : technique.id && hash[technique.id] ? false - : (hash[technique.id] = true) + : (hash[technique.id] = true), ) - .map((technique) => { + .map(technique => { return { id: technique.id, label: `${technique.id} - ${technique.name}`, quantity: - (techniquesCount.find((item) => item.key === technique.id) || {}).doc_count || 0, + (techniquesCount.find(item => item.key === technique.id) || {}) + .doc_count || 0, }; }) - .filter((technique) => (state.hideAlerts ? technique.quantity !== 0 : true)); + .filter(technique => + state.hideAlerts ? technique.quantity !== 0 : true, + ); const tacticsToRenderOrdered = tacticsToRender .sort((a, b) => b.quantity - a.quantity) .map((item, idx) => { const tooltipContent = `View details of ${item.label} (${item.id})`; const toolTipAnchorClass = - 'wz-display-inline-grid' + (state.hover === item.id ? ' wz-mitre-width' : ' '); + 'wz-display-inline-grid' + + (state.hover === item.id ? ' wz-mitre-width' : ' '); return ( setState({ ...state, hover: item.id })} @@ -376,8 +395,8 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { }} > { onClick={() => showFlyout(item.id)} > @@ -409,25 +428,31 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { {state.hover === item.id && ( - + { + onClick={e => { openDashboard(e, item.id); e.stopPropagation(); }} - color="primary" - type="visualizeApp" + color='primary' + type='visualizeApp' > {' '}   - + { + onClick={e => { openDiscover(e, item.id); e.stopPropagation(); }} - color="primary" - type="discoverApp" + color='primary' + type='discoverApp' > @@ -436,9 +461,9 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { } isOpen={state.actionsOpen === item.id} closePopover={() => closeActionsMenu()} - panelPaddingSize="none" + panelPaddingSize='none' style={{ width: '100%' }} - anchorPosition="downLeft" + anchorPosition='downLeft' > @@ -447,8 +472,10 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { }); if (isSearching || loadingAlerts || isLoading) { return ( - - + + ); } @@ -456,7 +483,7 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { return ( { ); } else { return ( - + ); } }; - const onChange = (searchValue) => { + const onChange = searchValue => { if (!searchValue) { setState({ ...state, filteredTechniques: false }); setIsSearching(false); } }; - const onSearch = async (searchValue) => { + const onSearch = async searchValue => { try { if (searchValue) { setIsSearching(true); @@ -490,8 +521,12 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { search: searchValue, }, }); - const filteredTechniques = (((response || {}).data || {}).data.affected_items || []).map( - (item) => [item].filter((reference) => reference.source === MITRE_ATTACK)[0].external_id + const filteredTechniques = ( + ((response || {}).data || {}).data.affected_items || [] + ).map( + item => + [item].filter(reference => reference.source === MITRE_ATTACK)[0] + .external_id, ); setState({ ...state, @@ -525,7 +560,7 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { setState({ ...state, actionsOpen: false }); }; - const showFlyout = (techniqueData) => { + const showFlyout = techniqueData => { setState({ ...state, isFlyoutVisible: true, @@ -545,7 +580,7 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => {
- +

Techniques

@@ -555,23 +590,27 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { Hide techniques with no alerts   - hideAlerts()} /> + hideAlerts()} + />
- + - +
{renderFacet()}
@@ -580,12 +619,15 @@ export const Techniques = withWindowSize((props: tTechniquesProps) => { onChangeFlyout={onChangeFlyout} currentTechnique={currentTechnique} filterParams={{ - filters: [...filterParams.filters, ...getMitreRuleIdFilter(currentTechnique)], // the flyout must receive the filters from the mitre global search bar + filters: [ + ...filterParams.filters, + ...getMitreRuleIdFilter(currentTechnique), + ], // the flyout must receive the filters from the mitre global search bar query: filterParams.query, time: filterParams.time, }} - openDashboard={(e) => openDashboard(e, currentTechnique)} - openDiscover={(e) => openDiscover(e, currentTechnique)} + openDashboard={e => openDashboard(e, currentTechnique)} + openDiscover={e => openDiscover(e, currentTechnique)} /> )}
diff --git a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx index 9fb6202fef..4c578586e5 100644 --- a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx +++ b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiStat, EuiFlexItem, @@ -20,7 +20,9 @@ import { useIsMounted } from '../../../../components/common/hooks/use-is-mounted export function LastAlertsStat({ severity }: { severity: string }) { const [countLastAlerts, setCountLastAlerts] = useState(null); const [discoverLocation, setDiscoverLocation] = useState(''); - const isMounted = useIsMounted(); + + const { isComponentMounted, getAbortController } = useIsMounted(); + const severityLabel = { low: { label: 'Low', @@ -56,15 +58,13 @@ export function LastAlertsStat({ severity }: { severity: string }) { }; useEffect(() => { - const controller = new AbortController(); - const signal = controller.signal; const getCountLastAlerts = async () => { try { const { indexPatternName, cluster, count } = await getLast24HoursAlerts( severityLabel[severity].ruleLevelRange, { signal }, ); - if (isMounted()) { + if (isComponentMounted()) { setCountLastAlerts(count); const core = getCore(); @@ -107,7 +107,12 @@ export function LastAlertsStat({ severity }: { severity: string }) { }; getCountLastAlerts(); - }, [isMounted]); + + return () => { + const abortController = getAbortController(); + abortController.abort(); + }; + }, [severity, isComponentMounted, getAbortController]); return (