[8.7] [Infrastructure UI] Add abort controller to Snapshot API call (#152819) (#152999)

# Backport

This will backport the following commits from `main` to `8.7`:
- [[Infrastructure UI] Add abort controller to Snapshot API call
(#152819)](https://github.com/elastic/kibana/pull/152819)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Carlos
Crespo","email":"crespocarlos@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-03-09T09:59:12Z","message":"[Infrastructure
UI] Add abort controller to Snapshot API call (#152819)\n\n##
Summary\r\n\r\nCloses
[152896](https://github.com/elastic/kibana/issues/152896)\r\n\r\nThis PR
adds AbortController to Snapshot API within the Hosts View\r\ncontext,
to cancel pending requests before making new
ones.\r\n\r\n\r\n223694389-e5473956-a290-447c-9561-b5d1b2f1ad1c.mov\r\n\r\n\r\n<br
/>\r\n\r\n**Current
behaviour**\r\n\r\n\r\nhttps://user-images.githubusercontent.com/2767137/223694452-ec9db680-cf49-4a27-bda9-db0fbadaef23.mov\r\n\r\nIt's
not part of this PR solving the problem of possible
sequential\r\nrequests caused by updates in the filter controls. It
works like this,\r\nbecause the filters show contextual options, so it
needs the unified\r\nsearch filters in order to fetch its
content.\r\n\r\n\r\n### How to test it\r\n- Rage click on the unified
search submit button\r\n- Rage click on the filter control options\r\n-
Navigate to a different page while the Hosts View is still waiting
for\r\nthe Snapshot API
response","sha":"75685cc4cea872286a93f253bb9fd5b8b61ed077","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Metrics
UI","Team:Infra Monitoring
UI","release_note:skip","backport:prev-minor","Feature:ObsHosts","v8.8.0"],"number":152819,"url":"https://github.com/elastic/kibana/pull/152819","mergeCommit":{"message":"[Infrastructure
UI] Add abort controller to Snapshot API call (#152819)\n\n##
Summary\r\n\r\nCloses
[152896](https://github.com/elastic/kibana/issues/152896)\r\n\r\nThis PR
adds AbortController to Snapshot API within the Hosts View\r\ncontext,
to cancel pending requests before making new
ones.\r\n\r\n\r\n223694389-e5473956-a290-447c-9561-b5d1b2f1ad1c.mov\r\n\r\n\r\n<br
/>\r\n\r\n**Current
behaviour**\r\n\r\n\r\nhttps://user-images.githubusercontent.com/2767137/223694452-ec9db680-cf49-4a27-bda9-db0fbadaef23.mov\r\n\r\nIt's
not part of this PR solving the problem of possible
sequential\r\nrequests caused by updates in the filter controls. It
works like this,\r\nbecause the filters show contextual options, so it
needs the unified\r\nsearch filters in order to fetch its
content.\r\n\r\n\r\n### How to test it\r\n- Rage click on the unified
search submit button\r\n- Rage click on the filter control options\r\n-
Navigate to a different page while the Hosts View is still waiting
for\r\nthe Snapshot API
response","sha":"75685cc4cea872286a93f253bb9fd5b8b61ed077"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/152819","number":152819,"mergeCommit":{"message":"[Infrastructure
UI] Add abort controller to Snapshot API call (#152819)\n\n##
Summary\r\n\r\nCloses
[152896](https://github.com/elastic/kibana/issues/152896)\r\n\r\nThis PR
adds AbortController to Snapshot API within the Hosts View\r\ncontext,
to cancel pending requests before making new
ones.\r\n\r\n\r\n223694389-e5473956-a290-447c-9561-b5d1b2f1ad1c.mov\r\n\r\n\r\n<br
/>\r\n\r\n**Current
behaviour**\r\n\r\n\r\nhttps://user-images.githubusercontent.com/2767137/223694452-ec9db680-cf49-4a27-bda9-db0fbadaef23.mov\r\n\r\nIt's
not part of this PR solving the problem of possible
sequential\r\nrequests caused by updates in the filter controls. It
works like this,\r\nbecause the filters show contextual options, so it
needs the unified\r\nsearch filters in order to fetch its
content.\r\n\r\n\r\n### How to test it\r\n- Rage click on the unified
search submit button\r\n- Rage click on the filter control options\r\n-
Navigate to a different page while the Hosts View is still waiting
for\r\nthe Snapshot API
response","sha":"75685cc4cea872286a93f253bb9fd5b8b61ed077"}}]}]
BACKPORT-->
This commit is contained in:
Carlos Crespo 2023-03-09 14:43:34 +01:00 committed by GitHub
parent c5b5c418ed
commit 943b5fceef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 122 additions and 98 deletions

View file

@ -5,11 +5,12 @@
* 2.0.
*/
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { HttpHandler } from '@kbn/core/public';
import { ToastInput } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { AbortError } from '@kbn/kibana-utils-plugin/common';
import { useTrackedPromise, CanceledPromiseError } from '../utils/use_tracked_promise';
import { InfraHttpError } from '../types';
@ -19,18 +20,20 @@ export function useHTTPRequest<Response>(
body?: string | null,
decode: (response: any) => Response = (response) => response,
fetch?: HttpHandler,
toastDanger?: (input: ToastInput) => void
toastDanger?: (input: ToastInput) => void,
abortable = false
) {
const kibana = useKibana();
const fetchService = fetch ? fetch : kibana.services.http?.fetch;
const toast = toastDanger ? toastDanger : kibana.notifications.toasts.danger;
const [response, setResponse] = useState<Response | null>(null);
const [error, setError] = useState<InfraHttpError | null>(null);
const abortController = useRef(new AbortController());
const onError = useCallback(
(e: unknown) => {
const err = e as InfraHttpError;
if (e && e instanceof CanceledPromiseError) {
if (e && (e instanceof CanceledPromiseError || (e as Error).name === AbortError.name)) {
return;
}
setError(err);
@ -72,6 +75,14 @@ export function useHTTPRequest<Response>(
[toast]
);
useEffect(() => {
return () => {
if (abortable) {
abortController.current.abort();
}
};
}, [abortable]);
const [request, makeRequest] = useTrackedPromise<any, Response>(
{
cancelPreviousOn: 'resolution',
@ -79,7 +90,15 @@ export function useHTTPRequest<Response>(
if (!fetchService) {
throw new Error('HTTP service is unavailable');
}
if (abortable) {
abortController.current.abort();
}
abortController.current = new AbortController();
return fetchService(pathname, {
signal: abortController.current.signal,
method,
body,
});

View file

@ -5,23 +5,21 @@
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { ControlGroupContainer, CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { Filter, TimeRange } from '@kbn/es-query';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import { DataView } from '@kbn/data-views-plugin/public';
import { Subscription } from 'rxjs';
import { LazyControlsRenderer } from './lazy_controls_renderer';
import { useControlPanels } from '../hooks/use_control_panels_url_state';
interface Props {
timeRange: TimeRange;
dataView: DataView;
filters: Filter[];
query: {
language: string;
query: string;
};
onFilterChange: (filters: Filter[]) => void;
query: Query;
timeRange: TimeRange;
onFiltersChange: (filters: Filter[]) => void;
}
// Disable refresh, allow our timerange changes to refresh the embeddable.
@ -31,31 +29,35 @@ const REFRESH_CONFIG = {
};
export const ControlsContent: React.FC<Props> = ({
timeRange,
dataView,
query,
filters,
onFilterChange,
query,
timeRange,
onFiltersChange,
}) => {
const [controlPanel, setControlPanels] = useControlPanels(dataView);
const [controlGroup, setControlGroup] = useState<ControlGroupContainer | undefined>();
const [controlPanels, setControlPanels] = useControlPanels(dataView);
const inputSubscription = useRef<Subscription>();
const filterSubscription = useRef<Subscription>();
const loadCompleteHandler = useCallback(
(controlGroup: ControlGroupContainer) => {
inputSubscription.current = controlGroup.onFiltersPublished$.subscribe((newFilters) => {
onFiltersChange(newFilters);
});
filterSubscription.current = controlGroup
.getInput$()
.subscribe(({ panels }) => setControlPanels(panels));
},
[onFiltersChange, setControlPanels]
);
useEffect(() => {
if (!controlGroup) {
return;
}
const filtersSubscription = controlGroup.onFiltersPublished$.subscribe((newFilters) => {
onFilterChange(newFilters);
});
const inputSubscription = controlGroup.getInput$().subscribe(({ panels }) => {
setControlPanels(panels);
});
return () => {
filtersSubscription.unsubscribe();
inputSubscription.unsubscribe();
filterSubscription.current?.unsubscribe();
inputSubscription.current?.unsubscribe();
};
}, [controlGroup, onFilterChange, setControlPanels]);
}, []);
return (
<LazyControlsRenderer
@ -71,11 +73,9 @@ export const ControlsContent: React.FC<Props> = ({
chainingSystem: 'HIERARCHICAL',
controlStyle: 'oneLine',
defaultControlWidth: 'small',
panels: controlPanel,
panels: controlPanels,
})}
onLoadComplete={(newControlGroup) => {
setControlGroup(newControlGroup);
}}
onLoadComplete={loadCompleteHandler}
query={query}
timeRange={timeRange}
/>

View file

@ -29,18 +29,21 @@ const HOST_TABLE_METRICS: Array<{ type: SnapshotMetricType }> = [
export const HostsTable = () => {
const { baseRequest, setHostViewState, hostViewState } = useHostsViewContext();
const { onSubmit, unifiedSearchDateRange } = useUnifiedSearchContext();
const { onSubmit, searchCriteria } = useUnifiedSearchContext();
const [properties, setProperties] = useTableProperties();
// Snapshot endpoint internally uses the indices stored in source.configuration.metricAlias.
// For the Unified Search, we create a data view, which for now will be built off of source.configuration.metricAlias too
// if we introduce data view selection, we'll have to change this hook and the endpoint to accept a new parameter for the indices
const { loading, nodes, error } = useSnapshot({
...baseRequest,
metrics: HOST_TABLE_METRICS,
});
const { loading, nodes, error } = useSnapshot(
{
...baseRequest,
metrics: HOST_TABLE_METRICS,
},
{ abortable: true }
);
const { columns, items } = useHostsTable(nodes, { time: unifiedSearchDateRange });
const { columns, items } = useHostsTable(nodes, { time: searchCriteria.dateRange });
useEffect(() => {
if (hostViewState.loading !== loading || nodes.length !== hostViewState.totalHits) {

View file

@ -17,13 +17,16 @@ interface Props extends Omit<ChartBaseProps, 'type'> {
export const Tile = ({ type, ...props }: Props) => {
const { baseRequest } = useHostsViewContext();
const { nodes, loading } = useSnapshot({
...baseRequest,
metrics: [{ type }],
groupBy: null,
includeTimeseries: true,
dropPartialBuckets: false,
});
const { nodes, loading } = useSnapshot(
{
...baseRequest,
metrics: [{ type }],
groupBy: null,
includeTimeseries: true,
dropPartialBuckets: false,
},
{ abortable: true }
);
return <KPIChart id={`$metric-${type}`} type={type} nodes={nodes} loading={loading} {...props} />;
};

View file

@ -30,13 +30,7 @@ export interface MetricChartProps {
const MIN_HEIGHT = 300;
export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => {
const {
unifiedSearchDateRange,
unifiedSearchQuery,
unifiedSearchFilters,
controlPanelFilters,
onSubmit,
} = useUnifiedSearchContext();
const { searchCriteria, onSubmit } = useUnifiedSearchContext();
const { metricsDataView } = useMetricsDataViewContext();
const { baseRequest } = useHostsViewContext();
const {
@ -54,12 +48,12 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) =>
});
const injectedLensAttributes = injectData({
filters: [...unifiedSearchFilters, ...controlPanelFilters],
query: unifiedSearchQuery,
filters: [...searchCriteria.filters, ...searchCriteria.panelFilters],
query: searchCriteria.query,
title,
});
const extraActionOptions = getExtraActions(injectedLensAttributes, unifiedSearchDateRange);
const extraActionOptions = getExtraActions(injectedLensAttributes, searchCriteria.dateRange);
const extraAction: Action[] = [extraActionOptions.openInLens];
const handleBrushEnd = ({ range }: BrushTriggerEvent['data']) => {
@ -109,9 +103,9 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) =>
style={{ height: MIN_HEIGHT }}
attributes={injectedLensAttributes}
viewMode={ViewMode.VIEW}
timeRange={unifiedSearchDateRange}
query={unifiedSearchQuery}
filters={unifiedSearchFilters}
timeRange={searchCriteria.dateRange}
query={searchCriteria.query}
filters={searchCriteria.filters}
extraActions={extraAction}
lastReloadRequestTime={baseRequest.requestTs}
executionContext={{

View file

@ -7,12 +7,17 @@
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import {
compareFilters,
COMPARE_ALL_OPTIONS,
type Filter,
type Query,
type TimeRange,
} from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { SavedQuery } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n';
import { EuiFlexGrid } from '@elastic/eui';
import deepEqual from 'fast-deep-equal';
import type { InfraClientStartDeps } from '../../../../types';
import { useUnifiedSearchContext } from '../hooks/use_unified_search';
import { ControlsContent } from './controls_content';
@ -25,15 +30,7 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
const {
services: { unifiedSearch, application },
} = useKibana<InfraClientStartDeps>();
const {
unifiedSearchDateRange,
unifiedSearchQuery,
unifiedSearchFilters,
controlPanelFilters,
onSubmit,
saveQuery,
clearSavedQuery,
} = useUnifiedSearchContext();
const { searchCriteria, onSubmit, saveQuery, clearSavedQuery } = useUnifiedSearchContext();
const { SearchBar } = unifiedSearch.ui;
@ -42,8 +39,7 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
};
const onPanelFiltersChange = (panelFilters: Filter[]) => {
// <ControlsContent /> triggers this event 2 times during its loading lifecycle
if (!deepEqual(controlPanelFilters, panelFilters)) {
if (!compareFilters(searchCriteria.panelFilters, panelFilters, COMPARE_ALL_OPTIONS)) {
onQueryChange({ panelFilters });
}
};
@ -74,9 +70,9 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
defaultMessage: 'Search hosts (E.g. cloud.provider:gcp AND system.load.1 > 0.5)',
})}
indexPatterns={[dataView]}
query={unifiedSearchQuery}
dateRangeFrom={unifiedSearchDateRange.from}
dateRangeTo={unifiedSearchDateRange.to}
query={searchCriteria.query}
dateRangeFrom={searchCriteria.dateRange.from}
dateRangeTo={searchCriteria.dateRange.to}
onQuerySubmit={onQuerySubmit}
onSaved={onQuerySave}
onSavedQueryUpdated={onQuerySave}
@ -86,11 +82,11 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
displayStyle="inPage"
/>
<ControlsContent
timeRange={unifiedSearchDateRange}
timeRange={searchCriteria.dateRange}
dataView={dataView}
query={unifiedSearchQuery}
filters={unifiedSearchFilters}
onFilterChange={onPanelFiltersChange}
query={searchCriteria.query}
filters={searchCriteria.filters}
onFiltersChange={onPanelFiltersChange}
/>
</EuiFlexGrid>
);

View file

@ -129,6 +129,7 @@ const PanelRT = rt.type({
dataViewId: rt.string,
fieldName: rt.string,
title: rt.union([rt.string, rt.undefined]),
selectedOptions: rt.array(rt.string),
}),
]),
});

View file

@ -157,13 +157,10 @@ export const useUnifiedSearch = () => {
return {
buildQuery,
clearSavedQuery,
controlPanelFilters: state.panelFilters,
onSubmit: debounceOnSubmit,
saveQuery,
getDateRangeAsTimestamp,
unifiedSearchQuery: state.query,
unifiedSearchDateRange: state.dateRange,
unifiedSearchFilters: state.filters,
searchCriteria: { ...state },
};
};

View file

@ -26,18 +26,26 @@ export interface UseSnapshotRequest
timerange?: InfraTimerangeInput;
requestTs?: number;
}
export function useSnapshot({
timerange,
currentTime,
accountId = '',
region = '',
groupBy = null,
sendRequestImmediately = true,
includeTimeseries = true,
dropPartialBuckets = true,
requestTs,
...args
}: UseSnapshotRequest) {
export interface UseSnapshotRequestOptions {
abortable?: boolean;
}
export function useSnapshot(
{
timerange,
currentTime,
accountId = '',
region = '',
groupBy = null,
sendRequestImmediately = true,
includeTimeseries = true,
dropPartialBuckets = true,
requestTs,
...args
}: UseSnapshotRequest,
options?: UseSnapshotRequestOptions
) {
const decodeResponse = (response: any) => {
return pipe(
SnapshotNodeResponseRT.decode(response),
@ -64,7 +72,10 @@ export function useSnapshot({
'/api/metrics/snapshot',
'POST',
JSON.stringify(payload),
decodeResponse
decodeResponse,
undefined,
undefined,
options?.abortable
);
useEffect(() => {

View file

@ -201,9 +201,9 @@ export const useTrackedPromise = <Arguments extends any[], Result>(
if (shouldTriggerOrThrow()) {
if (onReject) {
onReject(value);
} else {
throw value;
}
throw value;
}
}
),