mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Infrastructure UI] Add URL state to Hosts View (#144181)
Closes [#141492](https://github.com/elastic/kibana/issues/141492) ## Summary This PR adds `query`, `timeRange` and `filters` parameters to the URL state on the hosts view. URL parameters are updated after search filters are applied (after click on the "update" button. ## Testing Different cases: - Add new search criteria ( filter / time range / query ) and click on "update" - the URL should update - Save a query and reload - Load a saved query - Change an existing query    - Open the URL in a new browser tab/window - the filters should be added Co-authored-by: Carlos Crespo <carloshenrique.leonelcrespo@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nathan L Smith <nathan.smith@elastic.co>
This commit is contained in:
parent
bebcd354d3
commit
20e2fb5e1e
4 changed files with 234 additions and 60 deletions
|
@ -29,7 +29,7 @@ const HOST_METRICS: Array<{ type: SnapshotMetricType }> = [
|
|||
|
||||
export const HostsTable = () => {
|
||||
const { sourceId } = useSourceContext();
|
||||
const { esQuery, dateRangeTimestamp } = useUnifiedSearchContext();
|
||||
const { buildQuery, dateRangeTimestamp } = useUnifiedSearchContext();
|
||||
|
||||
const timeRange: InfraTimerangeInput = {
|
||||
from: dateRangeTimestamp.from,
|
||||
|
@ -38,6 +38,8 @@ export const HostsTable = () => {
|
|||
ignoreLookback: true,
|
||||
};
|
||||
|
||||
const esQuery = buildQuery();
|
||||
|
||||
// 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
|
||||
|
|
|
@ -24,9 +24,10 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
|
|||
const {
|
||||
unifiedSearchDateRange,
|
||||
unifiedSearchQuery,
|
||||
submitFilterChange,
|
||||
unifiedSearchFilters,
|
||||
onSubmit,
|
||||
saveQuery,
|
||||
clearSavedQUery,
|
||||
clearSavedQuery,
|
||||
} = useUnifiedSearchContext();
|
||||
|
||||
const { SearchBar } = unifiedSearch.ui;
|
||||
|
@ -40,7 +41,7 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
|
|||
};
|
||||
|
||||
const onClearSavedQuery = () => {
|
||||
clearSavedQUery();
|
||||
clearSavedQuery();
|
||||
};
|
||||
|
||||
const onQuerySave = (savedQuery: SavedQuery) => {
|
||||
|
@ -54,7 +55,7 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
|
|||
payload?: { dateRange: TimeRange; query?: Query };
|
||||
filters?: Filter[];
|
||||
}) => {
|
||||
submitFilterChange(payload?.query, payload?.dateRange, filters);
|
||||
onSubmit(payload?.query, payload?.dateRange, filters);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -64,6 +65,7 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
|
|||
query={unifiedSearchQuery}
|
||||
dateRangeFrom={unifiedSearchDateRange.from}
|
||||
dateRangeTo={unifiedSearchDateRange.to}
|
||||
filters={unifiedSearchFilters}
|
||||
onQuerySubmit={onQuerySubmit}
|
||||
onSaved={onQuerySave}
|
||||
onSavedQueryUpdated={onQuerySave}
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useReducer } from 'react';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import DateMath from '@kbn/datemath';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import * as rt from 'io-ts';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { constant, identity } from 'fp-ts/lib/function';
|
||||
import { enumeration } from '@kbn/securitysolution-io-ts-types';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { useUrlState } from '../../../../utils/use_url_state';
|
||||
import { useKibanaTimefilterTime } from '../../../../hooks/use_kibana_timefilter_time';
|
||||
|
||||
const DEFAULT_QUERY = {
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
};
|
||||
const DEFAULT_FROM_MINUTES_VALUE = 15;
|
||||
const INITIAL_DATE = new Date();
|
||||
export const INITIAL_DATE_RANGE = { from: `now-${DEFAULT_FROM_MINUTES_VALUE}m`, to: 'now' };
|
||||
const CALCULATED_DATE_RANGE_FROM = new Date(
|
||||
INITIAL_DATE.getMinutes() - DEFAULT_FROM_MINUTES_VALUE
|
||||
).getTime();
|
||||
const CALCULATED_DATE_RANGE_TO = INITIAL_DATE.getTime();
|
||||
|
||||
const INITIAL_HOSTS_STATE: HostsState = {
|
||||
query: DEFAULT_QUERY,
|
||||
filters: [],
|
||||
// for unified search
|
||||
dateRange: { ...INITIAL_DATE_RANGE },
|
||||
// for useSnapshot
|
||||
dateRangeTimestamp: {
|
||||
from: CALCULATED_DATE_RANGE_FROM,
|
||||
to: CALCULATED_DATE_RANGE_TO,
|
||||
},
|
||||
};
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'setQuery';
|
||||
payload: rt.TypeOf<typeof SetQueryType>;
|
||||
}
|
||||
| { type: 'setFilter'; payload: rt.TypeOf<typeof HostsFiltersRT> };
|
||||
|
||||
const reducer = (state: HostsState, action: Action): HostsState => {
|
||||
switch (action.type) {
|
||||
case 'setFilter':
|
||||
return { ...state, filters: [...action.payload] };
|
||||
case 'setQuery':
|
||||
const { filters, query, ...payload } = action.payload;
|
||||
const newFilters = !filters ? state.filters : filters;
|
||||
const newQuery = !query ? state.query : query;
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
filters: [...newFilters],
|
||||
query: { ...newQuery },
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
export const useHostsUrlState = () => {
|
||||
const [urlState, setUrlState] = useUrlState<HostsState>({
|
||||
defaultState: INITIAL_HOSTS_STATE,
|
||||
decodeUrlState,
|
||||
encodeUrlState,
|
||||
urlStateKey: '_a',
|
||||
});
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, urlState);
|
||||
|
||||
const [getTime] = useKibanaTimefilterTime(INITIAL_DATE_RANGE);
|
||||
|
||||
const getRangeInTimestamp = useCallback(({ from, to }: TimeRange) => {
|
||||
const fromTS = DateMath.parse(from)?.valueOf() ?? CALCULATED_DATE_RANGE_FROM;
|
||||
const toTS = DateMath.parse(to)?.valueOf() ?? CALCULATED_DATE_RANGE_TO;
|
||||
|
||||
return {
|
||||
from: fromTS,
|
||||
to: toTS,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!deepEqual(state, urlState)) {
|
||||
setUrlState(state);
|
||||
}
|
||||
}, [setUrlState, state, urlState]);
|
||||
|
||||
return {
|
||||
state,
|
||||
dispatch,
|
||||
getRangeInTimestamp,
|
||||
getTime,
|
||||
};
|
||||
};
|
||||
|
||||
const HostsFilterRT = rt.intersection([
|
||||
rt.partial({
|
||||
$state: rt.type({
|
||||
store: enumeration('FilterStateStore', FilterStateStore),
|
||||
}),
|
||||
}),
|
||||
rt.type({
|
||||
meta: rt.partial({
|
||||
alias: rt.union([rt.null, rt.string]),
|
||||
disabled: rt.boolean,
|
||||
negate: rt.boolean,
|
||||
controlledBy: rt.string,
|
||||
group: rt.string,
|
||||
index: rt.string,
|
||||
isMultiIndex: rt.boolean,
|
||||
type: rt.string,
|
||||
key: rt.string,
|
||||
params: rt.any,
|
||||
value: rt.any,
|
||||
}),
|
||||
}),
|
||||
rt.partial({
|
||||
query: rt.record(rt.string, rt.any),
|
||||
}),
|
||||
]);
|
||||
|
||||
const HostsFiltersRT = rt.array(HostsFilterRT);
|
||||
|
||||
export const HostsQueryStateRT = rt.type({
|
||||
language: rt.string,
|
||||
query: rt.any,
|
||||
});
|
||||
|
||||
export const StringDateRangeRT = rt.type({
|
||||
from: rt.string,
|
||||
to: rt.string,
|
||||
});
|
||||
|
||||
export const DateRangeRT = rt.type({
|
||||
from: rt.number,
|
||||
to: rt.number,
|
||||
});
|
||||
|
||||
export const HostsStateRT = rt.type({
|
||||
filters: HostsFiltersRT,
|
||||
query: HostsQueryStateRT,
|
||||
dateRange: StringDateRangeRT,
|
||||
dateRangeTimestamp: DateRangeRT,
|
||||
});
|
||||
|
||||
export type HostsState = rt.TypeOf<typeof HostsStateRT>;
|
||||
|
||||
const SetQueryType = rt.partial({
|
||||
query: HostsQueryStateRT,
|
||||
dateRange: StringDateRangeRT,
|
||||
filters: HostsFiltersRT,
|
||||
dateRangeTimestamp: DateRangeRT,
|
||||
});
|
||||
|
||||
const encodeUrlState = HostsStateRT.encode;
|
||||
const decodeUrlState = (value: unknown) => {
|
||||
return pipe(HostsStateRT.decode(value), fold(constant(undefined), identity));
|
||||
};
|
|
@ -6,98 +6,99 @@
|
|||
*/
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import createContainer from 'constate';
|
||||
import { useCallback, useReducer } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { buildEsQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import DateMath from '@kbn/datemath';
|
||||
import type { SavedQuery } from '@kbn/data-plugin/public';
|
||||
import { debounce } from 'lodash';
|
||||
import type { InfraClientStartDeps } from '../../../../types';
|
||||
import { useMetricsDataViewContext } from './use_data_view';
|
||||
import { useKibanaTimefilterTime } from '../../../../hooks/use_kibana_timefilter_time';
|
||||
|
||||
const DEFAULT_FROM_MINUTES_VALUE = 15;
|
||||
import { useSyncKibanaTimeFilterTime } from '../../../../hooks/use_kibana_timefilter_time';
|
||||
import { useHostsUrlState, INITIAL_DATE_RANGE } from './use_hosts_url_state';
|
||||
|
||||
export const useUnifiedSearch = () => {
|
||||
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
||||
|
||||
const { state, dispatch, getRangeInTimestamp, getTime } = useHostsUrlState();
|
||||
const { metricsDataView } = useMetricsDataViewContext();
|
||||
const { services } = useKibana<InfraClientStartDeps>();
|
||||
const {
|
||||
data: { query: queryManager },
|
||||
} = services;
|
||||
|
||||
const [getTime, setTime] = useKibanaTimefilterTime({
|
||||
from: `now-${DEFAULT_FROM_MINUTES_VALUE}m`,
|
||||
to: 'now',
|
||||
useSyncKibanaTimeFilterTime(INITIAL_DATE_RANGE, {
|
||||
from: state.dateRange.from,
|
||||
to: state.dateRange.to,
|
||||
});
|
||||
const { queryString, filterManager } = queryManager;
|
||||
|
||||
const currentDate = new Date();
|
||||
const fromTS =
|
||||
DateMath.parse(getTime().from)?.valueOf() ??
|
||||
new Date(currentDate.getMinutes() - DEFAULT_FROM_MINUTES_VALUE).getTime();
|
||||
const toTS = DateMath.parse(getTime().to)?.valueOf() ?? currentDate.getTime();
|
||||
const { filterManager } = queryManager;
|
||||
|
||||
const currentTimeRange = {
|
||||
from: fromTS,
|
||||
to: toTS,
|
||||
};
|
||||
|
||||
const submitFilterChange = useCallback(
|
||||
const onSubmit = useCallback(
|
||||
(query?: Query, dateRange?: TimeRange, filters?: Filter[]) => {
|
||||
if (filters) {
|
||||
filterManager.setFilters(filters);
|
||||
if (query || dateRange || filters) {
|
||||
const newDateRange = dateRange ?? getTime();
|
||||
|
||||
if (filters) {
|
||||
filterManager.setFilters(filters);
|
||||
}
|
||||
dispatch({
|
||||
type: 'setQuery',
|
||||
payload: {
|
||||
query,
|
||||
filters: filters ? filterManager.getFilters() : undefined,
|
||||
dateRange: newDateRange,
|
||||
dateRangeTimestamp: getRangeInTimestamp(newDateRange),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setTime({
|
||||
...getTime(),
|
||||
...dateRange,
|
||||
});
|
||||
|
||||
queryString.setQuery({ ...queryString.getQuery(), ...query });
|
||||
// Unified search holds the all state, we need to force the hook to rerender so that it can return the most recent values
|
||||
// This can be removed once we get the state from the URL
|
||||
forceUpdate();
|
||||
},
|
||||
[filterManager, queryString, getTime, setTime]
|
||||
[filterManager, getRangeInTimestamp, getTime, dispatch]
|
||||
);
|
||||
|
||||
// This won't prevent onSubmit from being fired twice when `clear filters` is clicked,
|
||||
// that happens because both onQuerySubmit and onFiltersUpdated are internally triggered on same event by SearchBar.
|
||||
// This just delays potential duplicate onSubmit calls
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debounceOnSubmit = useCallback(debounce(onSubmit, 100), [onSubmit]);
|
||||
|
||||
const saveQuery = useCallback(
|
||||
(newSavedQuery: SavedQuery) => {
|
||||
const savedQueryFilters = newSavedQuery.attributes.filters ?? [];
|
||||
const globalFilters = filterManager.getGlobalFilters();
|
||||
filterManager.setFilters([...savedQueryFilters, ...globalFilters]);
|
||||
|
||||
// Unified search holds the all state, we need to force the hook to rerender so that it can return the most recent values
|
||||
// This can be removed once we get the state from the URL
|
||||
forceUpdate();
|
||||
const query = newSavedQuery.attributes.query;
|
||||
|
||||
dispatch({
|
||||
type: 'setQuery',
|
||||
payload: {
|
||||
query,
|
||||
filters: [...savedQueryFilters, ...globalFilters],
|
||||
},
|
||||
});
|
||||
},
|
||||
[filterManager]
|
||||
[filterManager, dispatch]
|
||||
);
|
||||
|
||||
const clearSavedQUery = useCallback(() => {
|
||||
filterManager.setFilters(filterManager.getGlobalFilters());
|
||||
|
||||
// Unified search holds the all state, we need to force the hook to rerender so that it can return the most recent values
|
||||
// This can be removed once we get the state from the URL
|
||||
forceUpdate();
|
||||
}, [filterManager]);
|
||||
const clearSavedQuery = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'setFilter',
|
||||
payload: filterManager.getGlobalFilters(),
|
||||
});
|
||||
}, [filterManager, dispatch]);
|
||||
|
||||
const buildQuery = useCallback(() => {
|
||||
if (!metricsDataView) {
|
||||
return null;
|
||||
}
|
||||
return buildEsQuery(metricsDataView, queryString.getQuery(), filterManager.getFilters());
|
||||
}, [filterManager, metricsDataView, queryString]);
|
||||
return buildEsQuery(metricsDataView, state.query, state.filters);
|
||||
}, [metricsDataView, state.filters, state.query]);
|
||||
|
||||
return {
|
||||
dateRangeTimestamp: currentTimeRange,
|
||||
esQuery: buildQuery(),
|
||||
submitFilterChange,
|
||||
dateRangeTimestamp: state.dateRangeTimestamp,
|
||||
buildQuery,
|
||||
onSubmit: debounceOnSubmit,
|
||||
saveQuery,
|
||||
clearSavedQUery,
|
||||
unifiedSearchQuery: queryString.getQuery() as Query,
|
||||
clearSavedQuery,
|
||||
unifiedSearchQuery: state.query,
|
||||
unifiedSearchDateRange: getTime(),
|
||||
unifiedSearchFilters: filterManager.getFilters(),
|
||||
unifiedSearchFilters: state.filters,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue