[Uptime] Drop requests for overview page data when filter not initialized (#120031)

* Drop requests for overview page data when the value for the es filters has not been initially set.

* Update to not wait when no filters are defined.

* Add tests and clean up some files.

* Delete unneeded code.

* Reduce check condition to single boolean value to avoid triggering effect multiple times.

* Simplify implementation, fix race. Update tests and fix types.

* Make `useOverviewFilterCheck` consider kuery search as well.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Justin Kambic 2021-12-20 15:34:30 -05:00 committed by GitHub
parent 18d7793230
commit e8bf0392f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 249 additions and 54 deletions

View file

@ -14,6 +14,7 @@ import { useGetUrlParams } from '../../../hooks';
import { useMonitorId } from '../../../hooks';
import { ResponsiveWrapperProps, withResponsiveWrapper } from '../../common/higher_order';
import { UptimeRefreshContext } from '../../../contexts';
import { useOverviewFilterCheck } from '../../../hooks/use_overview_filter_check';
interface Props {
height: string;
@ -27,6 +28,7 @@ const Container: React.FC<Props & ResponsiveWrapperProps> = ({ height }) => {
dateRangeStart: dateStart,
dateRangeEnd: dateEnd,
} = useGetUrlParams();
const filterCheck = useOverviewFilterCheck();
const dispatch = useDispatch();
const monitorId = useMonitorId();
@ -38,8 +40,10 @@ const Container: React.FC<Props & ResponsiveWrapperProps> = ({ height }) => {
const { loading, pingHistogram: data } = useSelector(selectPingHistogram);
useEffect(() => {
dispatch(getPingHistogram.get({ monitorId, dateStart, dateEnd, query, filters: esKuery }));
}, [dateStart, dateEnd, monitorId, lastRefresh, esKuery, dispatch, query]);
filterCheck(() =>
dispatch(getPingHistogram.get({ monitorId, dateStart, dateEnd, query, filters: esKuery }))
);
}, [filterCheck, dateStart, dateEnd, monitorId, lastRefresh, esKuery, dispatch, query]);
return (
<PingHistogramComponent
data={data}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useIndexPattern, useUpdateKueryString } from '../../../../hooks';
import { useIndexPattern, generateUpdatedKueryString } from '../../../../hooks';
import { useFetcher } from '../../../../../../observability/public';
import { fetchSnapshotCount } from '../../../../state/api';
@ -17,7 +17,7 @@ export const useSnapShotCount = ({ query, filters }: { query: string; filters: [
const indexPattern = useIndexPattern();
const [esKuery, error] = useUpdateKueryString(indexPattern, query, parsedFilters);
const [esKuery, error] = generateUpdatedKueryString(indexPattern, query, parsedFilters);
const { data, loading } = useFetcher(
() =>

View file

@ -14,6 +14,7 @@ import { useUrlParams } from '../../../hooks';
import { UptimeRefreshContext } from '../../../contexts';
import { getConnectorsAction, getMonitorAlertsAction } from '../../../state/alerts/alerts';
import { useMappingCheck } from '../../../hooks/use_mapping_check';
import { useOverviewFilterCheck } from '../../../hooks/use_overview_filter_check';
export interface MonitorListProps {
filters?: string;
@ -31,6 +32,7 @@ const getPageSizeValue = () => {
export const MonitorList: React.FC<MonitorListProps> = (props) => {
const filters = useSelector(esKuerySelector);
const filterCheck = useOverviewFilterCheck();
const [pageSize, setPageSize] = useState<number>(getPageSizeValue);
@ -45,22 +47,25 @@ export const MonitorList: React.FC<MonitorListProps> = (props) => {
useMappingCheck(monitorList.error);
useEffect(() => {
dispatch(
getMonitorList({
dateRangeStart,
dateRangeEnd,
filters,
pageSize,
pagination,
statusFilter,
query,
})
filterCheck(() =>
dispatch(
getMonitorList({
dateRangeStart,
dateRangeEnd,
filters,
pageSize,
pagination,
statusFilter,
query,
})
)
);
}, [
dispatch,
dateRangeStart,
dateRangeEnd,
filters,
filterCheck,
lastRefresh,
pageSize,
pagination,

View file

@ -49,7 +49,7 @@ describe.skip('useQueryBar', () => {
);
useUrlParamsSpy = jest.spyOn(URL, 'useUrlParams');
useGetUrlParamsSpy = jest.spyOn(URL, 'useGetUrlParams');
useUpdateKueryStringSpy = jest.spyOn(ES_FILTERS, 'useUpdateKueryString');
useUpdateKueryStringSpy = jest.spyOn(ES_FILTERS, 'generateUpdatedKueryString');
updateUrlParamsMock = jest.fn();
useUrlParamsSpy.mockImplementation(() => [jest.fn(), updateUrlParamsMock]);

View file

@ -12,7 +12,7 @@ import { Query } from 'src/plugins/data/common';
import {
useGetUrlParams,
useIndexPattern,
useUpdateKueryString,
generateUpdatedKueryString,
useUrlParams,
} from '../../../hooks';
import { setEsKueryString } from '../../../state/actions';
@ -74,7 +74,7 @@ export const useQueryBar = (): UseQueryBarUtils => {
const [, updateUrlParams] = useUrlParams();
const [esFilters, error] = useUpdateKueryString(
const [esFilters, error] = generateUpdatedKueryString(
indexPattern,
query.language === SyntaxType.kuery ? (query.query as string) : undefined,
paramFilters,

View file

@ -41,7 +41,7 @@ const getKueryString = (urlFilters: string, excludedFilters?: string): string =>
return `NOT (${excludeKueryString})`;
};
export const useUpdateKueryString = (
export const generateUpdatedKueryString = (
indexPattern: IndexPattern | null,
filterQueryString = '',
urlFilters: string,

View file

@ -0,0 +1,102 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { createMemoryHistory } from 'history';
import React from 'react';
import * as reactRedux from 'react-redux';
import { useOverviewFilterCheck } from './use_overview_filter_check';
import { MockRouter } from '../lib/helper/rtl_helpers';
function getWrapper(customSearch?: string): React.FC {
return ({ children }) => {
const { location, ...rest } = createMemoryHistory();
return (
<MockRouter
history={{
...rest,
location: {
...location,
search: customSearch ? customSearch : location.search,
},
}}
>
{children}
</MockRouter>
);
};
}
const SEARCH_WITH_FILTERS = '?dateRangeStart=now-30m&filters=%5B%5B"url.port"%2C%5B"5601"%5D%5D%5D';
const SEARCH_WITH_KUERY = '?search=monitor.id%20%3A%20"header-test"';
describe('useOverviewFilterCheck', () => {
beforeEach(() => {
jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => false);
});
it('returns a function that will run code when there are no filters', () => {
const {
result: { current },
} = renderHook(() => useOverviewFilterCheck(), { wrapper: getWrapper() });
const fn = jest.fn();
current(fn);
expect(fn).toHaveBeenCalledTimes(1);
});
it('returns a function that will not run code if there are uninitialized filters', () => {
const {
result: { current },
} = renderHook(() => useOverviewFilterCheck(), {
wrapper: getWrapper(SEARCH_WITH_FILTERS),
});
const fn = jest.fn();
current(fn);
expect(fn).not.toHaveBeenCalled();
});
it('returns a function that will run code if filters are initialized', () => {
jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => true);
const {
result: { current },
} = renderHook(() => useOverviewFilterCheck(), {
wrapper: getWrapper(SEARCH_WITH_FILTERS),
});
const fn = jest.fn();
current(fn);
expect(fn).toHaveBeenCalledTimes(1);
});
it('returns a function that will not run code if search is uninitialized', () => {
jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => '');
const {
result: { current },
} = renderHook(() => useOverviewFilterCheck(), {
wrapper: getWrapper(SEARCH_WITH_KUERY),
});
const fn = jest.fn();
current(fn);
expect(fn).not.toHaveBeenCalledTimes(1);
});
it('returns a function that will run if search is initialized', () => {
jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => 'search is initialized');
const {
result: { current },
} = renderHook(() => useOverviewFilterCheck(), {
wrapper: getWrapper(SEARCH_WITH_KUERY),
});
const fn = jest.fn();
current(fn);
expect(fn).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,53 @@
/*
* 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 } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { getParsedParams } from '../lib/helper/parse_search';
import { esKuerySelector } from '../state/selectors';
function hasFilters(search: string) {
const parsed = getParsedParams(search);
return !!parsed.filters || !!parsed.search;
}
/**
* Specifically designed for the overview page, this hook will create
* a function that the caller can use to run code only once the filter
* index pattern has been initialized.
*
* In the case where no filters are
* defined in the URL path, the check will pass and call the function.
*/
export function useOverviewFilterCheck() {
const filters = useSelector(esKuerySelector);
const { search } = useLocation();
/**
* Here, `filters` represents the pre-processed output of parsing the kuery
* syntax and unifying it with any top-level filters the user has selected.
*
* The `hasFilters` flag will be true when the URL contains a truthy `filters`
* query key, _or_ a truthy `search` key. The callback `shouldRun` if:
*
* 1. `filters` are defined: the initial processing has finished and the app is
* ready to send its initial requests.
* 2. There are no search/filters defined in the URL, i.e. `!hasFilters === true`.
*/
const shouldRun = !!filters || !hasFilters(search);
return useCallback(
(fn: () => void) => {
if (shouldRun) {
fn();
}
},
[shouldRun]
);
}

View file

@ -6,13 +6,14 @@
*/
import { useCallback, useEffect } from 'react';
import { parse, stringify } from 'query-string';
import { stringify } from 'query-string';
import { useLocation, useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { UptimeUrlParams, getSupportedUrlParams } from '../lib/helper';
import { selectedFiltersSelector } from '../state/selectors';
import { setSelectedFilters } from '../state/actions/selected_filters';
import { getFiltersFromMap } from './use_selected_filters';
import { getParsedParams } from '../lib/helper/parse_search';
export type GetUrlParams = () => UptimeUrlParams;
export type UpdateUrlParams = (updatedParams: {
@ -21,10 +22,6 @@ export type UpdateUrlParams = (updatedParams: {
export type UptimeUrlParamsHook = () => [GetUrlParams, UpdateUrlParams];
const getParsedParams = (search: string) => {
return search ? parse(search[0] === '?' ? search.slice(1) : search, { sort: false }) : {};
};
export const useGetUrlParams: GetUrlParams = () => {
const { search } = useLocation();

View file

@ -0,0 +1,28 @@
/*
* 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 { getParsedParams } from './parse_search';
describe('getParsedParams', () => {
it('parses the query operator out', () => {
expect(getParsedParams('?val1=3&val2=5')).toEqual({
val1: '3',
val2: '5',
});
});
it('returns empty object for no search value', () => {
expect(getParsedParams('')).toEqual({});
});
it('also parses queries if there is no query operator', () => {
expect(getParsedParams('val1=3&val2=5')).toEqual({
val1: '3',
val2: '5',
});
});
});

View file

@ -0,0 +1,12 @@
/*
* 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 { parse } from 'query-string';
export function getParsedParams(search: string) {
return search ? parse(search[0] === '?' ? search.slice(1) : search, { sort: false }) : {};
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { ReactElement } from 'react';
import React, { ReactElement, ReactNode } from 'react';
import { of } from 'rxjs';
// eslint-disable-next-line import/no-extraneous-dependencies
import {
@ -52,7 +52,7 @@ export interface KibanaProviderOptions<ExtraCore> {
}
interface MockKibanaProviderProps<ExtraCore> extends KibanaProviderOptions<ExtraCore> {
children: React.ReactNode;
children: ReactElement | ReactNode;
}
interface MockRouterProps<ExtraCore> extends MockKibanaProviderProps<ExtraCore> {

View file

@ -1,26 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ui reducer adds integration popover status to state 1`] = `
Object {
"alertFlyoutVisible": false,
"basePath": "",
"esKuery": "",
"integrationsPopoverOpen": Object {
"id": "popover-2",
"open": true,
},
"monitorId": "test",
"searchText": "",
}
`;
exports[`ui reducer sets the application's base path 1`] = `
Object {
"alertFlyoutVisible": false,
"basePath": "yyz",
"esKuery": "",
"integrationsPopoverOpen": null,
"monitorId": "test",
"searchText": "",
}
`;

View file

@ -29,7 +29,16 @@ describe('ui reducer', () => {
},
action
)
).toMatchSnapshot();
).toMatchInlineSnapshot(`
Object {
"alertFlyoutVisible": false,
"basePath": "yyz",
"esKuery": "",
"integrationsPopoverOpen": null,
"monitorId": "test",
"searchText": "",
}
`);
});
it('adds integration popover status to state', () => {
@ -49,7 +58,19 @@ describe('ui reducer', () => {
},
action
)
).toMatchSnapshot();
).toMatchInlineSnapshot(`
Object {
"alertFlyoutVisible": false,
"basePath": "",
"esKuery": "",
"integrationsPopoverOpen": Object {
"id": "popover-2",
"open": true,
},
"monitorId": "test",
"searchText": "",
}
`);
});
it('updates the alert flyout value', () => {

View file

@ -58,7 +58,6 @@ export const uiReducer = handleActions<UiState, UiPayload>(
...state,
esKuery: action.payload as string,
}),
[String(setAlertFlyoutType)]: (state, action: Action<string>) => ({
...state,
alertFlyoutType: action.payload,