[Unified observability] Fix refresh button in the overview page (#126927)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alejandro Fernández Gómez 2022-03-16 09:12:08 +01:00 committed by GitHub
parent c1704d9c9d
commit a13e99c67c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 354 additions and 337 deletions

View file

@ -34,7 +34,13 @@ describe('renderApp', () => {
data: {
query: {
timefilter: {
timefilter: { setTime: jest.fn(), getTime: jest.fn().mockImplementation(() => ({})) },
timefilter: {
setTime: jest.fn(),
getTime: jest.fn().mockReturnValue({}),
getTimeDefaults: jest.fn().mockReturnValue({}),
getRefreshInterval: jest.fn().mockReturnValue({}),
getRefreshIntervalDefaults: jest.fn().mockReturnValue({}),
},
},
},
},

View file

@ -19,6 +19,7 @@ import {
} from '../../../../../src/plugins/kibana_react/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import type { LazyObservabilityPageTemplateProps } from '../components/shared/page_template/lazy_page_template';
import { DatePickerContextProvider } from '../context/date_picker_context';
import { HasDataContextProvider } from '../context/has_data_context';
import { PluginContext } from '../context/plugin_context';
import { useRouteParams } from '../hooks/use_route_params';
@ -90,9 +91,11 @@ export const renderApp = ({
<EuiThemeProvider darkMode={isDarkMode}>
<i18nCore.Context>
<RedirectAppLinks application={core.application} className={APP_WRAPPER_CLASS}>
<HasDataContextProvider>
<App />
</HasDataContextProvider>
<DatePickerContextProvider>
<HasDataContextProvider>
<App />
</HasDataContextProvider>
</DatePickerContextProvider>
</RedirectAppLinks>
</i18nCore.Context>
</EuiThemeProvider>

View file

@ -21,7 +21,7 @@ import moment from 'moment';
import React, { useContext } from 'react';
import { useHistory } from 'react-router-dom';
import { ThemeContext } from 'styled-components';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { useDatePickerContext } from '../../../../hooks/use_date_picker_context';
import { SectionContainer } from '../';
import { getDataHandler } from '../../../../data_handler';
import { useChartTheme } from '../../../../hooks/use_chart_theme';
@ -58,11 +58,12 @@ export function APMSection({ bucketSize }: Props) {
const chartTheme = useChartTheme();
const history = useHistory();
const { forceUpdate, hasDataMap } = useHasData();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd, lastUpdated } =
useDatePickerContext();
const { data, status } = useFetcher(
() => {
if (bucketSize) {
if (bucketSize && absoluteStart && absoluteEnd) {
return getDataHandler('apm')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
@ -70,9 +71,9 @@ export function APMSection({ bucketSize }: Props) {
});
}
},
// Absolute times shouldn't be used here, since it would refetch on every render
// `forceUpdate` and `lastUpdated` should trigger a reload
// eslint-disable-next-line react-hooks/exhaustive-deps
[bucketSize, relativeStart, relativeEnd, forceUpdate]
[bucketSize, relativeStart, relativeEnd, absoluteStart, absoluteEnd, forceUpdate, lastUpdated]
);
if (!hasDataMap.apm?.hasData) {

View file

@ -26,7 +26,7 @@ import { getDataHandler } from '../../../../data_handler';
import { useChartTheme } from '../../../../hooks/use_chart_theme';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useHasData } from '../../../../hooks/use_has_data';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { useDatePickerContext } from '../../../../hooks/use_date_picker_context';
import { LogsFetchDataResponse } from '../../../../typings';
import { formatStatValue } from '../../../../utils/format_stat_value';
import { ChartContainer } from '../../chart_container';
@ -57,11 +57,12 @@ export function LogsSection({ bucketSize }: Props) {
const history = useHistory();
const chartTheme = useChartTheme();
const { forceUpdate, hasDataMap } = useHasData();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd, lastUpdated } =
useDatePickerContext();
const { data, status } = useFetcher(
() => {
if (bucketSize) {
if (bucketSize && absoluteStart && absoluteEnd) {
return getDataHandler('infra_logs')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
@ -69,9 +70,10 @@ export function LogsSection({ bucketSize }: Props) {
});
}
},
// Absolute times shouldn't be used here, since it would refetch on every render
// `forceUpdate` and `lastUpdated` trigger a reload
// eslint-disable-next-line react-hooks/exhaustive-deps
[bucketSize, relativeStart, relativeEnd, forceUpdate]
[bucketSize, relativeStart, relativeEnd, absoluteStart, absoluteEnd, forceUpdate, lastUpdated]
);
if (!hasDataMap.infra_logs?.hasData) {

View file

@ -25,7 +25,7 @@ import { SectionContainer } from '../';
import { getDataHandler } from '../../../../data_handler';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useHasData } from '../../../../hooks/use_has_data';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { useDatePickerContext } from '../../../../hooks/use_date_picker_context';
import { HostLink } from './host_link';
import { formatDuration } from './lib/format_duration';
import { MetricWithSparkline } from './metric_with_sparkline';
@ -51,25 +51,31 @@ const bytesPerSecondFormatter = (value: NumberOrNull) =>
export function MetricsSection({ bucketSize }: Props) {
const { forceUpdate, hasDataMap } = useHasData();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd, lastUpdated } =
useDatePickerContext();
const [sortDirection, setSortDirection] = useState<Direction>('asc');
const [sortField, setSortField] = useState<keyof MetricsFetchDataSeries>('uptime');
const [sortedData, setSortedData] = useState<MetricsFetchDataResponse | null>(null);
const { data, status } = useFetcher(
() => {
if (bucketSize) {
return getDataHandler('infra_metrics')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
...bucketSize,
});
}
},
// Absolute times shouldn't be used here, since it would refetch on every render
const { data, status } = useFetcher(() => {
if (bucketSize && absoluteStart && absoluteEnd) {
return getDataHandler('infra_metrics')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
...bucketSize,
});
}
// `forceUpdate` and `lastUpdated` should trigger a reload
// eslint-disable-next-line react-hooks/exhaustive-deps
[bucketSize, relativeStart, relativeEnd, forceUpdate]
);
}, [
bucketSize,
relativeStart,
relativeEnd,
absoluteStart,
absoluteEnd,
forceUpdate,
lastUpdated,
]);
const handleTableChange = useCallback(
({ sort }: Criteria<MetricsFetchDataSeries>) => {
@ -125,7 +131,7 @@ export function MetricsSection({ bucketSize }: Props) {
<HostLink
id={record.id}
name={value}
timerange={{ from: absoluteStart, to: absoluteEnd }}
timerange={{ from: absoluteStart!, to: absoluteEnd! }}
/>
),
},

View file

@ -27,7 +27,7 @@ import { getDataHandler } from '../../../../data_handler';
import { useChartTheme } from '../../../../hooks/use_chart_theme';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useHasData } from '../../../../hooks/use_has_data';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { useDatePickerContext } from '../../../../hooks/use_date_picker_context';
import { Series } from '../../../../typings';
import { ChartContainer } from '../../chart_container';
import { StyledStat } from '../../styled_stat';
@ -43,11 +43,12 @@ export function UptimeSection({ bucketSize }: Props) {
const chartTheme = useChartTheme();
const history = useHistory();
const { forceUpdate, hasDataMap } = useHasData();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd, lastUpdated } =
useDatePickerContext();
const { data, status } = useFetcher(
() => {
if (bucketSize) {
if (bucketSize && absoluteStart && absoluteEnd) {
return getDataHandler('synthetics')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
@ -55,9 +56,9 @@ export function UptimeSection({ bucketSize }: Props) {
});
}
},
// Absolute times shouldn't be used here, since it would refetch on every render
// `forceUpdate` and `lastUpdated` should trigger a reload
// eslint-disable-next-line react-hooks/exhaustive-deps
[bucketSize, relativeStart, relativeEnd, forceUpdate]
[bucketSize, relativeStart, relativeEnd, absoluteStart, absoluteEnd, forceUpdate, lastUpdated]
);
if (!hasDataMap.synthetics?.hasData) {

View file

@ -11,7 +11,7 @@ import { SectionContainer } from '../';
import { getDataHandler } from '../../../../data_handler';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useHasData } from '../../../../hooks/use_has_data';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { useDatePickerContext } from '../../../../hooks/use_date_picker_context';
import CoreVitals from '../../../shared/core_web_vitals';
import { BucketSize } from '../../../../pages/overview';
@ -21,13 +21,14 @@ interface Props {
export function UXSection({ bucketSize }: Props) {
const { forceUpdate, hasDataMap } = useHasData();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd, lastUpdated } =
useDatePickerContext();
const uxHasDataResponse = hasDataMap.ux;
const serviceName = uxHasDataResponse?.serviceName as string;
const { data, status } = useFetcher(
() => {
if (serviceName && bucketSize) {
if (serviceName && bucketSize && absoluteStart && absoluteEnd) {
return getDataHandler('ux')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
@ -36,9 +37,18 @@ export function UXSection({ bucketSize }: Props) {
});
}
},
// Absolute times shouldn't be used here, since it would refetch on every render
// `forceUpdate` and `lastUpdated` should trigger a reload
// eslint-disable-next-line react-hooks/exhaustive-deps
[bucketSize, relativeStart, relativeEnd, forceUpdate, serviceName]
[
bucketSize,
relativeStart,
relativeEnd,
absoluteStart,
absoluteEnd,
forceUpdate,
serviceName,
lastUpdated,
]
);
if (!uxHasDataResponse?.hasData) {

View file

@ -15,6 +15,7 @@ import qs from 'query-string';
import { DatePicker } from './';
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
import { of } from 'rxjs';
import { DatePickerContextProvider } from '../../../context/date_picker_context';
let history: MemoryHistory;
@ -69,7 +70,13 @@ function mountDatePicker(initialParams: {
data: {
query: {
timefilter: {
timefilter: { setTime: setTimeSpy, getTime: getTimeSpy },
timefilter: {
setTime: setTimeSpy,
getTime: getTimeSpy,
getTimeDefaults: jest.fn().mockReturnValue({}),
getRefreshIntervalDefaults: jest.fn().mockReturnValue({}),
getRefreshInterval: jest.fn().mockReturnValue({}),
},
},
},
},
@ -79,7 +86,9 @@ function mountDatePicker(initialParams: {
},
}}
>
<DatePickerWrapper />
<DatePickerContextProvider>
<DatePickerWrapper />
</DatePickerContextProvider>
</KibanaContextProvider>
</Router>
);
@ -106,7 +115,8 @@ describe('DatePicker', () => {
rangeTo: 'now',
});
expect(mockHistoryReplace).toHaveBeenCalledTimes(0);
// It updates the URL when it doesn't contain the range.
expect(mockHistoryPush).toHaveBeenCalledTimes(1);
wrapper.find(EuiSuperDatePicker).props().onTimeChange({
start: 'now-90m',
@ -114,7 +124,7 @@ describe('DatePicker', () => {
isInvalid: false,
isQuickSelection: true,
});
expect(mockHistoryPush).toHaveBeenCalledTimes(1);
expect(mockHistoryPush).toHaveBeenCalledTimes(2);
expect(mockHistoryPush).toHaveBeenLastCalledWith(
expect.objectContaining({
search: 'rangeFrom=now-90m&rangeTo=now-60m',
@ -152,17 +162,6 @@ describe('DatePicker', () => {
});
describe('if both `rangeTo` and `rangeFrom` is set', () => {
it('calls setTime ', async () => {
const { setTimeSpy } = mountDatePicker({
rangeTo: 'now-20m',
rangeFrom: 'now-22m',
});
expect(setTimeSpy).toHaveBeenCalledWith({
to: 'now-20m',
from: 'now-22m',
});
});
it('does not update the url', () => {
expect(mockHistoryReplace).toHaveBeenCalledTimes(0);
});

View file

@ -6,13 +6,10 @@
*/
import { EuiSuperDatePicker } from '@elastic/eui';
import React, { useEffect } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import React, { useCallback } from 'react';
import { UI_SETTINGS, useKibanaUISettings } from '../../../hooks/use_kibana_ui_settings';
import { fromQuery, toQuery } from '../../../utils/url';
import { TimePickerQuickRange } from './typings';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { useDatePickerContext } from '../../../hooks/use_date_picker_context';
export interface DatePickerProps {
rangeFrom?: string;
@ -29,19 +26,7 @@ export function DatePicker({
refreshInterval,
onTimeRangeRefresh,
}: DatePickerProps) {
const location = useLocation();
const history = useHistory();
const { data } = useKibana<ObservabilityPublicPluginsStart>().services;
useEffect(() => {
// set time if both to and from are given in the url
if (rangeFrom && rangeTo) {
data.query.timefilter.timefilter.setTime({
from: rangeFrom,
to: rangeTo,
});
}
}, [data, rangeFrom, rangeTo]);
const { updateTimeRange, updateRefreshInterval } = useDatePickerContext();
const timePickerQuickRanges = useKibanaUISettings<TimePickerQuickRange[]>(
UI_SETTINGS.TIMEPICKER_QUICK_RANGES
@ -53,21 +38,6 @@ export function DatePicker({
label: display,
}));
function updateUrl(nextQuery: {
rangeFrom?: string;
rangeTo?: string;
refreshPaused?: boolean;
refreshInterval?: number;
}) {
history.push({
...location,
search: fromQuery({
...toQuery(location.search),
...nextQuery,
}),
});
}
function onRefreshChange({
isPaused,
refreshInterval: interval,
@ -75,23 +45,29 @@ export function DatePicker({
isPaused: boolean;
refreshInterval: number;
}) {
updateUrl({ refreshPaused: isPaused, refreshInterval: interval });
updateRefreshInterval({ isPaused, interval });
}
function onTimeChange({ start, end }: { start: string; end: string }) {
updateUrl({ rangeFrom: start, rangeTo: end });
}
const onRefresh = useCallback(
(newRange: { start: string; end: string }) => {
if (onTimeRangeRefresh) {
onTimeRangeRefresh(newRange);
}
updateTimeRange(newRange);
},
[onTimeRangeRefresh, updateTimeRange]
);
return (
<EuiSuperDatePicker
start={rangeFrom}
end={rangeTo}
onTimeChange={onTimeChange}
onTimeChange={onRefresh}
onRefresh={onRefresh}
isPaused={refreshPaused}
refreshInterval={refreshInterval}
onRefreshChange={onRefreshChange}
commonlyUsedRanges={commonlyUsedRanges}
onRefresh={onTimeRangeRefresh}
/>
);
}

View file

@ -0,0 +1,156 @@
/*
* 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 React, { createContext, useState, useMemo, useCallback } from 'react';
import useMount from 'react-use/lib/useMount';
import { useLocation, useHistory } from 'react-router-dom';
import { parse } from 'query-string';
import { fromQuery, ObservabilityPublicPluginsStart, toQuery } from '..';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { getAbsoluteTime } from '../utils/date';
export interface DatePickerContextValue {
relativeStart: string;
relativeEnd: string;
absoluteStart?: number;
absoluteEnd?: number;
refreshInterval: number;
refreshPaused: boolean;
updateTimeRange: (params: { start: string; end: string }) => void;
updateRefreshInterval: (params: { interval: number; isPaused: boolean }) => void;
lastUpdated: number;
}
/**
* This context contains the time range (both relative and absolute) and the
* autorefresh status of the overview page date picker.
* It also updates the URL when any of the values change
*/
export const DatePickerContext = createContext({} as DatePickerContextValue);
export function DatePickerContextProvider({ children }: { children: React.ReactElement }) {
const location = useLocation();
const history = useHistory();
const updateUrl = useCallback(
(nextQuery: {
rangeFrom?: string;
rangeTo?: string;
refreshPaused?: boolean;
refreshInterval?: number;
}) => {
history.push({
...location,
search: fromQuery({
...toQuery(location.search),
...nextQuery,
}),
});
},
[history, location]
);
const [lastUpdated, setLastUpdated] = useState(Date.now());
const { data } = useKibana<ObservabilityPublicPluginsStart>().services;
const defaultTimeRange = data.query.timefilter.timefilter.getTimeDefaults();
const sharedTimeRange = data.query.timefilter.timefilter.getTime();
const defaultRefreshInterval = data.query.timefilter.timefilter.getRefreshIntervalDefaults();
const sharedRefreshInterval = data.query.timefilter.timefilter.getRefreshInterval();
const {
rangeFrom = sharedTimeRange.from ?? defaultTimeRange.from,
rangeTo = sharedTimeRange.to ?? defaultTimeRange.to,
refreshInterval = sharedRefreshInterval.value || defaultRefreshInterval.value || 10000, // we want to override a default of 0
refreshPaused = sharedRefreshInterval.pause ?? defaultRefreshInterval.pause,
} = parse(location.search, {
sort: false,
});
const relativeStart = rangeFrom as string;
const relativeEnd = rangeTo as string;
const absoluteStart = useMemo(
() => getAbsoluteTime(relativeStart),
// `lastUpdated` works as a cache buster
// eslint-disable-next-line react-hooks/exhaustive-deps
[relativeStart, lastUpdated]
);
const absoluteEnd = useMemo(
() => getAbsoluteTime(relativeEnd, { roundUp: true }),
// `lastUpdated` works as a cache buster
// eslint-disable-next-line react-hooks/exhaustive-deps
[relativeEnd, lastUpdated]
);
const updateTimeRange = useCallback(
({ start, end }: { start: string; end: string }) => {
data.query.timefilter.timefilter.setTime({ from: start, to: end });
updateUrl({ rangeFrom: start, rangeTo: end });
setLastUpdated(Date.now());
},
[data.query.timefilter.timefilter, updateUrl]
);
const updateRefreshInterval = useCallback(
({ interval, isPaused }) => {
updateUrl({ refreshInterval: interval, refreshPaused: isPaused });
data.query.timefilter.timefilter.setRefreshInterval({ value: interval, pause: isPaused });
setLastUpdated(Date.now());
},
[data.query.timefilter.timefilter, updateUrl]
);
useMount(() => {
updateUrl({ rangeFrom: relativeStart, rangeTo: relativeEnd });
});
return (
<DatePickerContext.Provider
value={{
relativeStart,
relativeEnd,
absoluteStart,
absoluteEnd,
refreshInterval: parseRefreshInterval(refreshInterval),
refreshPaused: parseRefreshPaused(refreshPaused),
updateTimeRange,
updateRefreshInterval,
lastUpdated,
}}
>
{children}
</DatePickerContext.Provider>
);
}
function parseRefreshInterval(value: string | string[] | number | null): number {
switch (typeof value) {
case 'number':
return value;
case 'string':
return parseInt(value, 10) || 0;
default:
return 0;
}
}
function parseRefreshPaused(value: string | string[] | boolean | null): boolean {
if (typeof value === 'boolean') {
return value;
}
switch (value) {
case 'false':
return false;
case 'true':
default:
return true;
}
}

View file

@ -10,8 +10,6 @@ import { CoreStart } from 'kibana/public';
import React from 'react';
import { registerDataHandler, unregisterDataHandler } from '../data_handler';
import { useHasData } from '../hooks/use_has_data';
import * as routeParams from '../hooks/use_route_params';
import * as timeRange from '../hooks/use_time_range';
import { HasData, ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data';
import { HasDataContextProvider } from './has_data_context';
import * as pluginContext from '../hooks/use_plugin_context';
@ -21,9 +19,6 @@ import { createMemoryHistory } from 'history';
import { ApmIndicesConfig } from '../../common/typings';
import { act } from '@testing-library/react';
const relativeStart = '2020-10-08T06:00:00.000Z';
const relativeEnd = '2020-10-08T07:00:00.000Z';
const sampleAPMIndices = { transaction: 'apm-*' } as ApmIndicesConfig;
function wrapper({ children }: { children: React.ReactElement }) {
@ -57,19 +52,6 @@ function registerApps<T extends ObservabilityFetchDataPlugins>(
describe('HasDataContextProvider', () => {
beforeAll(() => {
jest.spyOn(routeParams, 'useRouteParams').mockImplementation(() => ({
query: {
from: relativeStart,
to: relativeEnd,
},
path: {},
}));
jest.spyOn(timeRange, 'useTimeRange').mockImplementation(() => ({
relativeStart,
relativeEnd,
absoluteStart: new Date(relativeStart).valueOf(),
absoluteEnd: new Date(relativeEnd).valueOf(),
}));
jest.spyOn(pluginContext, 'usePluginContext').mockReturnValue({
core: { http: { get: jest.fn() } } as unknown as CoreStart,
} as PluginContextValue);

View file

@ -12,7 +12,7 @@ import { asyncForEach } from '@kbn/std';
import { getDataHandler } from '../data_handler';
import { FETCH_STATUS } from '../hooks/use_fetcher';
import { usePluginContext } from '../hooks/use_plugin_context';
import { useTimeRange } from '../hooks/use_time_range';
import { useDatePickerContext } from '../hooks/use_date_picker_context';
import { getObservabilityAlerts } from '../services/get_observability_alerts';
import { ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data';
import { ApmIndicesConfig } from '../../common/typings';
@ -44,7 +44,7 @@ const apps: DataContextApps[] = ['apm', 'synthetics', 'infra_logs', 'infra_metri
export function HasDataContextProvider({ children }: { children: React.ReactNode }) {
const { core } = usePluginContext();
const [forceUpdate, setForceUpdate] = useState('');
const { absoluteStart, absoluteEnd } = useTimeRange();
const { absoluteStart, absoluteEnd } = useDatePickerContext();
const [hasDataMap, setHasDataMap] = useState<HasDataContextValue['hasDataMap']>({});
@ -76,7 +76,7 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode
};
switch (app) {
case 'ux':
const params = { absoluteTime: { start: absoluteStart, end: absoluteEnd } };
const params = { absoluteTime: { start: absoluteStart!, end: absoluteEnd! } };
const resultUx = await getDataHandler(app)?.hasData(params);
updateState({
hasData: resultUx?.hasData,

View file

@ -0,0 +1,13 @@
/*
* 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 { useContext } from 'react';
import { DatePickerContext } from '../context/date_picker_context';
export function useDatePickerContext() {
return useContext(DatePickerContext);
}

View file

@ -1,116 +0,0 @@
/*
* 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 { useTimeRange } from './use_time_range';
import * as pluginContext from './use_plugin_context';
import { AppMountParameters, CoreStart } from 'kibana/public';
import { ObservabilityPublicPluginsStart } from '../plugin';
import * as kibanaUISettings from './use_kibana_ui_settings';
import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock';
jest.mock('react-router-dom', () => ({
useLocation: () => ({
pathname: '/observability/overview/',
search: '',
}),
}));
describe('useTimeRange', () => {
beforeAll(() => {
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
core: {} as CoreStart,
appMountParameters: {} as AppMountParameters,
config: {
unsafe: {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
rules: { enabled: false },
},
},
plugins: {
data: {
query: {
timefilter: {
timefilter: {
getTime: jest.fn().mockImplementation(() => ({
from: '2020-10-08T06:00:00.000Z',
to: '2020-10-08T07:00:00.000Z',
})),
},
},
},
},
} as unknown as ObservabilityPublicPluginsStart,
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
ObservabilityPageTemplate: () => null,
}));
jest.spyOn(kibanaUISettings, 'useKibanaUISettings').mockImplementation(() => ({
from: '2020-10-08T05:00:00.000Z',
to: '2020-10-08T06:00:00.000Z',
}));
});
describe('when range from and to are not provided', () => {
describe('when data plugin has time set', () => {
it('returns ranges and absolute times from data plugin', () => {
const relativeStart = '2020-10-08T06:00:00.000Z';
const relativeEnd = '2020-10-08T07:00:00.000Z';
const timeRange = useTimeRange();
expect(timeRange).toEqual({
relativeStart,
relativeEnd,
absoluteStart: new Date(relativeStart).valueOf(),
absoluteEnd: new Date(relativeEnd).valueOf(),
});
});
});
describe("when data plugin doesn't have time set", () => {
beforeAll(() => {
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
core: {} as CoreStart,
appMountParameters: {} as AppMountParameters,
config: {
unsafe: {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
rules: { enabled: false },
},
},
plugins: {
data: {
query: {
timefilter: {
timefilter: {
getTime: jest.fn().mockImplementation(() => ({
from: undefined,
to: undefined,
})),
},
},
},
},
} as unknown as ObservabilityPublicPluginsStart,
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
ObservabilityPageTemplate: () => null,
}));
});
it('returns ranges and absolute times from kibana default settings', () => {
const relativeStart = '2020-10-08T05:00:00.000Z';
const relativeEnd = '2020-10-08T06:00:00.000Z';
const timeRange = useTimeRange();
expect(timeRange).toEqual({
relativeStart,
relativeEnd,
absoluteStart: new Date(relativeStart).valueOf(),
absoluteEnd: new Date(relativeEnd).valueOf(),
});
});
});
});
});

View file

@ -1,41 +0,0 @@
/*
* 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';
import { useLocation } from 'react-router-dom';
import { TimePickerTimeDefaults } from '../components/shared/date_picker/typings';
import { getAbsoluteTime } from '../utils/date';
import { UI_SETTINGS, useKibanaUISettings } from './use_kibana_ui_settings';
import { usePluginContext } from './use_plugin_context';
const getParsedParams = (search: string) => {
return parse(search.slice(1), { sort: false });
};
export function useTimeRange() {
const { plugins } = usePluginContext();
const timePickerTimeDefaults = useKibanaUISettings<TimePickerTimeDefaults>(
UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS
);
const timePickerSharedState = plugins.data.query.timefilter.timefilter.getTime();
const { rangeFrom, rangeTo } = getParsedParams(useLocation().search);
const relativeStart = (rangeFrom ??
timePickerSharedState.from ??
timePickerTimeDefaults.from) as string;
const relativeEnd = (rangeTo ?? timePickerSharedState.to ?? timePickerTimeDefaults.to) as string;
return {
relativeStart,
relativeEnd,
absoluteStart: getAbsoluteTime(relativeStart)!,
absoluteEnd: getAbsoluteTime(relativeEnd, { roundUp: true })!,
};
}

View file

@ -52,6 +52,7 @@ export const plugin: PluginInitializer<
export * from './components/shared/action_menu/';
export type { UXMetrics } from './components/shared/core_web_vitals/';
export { DatePickerContextProvider } from './context/date_picker_context';
export {
getCoreVitalsComponent,
HeaderMenuPortal,

View file

@ -20,7 +20,6 @@ import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
import { useFetcher } from '../../hooks/use_fetcher';
import { useHasData } from '../../hooks/use_has_data';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { useTimeRange } from '../../hooks/use_time_range';
import { useAlertIndexNames } from '../../hooks/use_alert_index_names';
import { RouteParams } from '../../routes';
import { getNewsFeed } from '../../services/get_news_feed';
@ -32,6 +31,7 @@ import { AlertsTableTGrid } from '../alerts/containers/alerts_table_t_grid/alert
import { SectionContainer } from '../../components/app/section';
import { ObservabilityAppServices } from '../../application/types';
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
import { useDatePickerContext } from '../../hooks/use_date_picker_context';
interface Props {
routeParams: RouteParams<'/overview'>;
}
@ -57,29 +57,22 @@ export function OverviewPage({ routeParams }: Props) {
const { core, ObservabilityPageTemplate } = usePluginContext();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const relativeTime = { start: relativeStart, end: relativeEnd };
const absoluteTime = { start: absoluteStart, end: absoluteEnd };
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd, refreshInterval, refreshPaused } =
useDatePickerContext();
const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]);
const { hasAnyData, isAllRequestsComplete } = useHasData();
const refetch = useRef<() => void>();
const bucketSize = calculateBucketSize({
start: absoluteTime.start,
end: absoluteTime.end,
});
const bucketSizeValue = useMemo(() => {
if (bucketSize?.bucketSize) {
return {
bucketSize: bucketSize.bucketSize,
intervalString: bucketSize.intervalString,
};
}
}, [bucketSize?.bucketSize, bucketSize?.intervalString]);
const bucketSize = useMemo(
() =>
calculateBucketSize({
start: absoluteStart,
end: absoluteEnd,
}),
[absoluteStart, absoluteEnd]
);
const setRefetch = useCallback((ref) => {
refetch.current = ref;
@ -105,8 +98,6 @@ export function OverviewPage({ routeParams }: Props) {
docsLink: core.docLinks.links.observability.guide,
});
const { refreshInterval = 10000, refreshPaused = true } = routeParams.query;
return (
<ObservabilityPageTemplate
noDataConfig={noDataConfig}
@ -116,8 +107,8 @@ export function OverviewPage({ routeParams }: Props) {
pageTitle: overviewPageTitle,
rightSideItems: [
<DatePicker
rangeFrom={relativeTime.start}
rangeTo={relativeTime.end}
rangeFrom={relativeStart}
rangeTo={relativeEnd}
refreshInterval={refreshInterval}
refreshPaused={refreshPaused}
onTimeRangeRefresh={onTimeRangeRefresh}
@ -146,8 +137,8 @@ export function OverviewPage({ routeParams }: Props) {
>
<AlertsTableTGrid
setRefetch={setRefetch}
rangeFrom={relativeTime.start}
rangeTo={relativeTime.end}
rangeFrom={relativeStart}
rangeTo={relativeEnd}
indexNames={indexNames}
/>
</CasesContext>
@ -155,7 +146,7 @@ export function OverviewPage({ routeParams }: Props) {
</EuiFlexItem>
<EuiFlexItem>
{/* Data sections */}
{hasAnyData && <DataSections bucketSize={bucketSizeValue} />}
{hasAnyData && <DataSections bucketSize={bucketSize} />}
<EmptySections />
</EuiFlexItem>
<EuiSpacer size="s" />

View file

@ -12,7 +12,7 @@ import { DatePicker } from '../../components/shared/date_picker';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
import { useHasData } from '../../hooks/use_has_data';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { useTimeRange } from '../../hooks/use_time_range';
import { useDatePickerContext } from '../../hooks/use_date_picker_context';
import { RouteParams } from '../../routes';
import { getNoDataConfig } from '../../utils/no_data_config';
import { LoadingObservability } from './loading_observability';
@ -36,7 +36,7 @@ export function OverviewPage({ routeParams }: Props) {
const { core, ObservabilityPageTemplate } = usePluginContext();
const { relativeStart, relativeEnd } = useTimeRange();
const { relativeStart, relativeEnd } = useDatePickerContext();
const relativeTime = { start: relativeStart, end: relativeEnd };

View file

@ -16,17 +16,10 @@ import moment from 'moment';
import '../../../../../lib/__mocks__/use_composite_image.mock';
import { mockRef } from '../../../../../lib/__mocks__/screenshot_ref.mock';
jest.mock('../../../../../../../observability/public');
mockReduxHooks();
jest.mock('../../../../../../../observability/public', () => {
const originalModule = jest.requireActual('../../../../../../../observability/public');
return {
...originalModule,
useFetcher: jest.fn().mockReturnValue({ data: null, status: 'pending' }),
};
});
describe('Ping Timestamp component', () => {
let checkGroup: string;
let timestamp: string;

View file

@ -12,20 +12,24 @@ import * as observabilityPublic from '../../../../observability/public';
import '../../lib/__mocks__/use_composite_image.mock';
import { mockRef } from '../../lib/__mocks__/screenshot_ref.mock';
jest.mock('../../../../observability/public', () => {
const originalModule = jest.requireActual('../../../../observability/public');
return {
...originalModule,
useFetcher: jest.fn().mockReturnValue({ data: null, status: 'success' }),
};
});
jest.mock('../../../../observability/public');
jest.mock('react-use/lib/useIntersection', () => () => ({
isIntersecting: true,
}));
describe('StepScreenshotDisplayProps', () => {
beforeAll(() => {
jest.spyOn(observabilityPublic, 'useFetcher').mockReturnValue({
data: null,
status: observabilityPublic.FETCH_STATUS.SUCCESS,
refetch: () => {},
});
});
afterAll(() => {
(observabilityPublic.useFetcher as any).mockClear();
});
it('displays screenshot thumbnail when present', () => {
const { getByAltText } = render(
<StepScreenshotDisplay

View file

@ -58,7 +58,19 @@ const mockCorePlugins = {
),
},
},
data: {},
data: {
query: {
timefilter: {
timefilter: {
setTime: jest.fn(),
getTime: jest.fn().mockReturnValue({}),
getTimeDefaults: jest.fn().mockReturnValue({}),
getRefreshIntervalDefaults: jest.fn().mockReturnValue({}),
getRefreshInterval: jest.fn().mockReturnValue({}),
},
},
},
},
};
const coreStart = coreMock.createStart({ basePath: '/basepath' });

View file

@ -34,6 +34,7 @@ import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
import { UXActionMenu } from '../components/app/rum_dashboard/action_menu';
import {
DatePickerContextProvider,
InspectorContextProvider,
useBreadcrumbs,
} from '../../../observability/public';
@ -133,14 +134,16 @@ export function UXAppRoot({
>
<i18nCore.Context>
<RouterProvider history={history} router={uxRouter}>
<InspectorContextProvider>
<UrlParamsProvider>
<EuiErrorBoundary>
<UxApp />
</EuiErrorBoundary>
<UXActionMenu appMountParameters={appMountParameters} />
</UrlParamsProvider>
</InspectorContextProvider>
<DatePickerContextProvider>
<InspectorContextProvider>
<UrlParamsProvider>
<EuiErrorBoundary>
<UxApp />
</EuiErrorBoundary>
<UXActionMenu appMountParameters={appMountParameters} />
</UrlParamsProvider>
</InspectorContextProvider>
</DatePickerContextProvider>
</RouterProvider>
</i18nCore.Context>
</KibanaContextProvider>

View file

@ -9,7 +9,7 @@ import { useMemo } from 'react';
import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params';
export function useUxQuery() {
const { urlParams, uxUiFilters } = useLegacyUrlParams();
const { rangeId, urlParams, uxUiFilters } = useLegacyUrlParams();
const { start, end, searchTerm, percentile } = urlParams;
@ -27,7 +27,10 @@ export function useUxQuery() {
}
return null;
}, [start, end, searchTerm, percentile, uxUiFilters]);
// `rangeId` acts as a cache buster for stable date ranges like `Today`
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [start, end, searchTerm, percentile, uxUiFilters, rangeId]);
return queryParams;
}

View file

@ -32,7 +32,7 @@ interface JSErrorItem {
}
export function JSErrors() {
const { urlParams, uxUiFilters } = useLegacyUrlParams();
const { rangeId, urlParams, uxUiFilters } = useLegacyUrlParams();
const { start, end, serviceName, searchTerm } = urlParams;
@ -56,7 +56,9 @@ export function JSErrors() {
}
return Promise.resolve(null);
},
[start, end, serviceName, uxUiFilters, pagination, searchTerm]
// `rangeId` acts as a cache buster for stable ranges like "Today"
// eslint-disable-next-line react-hooks/exhaustive-deps
[start, end, serviceName, uxUiFilters, pagination, searchTerm, rangeId]
);
const {

View file

@ -32,7 +32,7 @@ export interface PercentileRange {
export function PageLoadDistribution() {
const { http } = useKibanaServices();
const { urlParams, uxUiFilters } = useLegacyUrlParams();
const { rangeId, urlParams, uxUiFilters } = useLegacyUrlParams();
const { start, end, rangeFrom, rangeTo, searchTerm } = urlParams;
@ -67,6 +67,8 @@ export function PageLoadDistribution() {
}
return Promise.resolve(null);
},
// `rangeId` acts as a cache buster for stable ranges like "Today"
// eslint-disable-next-line react-hooks/exhaustive-deps
[
end,
start,
@ -75,6 +77,7 @@ export function PageLoadDistribution() {
percentileRange.max,
searchTerm,
serviceName,
rangeId,
]
);

View file

@ -26,7 +26,7 @@ import { BreakdownItem } from '../../../../../typings/ui_filters';
export function PageViewsTrend() {
const { http } = useKibanaServices();
const { urlParams, uxUiFilters } = useLegacyUrlParams();
const { rangeId, urlParams, uxUiFilters } = useLegacyUrlParams();
const { serviceName } = uxUiFilters;
const { start, end, searchTerm, rangeTo, rangeFrom } = urlParams;
@ -54,7 +54,9 @@ export function PageViewsTrend() {
}
return Promise.resolve(undefined);
},
[start, end, serviceName, uxUiFilters, searchTerm, breakdown]
// `rangeId` acts as a cache buster for stable ranges like "Today"
// eslint-disable-next-line react-hooks/exhaustive-deps
[start, end, serviceName, uxUiFilters, searchTerm, breakdown, rangeId]
);
const exploratoryViewLink = createExploratoryViewUrl(

View file

@ -13,6 +13,7 @@ import { RUM_AGENT_NAMES } from '../../../../../common/agent_name';
export function WebApplicationSelect() {
const {
rangeId,
urlParams: { start, end },
} = useLegacyUrlParams();
@ -30,7 +31,9 @@ export function WebApplicationSelect() {
});
}
},
[start, end]
// `rangeId` works as a cache buster for ranges that never change, like `Today`
// eslint-disable-next-line react-hooks/exhaustive-deps
[start, end, rangeId]
);
const rumServiceNames = data?.rumServices ?? [];

View file

@ -13,7 +13,7 @@ import { useFetcher } from '../../../../hooks/use_fetcher';
import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params';
export function VisitorBreakdown() {
const { urlParams, uxUiFilters } = useLegacyUrlParams();
const { rangeId, urlParams, uxUiFilters } = useLegacyUrlParams();
const { start, end, searchTerm } = urlParams;
@ -35,7 +35,9 @@ export function VisitorBreakdown() {
}
return Promise.resolve(null);
},
[end, start, uxUiFilters, searchTerm]
// `rangeId` acts as a cache buster for stable ranges like "Today"
// eslint-disable-next-line react-hooks/exhaustive-deps
[end, start, uxUiFilters, searchTerm, rangeId]
);
return (

View file

@ -45,7 +45,7 @@ const EmbeddedPanel = styled.div`
`;
export function EmbeddedMapComponent() {
const { urlParams } = useLegacyUrlParams();
const { rangeId, urlParams } = useLegacyUrlParams();
const { start, end, serviceName } = urlParams;
@ -124,7 +124,7 @@ export function EmbeddedMapComponent() {
embeddable.reload();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [start, end]);
}, [start, end, rangeId]);
useEffect(() => {
async function setupEmbeddable() {