mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# 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:
parent
c5b5c418ed
commit
943b5fceef
10 changed files with 122 additions and 98 deletions
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
|
|
@ -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 },
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -201,9 +201,9 @@ export const useTrackedPromise = <Arguments extends any[], Result>(
|
|||
if (shouldTriggerOrThrow()) {
|
||||
if (onReject) {
|
||||
onReject(value);
|
||||
} else {
|
||||
throw value;
|
||||
}
|
||||
|
||||
throw value;
|
||||
}
|
||||
}
|
||||
),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue