mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Infra] Fix duplicate requests (#189505)
fixes: [#189484](https://github.com/elastic/kibana/issues/189484) ## Summary Fix duplicate request during hosts view mounting cycle. https://github.com/user-attachments/assets/dce73b2a-77e8-461a-bd9e-661fc40afe7a https://github.com/user-attachments/assets/0e8c41b4-1a36-470f-855d-c189221ad1f6 The problem happened because the `buildEsQuery` returns a complex object, making the `useFetcher` to treat every payload, as a new one, triggering duplicate requests. ### Extra I have refactored the `use_unified_search` and `use_unified_search_url_state`. It was misusing `useReducer` and there was some magic happening with `rxJS`. I tried to make the code easier to understand ### How to test - Start a local Kibana and es instances - run `node scripts/synthtrace infra_hosts_with_apm_hosts --live ` - Navigate to Infrastructure > Hosts - Open the dev tools and check if there is more than one call to `api/metrics/infra` endpoint --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
4a63054366
commit
b009ea0011
6 changed files with 117 additions and 84 deletions
|
@ -16,7 +16,6 @@ import { AlertsCount } from '../../../hooks/use_alerts_count';
|
|||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
||||
import { createAlertsEsQuery } from '../../../utils/filters/create_alerts_es_query';
|
||||
import { ALERT_STATUS_ALL, infraAlertFeatureIds } from './constants';
|
||||
import { HostsStateUpdater } from '../../../pages/metrics/hosts/hooks/use_unified_search_url_state';
|
||||
import AlertsStatusFilter from './alerts_status_filter';
|
||||
import { useAssetDetailsUrlState } from '../../asset_details/hooks/use_asset_details_url_state';
|
||||
|
||||
|
@ -24,7 +23,7 @@ interface AlertsOverviewProps {
|
|||
assetId: string;
|
||||
dateRange: TimeRange;
|
||||
onLoaded: (alertsCount?: AlertsCount) => void;
|
||||
onRangeSelection?: HostsStateUpdater;
|
||||
onRangeSelection?: (dateRange: TimeRange) => void;
|
||||
assetType?: InventoryItemType;
|
||||
}
|
||||
|
||||
|
@ -86,7 +85,7 @@ export const AlertsOverview = ({
|
|||
const from = new Date(start).toISOString();
|
||||
const to = new Date(end).toISOString();
|
||||
|
||||
onRangeSelection({ dateRange: { from, to } });
|
||||
onRangeSelection({ from, to });
|
||||
}
|
||||
},
|
||||
[onRangeSelection]
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import type { Query, TimeRange, Filter } from '@kbn/es-query';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useEuiTheme, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
@ -22,28 +22,21 @@ export const UnifiedSearchBar = () => {
|
|||
services: { unifiedSearch, application },
|
||||
} = useKibanaContextForPlugin();
|
||||
const { metricsView } = useMetricsDataViewContext();
|
||||
const { searchCriteria, onSubmit } = useUnifiedSearchContext();
|
||||
const { searchCriteria, onLimitChange, onPanelFiltersChange, onSubmit } =
|
||||
useUnifiedSearchContext();
|
||||
|
||||
const { SearchBar } = unifiedSearch.ui;
|
||||
|
||||
const onLimitChange = (limit: number) => {
|
||||
onSubmit({ limit });
|
||||
};
|
||||
|
||||
const onPanelFiltersChange = useCallback(
|
||||
(panelFilters: Filter[]) => {
|
||||
onSubmit({ panelFilters });
|
||||
const handleRefresh = useCallback(
|
||||
(payload: { dateRange: TimeRange }, isUpdate?: boolean) => {
|
||||
// This makes sure `onSubmit` is only called when the submit button is clicked
|
||||
if (isUpdate === false) {
|
||||
onSubmit(payload);
|
||||
}
|
||||
},
|
||||
[onSubmit]
|
||||
);
|
||||
|
||||
const handleRefresh = (payload: { query?: Query; dateRange: TimeRange }, isUpdate?: boolean) => {
|
||||
// This makes sure `onQueryChange` is only called when the submit button is clicked
|
||||
if (isUpdate === false) {
|
||||
onSubmit(payload);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StickyContainer>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
|
|
|
@ -10,11 +10,12 @@ import { AlertConsumers, ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils';
|
|||
import { BrushEndListener, type XYBrushEvent } from '@elastic/charts';
|
||||
import { useSummaryTimeRange } from '@kbn/observability-plugin/public';
|
||||
import { useBoolean } from '@kbn/react-hooks';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
|
||||
import { HeightRetainer } from '../../../../../../components/height_retainer';
|
||||
import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
|
||||
import { useAlertsQuery } from '../../../hooks/use_alerts_query';
|
||||
import { HostsState, HostsStateUpdater } from '../../../hooks/use_unified_search_url_state';
|
||||
import type { HostsState } from '../../../hooks/use_unified_search_url_state';
|
||||
import { AlertsEsQuery } from '../../../../../../utils/filters/create_alerts_es_query';
|
||||
import {
|
||||
ALERTS_PER_PAGE,
|
||||
|
@ -35,7 +36,7 @@ export const AlertsTabContent = () => {
|
|||
const { alertStatus, setAlertStatus, alertsEsQueryByStatus } = useAlertsQuery();
|
||||
const [isAlertFlyoutVisible, { toggle: toggleAlertFlyout }] = useBoolean(false);
|
||||
|
||||
const { onSubmit, searchCriteria } = useUnifiedSearchContext();
|
||||
const { onDateRangeChange, searchCriteria } = useUnifiedSearchContext();
|
||||
|
||||
const { triggersActionsUi } = services;
|
||||
|
||||
|
@ -71,7 +72,7 @@ export const AlertsTabContent = () => {
|
|||
<MemoAlertSummaryWidget
|
||||
alertsQuery={alertsEsQueryByStatus}
|
||||
dateRange={searchCriteria.dateRange}
|
||||
onRangeSelection={onSubmit}
|
||||
onRangeSelection={onDateRangeChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{alertsEsQueryByStatus && (
|
||||
|
@ -102,7 +103,7 @@ export const AlertsTabContent = () => {
|
|||
interface MemoAlertSummaryWidgetProps {
|
||||
alertsQuery: AlertsEsQuery;
|
||||
dateRange: HostsState['dateRange'];
|
||||
onRangeSelection: HostsStateUpdater;
|
||||
onRangeSelection: (dateRange: TimeRange) => void;
|
||||
}
|
||||
|
||||
const MemoAlertSummaryWidget = React.memo(
|
||||
|
@ -122,7 +123,7 @@ const MemoAlertSummaryWidget = React.memo(
|
|||
const from = new Date(start).toISOString();
|
||||
const to = new Date(end).toISOString();
|
||||
|
||||
onRangeSelection({ dateRange: { from, to } });
|
||||
onRangeSelection({ from, to });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -43,13 +43,15 @@ export const useHostsView = () => {
|
|||
} = useKibanaContextForPlugin();
|
||||
const { buildQuery, parsedDateRange, searchCriteria } = useUnifiedSearchContext();
|
||||
|
||||
const baseRequest = useMemo(
|
||||
const payload = useMemo(
|
||||
() =>
|
||||
createInfraMetricsRequest({
|
||||
dateRange: parsedDateRange,
|
||||
esQuery: buildQuery(),
|
||||
limit: searchCriteria.limit,
|
||||
}),
|
||||
JSON.stringify(
|
||||
createInfraMetricsRequest({
|
||||
dateRange: parsedDateRange,
|
||||
esQuery: buildQuery(),
|
||||
limit: searchCriteria.limit,
|
||||
})
|
||||
),
|
||||
[buildQuery, parsedDateRange, searchCriteria.limit]
|
||||
);
|
||||
|
||||
|
@ -60,7 +62,7 @@ export const useHostsView = () => {
|
|||
BASE_INFRA_METRICS_PATH,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(baseRequest),
|
||||
body: payload,
|
||||
}
|
||||
);
|
||||
const duration = performance.now() - start;
|
||||
|
@ -72,7 +74,7 @@ export const useHostsView = () => {
|
|||
);
|
||||
return metricsResponse;
|
||||
},
|
||||
[baseRequest, searchCriteria.limit, telemetry]
|
||||
[payload, searchCriteria.limit, telemetry]
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
*/
|
||||
import createContainer from 'constate';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { buildEsQuery, fromKueryExpression, type Query } from '@kbn/es-query';
|
||||
import { map, skip, startWith } from 'rxjs';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { buildEsQuery, Filter, fromKueryExpression, TimeRange, type Query } from '@kbn/es-query';
|
||||
import { Subscription, map, tap } from 'rxjs';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||
import { useSearchSessionContext } from '../../../../hooks/use_search_session';
|
||||
|
@ -18,10 +17,10 @@ import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
|
|||
import { telemetryTimeRangeFormatter } from '../../../../../common/formatters/telemetry_time_range';
|
||||
import { useMetricsDataViewContext } from '../../../../containers/metrics_source';
|
||||
import {
|
||||
HostsSearchPayload,
|
||||
useHostsUrlState,
|
||||
type HostsState,
|
||||
type StringDateRangeTimestamp,
|
||||
StringDateRange,
|
||||
} from './use_unified_search_url_state';
|
||||
import { retrieveFieldsFromFilter } from '../../../../utils/filters/build';
|
||||
|
||||
|
@ -60,11 +59,7 @@ export const useUnifiedSearch = () => {
|
|||
|
||||
const {
|
||||
data: {
|
||||
query: {
|
||||
filterManager: filterManagerService,
|
||||
queryString: queryStringService,
|
||||
timefilter: timeFilterService,
|
||||
},
|
||||
query: { filterManager: filterManagerService, queryString: queryStringService },
|
||||
},
|
||||
telemetry,
|
||||
} = services;
|
||||
|
@ -76,30 +71,53 @@ export const useUnifiedSearch = () => {
|
|||
[kibanaQuerySettings]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(params?: HostsSearchPayload) => {
|
||||
const onFiltersChange = useCallback(
|
||||
(filters: Filter[]) => {
|
||||
setSearch({ type: 'SET_FILTERS', filters });
|
||||
},
|
||||
[setSearch]
|
||||
);
|
||||
|
||||
const onPanelFiltersChange = useCallback(
|
||||
(panelFilters: Filter[]) => {
|
||||
setSearch({ type: 'SET_PANEL_FILTERS', panelFilters });
|
||||
},
|
||||
[setSearch]
|
||||
);
|
||||
|
||||
const onLimitChange = useCallback(
|
||||
(limit: number) => {
|
||||
setSearch({ type: 'SET_LIMIT', limit });
|
||||
},
|
||||
[setSearch]
|
||||
);
|
||||
|
||||
const onDateRangeChange = useCallback(
|
||||
(dateRange: StringDateRange) => {
|
||||
setSearch({ type: 'SET_DATE_RANGE', dateRange });
|
||||
},
|
||||
[setSearch]
|
||||
);
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
(query: Query) => {
|
||||
try {
|
||||
setError(null);
|
||||
/*
|
||||
/ Validates the Search Bar input values before persisting them in the state.
|
||||
/ Since the search can be triggered by components that are unaware of the Unified Search state (e.g Controls and Host Limit),
|
||||
/ this will always validates the query bar value, regardless of whether it's been sent in the current event or not.
|
||||
*/
|
||||
validateQuery(params?.query ?? (queryStringService.getQuery() as Query));
|
||||
setSearch(params ?? {});
|
||||
updateSearchSessionId();
|
||||
validateQuery(query);
|
||||
setSearch({ type: 'SET_QUERY', query });
|
||||
} catch (err) {
|
||||
/*
|
||||
/ Persists in the state the params so they can be used in case the query bar is fixed by the user.
|
||||
/ This is needed because the Unified Search observables are unnaware of the other componets in the search bar.
|
||||
/ Invalid query isn't persisted because it breaks the Control component
|
||||
*/
|
||||
const { query, ...validParams } = params ?? {};
|
||||
setSearch(validParams ?? {});
|
||||
setError(err);
|
||||
}
|
||||
},
|
||||
[queryStringService, setSearch, updateSearchSessionId, validateQuery]
|
||||
[validateQuery, setSearch]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
({ dateRange }: { dateRange: TimeRange }) => {
|
||||
onDateRangeChange(dateRange);
|
||||
updateSearchSessionId();
|
||||
},
|
||||
[onDateRangeChange, updateSearchSessionId]
|
||||
);
|
||||
|
||||
const parsedDateRange = useMemo(() => {
|
||||
|
@ -153,27 +171,31 @@ export const useUnifiedSearch = () => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
const filters$ = filterManagerService.getUpdates$().pipe(
|
||||
startWith(undefined),
|
||||
map(() => filterManagerService.getFilters())
|
||||
const subscription = new Subscription();
|
||||
subscription.add(
|
||||
filterManagerService
|
||||
.getUpdates$()
|
||||
.pipe(
|
||||
map(() => filterManagerService.getFilters()),
|
||||
tap((filters) => onFiltersChange(filters))
|
||||
)
|
||||
.subscribe()
|
||||
);
|
||||
|
||||
const query$ = queryStringService.getUpdates$().pipe(
|
||||
startWith(undefined),
|
||||
map(() => queryStringService.getQuery() as Query)
|
||||
subscription.add(
|
||||
queryStringService
|
||||
.getUpdates$()
|
||||
.pipe(
|
||||
map(() => queryStringService.getQuery() as Query),
|
||||
tap((query) => onQueryChange(query))
|
||||
)
|
||||
.subscribe()
|
||||
);
|
||||
|
||||
const subscription = combineLatest({
|
||||
filters: filters$,
|
||||
query: query$,
|
||||
})
|
||||
.pipe(skip(1))
|
||||
.subscribe(onSubmit);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [filterManagerService, onSubmit, queryStringService, timeFilterService.timefilter]);
|
||||
}, [filterManagerService, queryStringService, onQueryChange, onFiltersChange]);
|
||||
|
||||
// Track telemetry event on query/filter/date changes
|
||||
useEffect(() => {
|
||||
|
@ -190,6 +212,9 @@ export const useUnifiedSearch = () => {
|
|||
parsedDateRange,
|
||||
getDateRangeAsTimestamp,
|
||||
searchCriteria,
|
||||
onDateRangeChange,
|
||||
onLimitChange,
|
||||
onPanelFiltersChange,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useReducer } from 'react';
|
||||
import { Dispatch, useReducer } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import * as rt from 'io-ts';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
@ -37,16 +37,31 @@ const INITIAL_HOSTS_STATE: HostsState = {
|
|||
limit: DEFAULT_HOST_LIMIT,
|
||||
};
|
||||
|
||||
const reducer = (prevState: HostsState, params: HostsSearchPayload) => {
|
||||
const payload = Object.fromEntries(Object.entries(params).filter(([_, v]) => !!v));
|
||||
export type HostsStateAction =
|
||||
| { type: 'SET_DATE_RANGE'; dateRange: StringDateRange }
|
||||
| { type: 'SET_LIMIT'; limit: number }
|
||||
| { type: 'SET_FILTERS'; filters: HostsState['filters'] }
|
||||
| { type: 'SET_QUERY'; query: HostsState['query'] }
|
||||
| { type: 'SET_PANEL_FILTERS'; panelFilters: HostsState['panelFilters'] };
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
...payload,
|
||||
};
|
||||
const reducer = (state: HostsState, action: HostsStateAction): HostsState => {
|
||||
switch (action.type) {
|
||||
case 'SET_DATE_RANGE':
|
||||
return { ...state, dateRange: action.dateRange };
|
||||
case 'SET_LIMIT':
|
||||
return { ...state, limit: action.limit };
|
||||
case 'SET_FILTERS':
|
||||
return { ...state, filters: action.filters };
|
||||
case 'SET_QUERY':
|
||||
return { ...state, query: action.query };
|
||||
case 'SET_PANEL_FILTERS':
|
||||
return { ...state, panelFilters: action.panelFilters };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const useHostsUrlState = (): [HostsState, HostsStateUpdater] => {
|
||||
export const useHostsUrlState = (): [HostsState, Dispatch<HostsStateAction>] => {
|
||||
const [getTime] = useKibanaTimefilterTime(INITIAL_DATE_RANGE);
|
||||
const [localStorageHostLimit, setLocalStorageHostLimit] = useLocalStorage<number>(
|
||||
LOCAL_STORAGE_HOST_LIMIT_KEY,
|
||||
|
@ -74,7 +89,7 @@ export const useHostsUrlState = (): [HostsState, HostsStateUpdater] => {
|
|||
}
|
||||
|
||||
useSyncKibanaTimeFilterTime(INITIAL_DATE_RANGE, urlState.dateRange, (dateRange) =>
|
||||
setSearch({ dateRange })
|
||||
setSearch({ type: 'SET_DATE_RANGE', dateRange })
|
||||
);
|
||||
|
||||
return [search, setSearch];
|
||||
|
@ -131,8 +146,6 @@ export type HostsState = rt.TypeOf<typeof HostsStateRT>;
|
|||
|
||||
export type HostsSearchPayload = Partial<HostsState>;
|
||||
|
||||
export type HostsStateUpdater = (params: HostsSearchPayload) => void;
|
||||
|
||||
export type StringDateRange = rt.TypeOf<typeof StringDateRangeRT>;
|
||||
export interface StringDateRangeTimestamp {
|
||||
from: number;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue