[Infra UI] Convert node detail page time range to date strings (#43881) (#44517)

* [Infra UI] Convert node detail page time range to date strings

* remvoing setInterval test

* making time parsing more robust.

* removing extra code

* fixing io-ts import
This commit is contained in:
Chris Cowan 2019-08-30 15:02:13 -07:00 committed by GitHub
parent 2c60eeb8c7
commit 1ce9d993f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 94 additions and 125 deletions

View file

@ -8,11 +8,12 @@ import { EuiPageContentBody, EuiTitle } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { InfraMetricData, InfraTimerangeInput } from '../../graphql/types';
import { InfraMetricData } from '../../graphql/types';
import { InfraMetricLayout, InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
import { NoData } from '../empty_states';
import { InfraLoadingPanel } from '../loading';
import { Section } from './section';
import { MetricsTimeInput } from '../../containers/metrics/with_metrics_time';
interface Props {
metrics: InfraMetricData[];
@ -21,7 +22,7 @@ interface Props {
refetch: () => void;
nodeId: string;
label: string;
onChangeRangeTime?: (time: InfraTimerangeInput) => void;
onChangeRangeTime?: (time: MetricsTimeInput) => void;
isLiveStreaming?: boolean;
stopLiveStreaming?: () => void;
intl: InjectedIntl;

View file

@ -5,14 +5,15 @@
*/
import React from 'react';
import { InfraMetricData, InfraTimerangeInput } from '../../graphql/types';
import { InfraMetricData } from '../../graphql/types';
import { InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
import { sections } from './sections';
import { MetricsTimeInput } from '../../containers/metrics/with_metrics_time';
interface Props {
section: InfraMetricLayoutSection;
metrics: InfraMetricData[];
onChangeRangeTime?: (time: InfraTimerangeInput) => void;
onChangeRangeTime?: (time: MetricsTimeInput) => void;
crosshairValue?: number;
onCrosshairUpdate?: (crosshairValue: number) => void;
isLiveStreaming?: boolean;

View file

@ -18,7 +18,7 @@ import {
} from '@elastic/charts';
import { EuiPageContentBody, EuiTitle } from '@elastic/eui';
import { InfraMetricLayoutSection } from '../../../pages/metrics/layouts/types';
import { InfraMetricData, InfraTimerangeInput } from '../../../graphql/types';
import { InfraMetricData } from '../../../graphql/types';
import { getChartTheme } from '../../metrics_explorer/helpers/get_chart_theme';
import { InfraFormatterType } from '../../../lib/lib';
import { SeriesChart } from './series_chart';
@ -32,11 +32,12 @@ import {
} from './helpers';
import { ErrorMessage } from './error_message';
import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting';
import { MetricsTimeInput } from '../../../containers/metrics/with_metrics_time';
interface Props {
section: InfraMetricLayoutSection;
metric: InfraMetricData;
onChangeRangeTime?: (time: InfraTimerangeInput) => void;
onChangeRangeTime?: (time: MetricsTimeInput) => void;
isLiveStreaming?: boolean;
stopLiveStreaming?: () => void;
intl: InjectedIntl;
@ -60,8 +61,8 @@ export const ChartSection = injectI18n(
stopLiveStreaming();
}
onChangeRangeTime({
from,
to,
from: moment(from).toISOString(),
to: moment(to).toISOString(),
interval: '>=1m',
});
}

View file

@ -7,28 +7,26 @@
import React from 'react';
import { MetricsTimeControls } from './time_controls';
import { mount } from 'enzyme';
import moment from 'moment';
import { InfraTimerangeInput } from '../../graphql/types';
import DateMath from '@elastic/datemath';
import { MetricsTimeInput } from '../../containers/metrics/with_metrics_time';
describe('MetricsTimeControls', () => {
it('should set a valid from and to value for Today', () => {
const currentTimeRange = {
from: moment()
.subtract(15, 'm')
.valueOf(),
to: moment().valueOf(),
from: 'now-15m',
to: 'now',
interval: '>=1m',
};
const handleTimeChange = jest.fn().mockImplementation((time: InfraTimerangeInput) => void 0);
const handleTimeChange = jest.fn().mockImplementation((time: MetricsTimeInput) => void 0);
const handleRefreshChange = jest.fn().mockImplementation((refreshInterval: number) => void 0);
const handleAutoReload = jest.fn().mockImplementation((isAutoReloading: boolean) => void 0);
const handleOnRefresh = jest.fn().mockImplementation(() => void 0);
const component = mount(
<MetricsTimeControls
currentTimeRange={currentTimeRange}
onChangeTimeRange={handleTimeChange}
setRefreshInterval={handleRefreshChange}
setAutoReload={handleAutoReload}
onRefresh={handleOnRefresh}
/>
);
component
@ -41,12 +39,7 @@ describe('MetricsTimeControls', () => {
.simulate('click');
expect(handleTimeChange.mock.calls.length).toBe(1);
const timeRangeInput = handleTimeChange.mock.calls[0][0];
const expectedFrom = DateMath.parse('now/d');
const expectedTo = DateMath.parse('now/d', { roundUp: true });
if (!expectedFrom || !expectedTo) {
throw new Error('This should never happen!');
}
expect(timeRangeInput.from).toBe(expectedFrom.valueOf());
expect(timeRangeInput.to).toBe(expectedTo.valueOf());
expect(timeRangeInput.from).toBe('now/d');
expect(timeRangeInput.to).toBe('now/d');
});
});

View file

@ -4,22 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import dateMath from '@elastic/datemath';
import { EuiSuperDatePicker, OnRefreshChangeProps, OnTimeChangeProps } from '@elastic/eui';
import moment from 'moment';
import React from 'react';
import euiStyled from '../../../../../common/eui_styled_components';
import { InfraTimerangeInput } from '../../graphql/types';
const EuiSuperDatePickerAbsoluteFormat = 'YYYY-MM-DDTHH:mm:ss.sssZ';
import { MetricsTimeInput } from '../../containers/metrics/with_metrics_time';
interface MetricsTimeControlsProps {
currentTimeRange: InfraTimerangeInput;
currentTimeRange: MetricsTimeInput;
isLiveStreaming?: boolean;
refreshInterval?: number | null;
onChangeTimeRange: (time: InfraTimerangeInput) => void;
onChangeTimeRange: (time: MetricsTimeInput) => void;
setRefreshInterval: (refreshInterval: number) => void;
setAutoReload: (isAutoReloading: boolean) => void;
onRefresh: () => void;
}
export class MetricsTimeControls extends React.Component<MetricsTimeControlsProps> {
@ -28,28 +25,24 @@ export class MetricsTimeControls extends React.Component<MetricsTimeControlsProp
return (
<MetricsTimeControlsContainer>
<EuiSuperDatePicker
start={moment(currentTimeRange.from).format(EuiSuperDatePickerAbsoluteFormat)}
end={moment(currentTimeRange.to).format(EuiSuperDatePickerAbsoluteFormat)}
start={currentTimeRange.from}
end={currentTimeRange.to}
isPaused={!isLiveStreaming}
refreshInterval={refreshInterval ? refreshInterval : 0}
onTimeChange={this.handleTimeChange}
onRefreshChange={this.handleRefreshChange}
onRefresh={this.props.onRefresh}
/>
</MetricsTimeControlsContainer>
);
}
private handleTimeChange = ({ start, end }: OnTimeChangeProps) => {
const parsedStart = dateMath.parse(start);
const parsedEnd = dateMath.parse(end, { roundUp: true });
if (parsedStart && parsedEnd) {
this.props.onChangeTimeRange({
from: parsedStart.valueOf(),
to: parsedEnd.valueOf(),
interval: '>=1m',
});
}
this.props.onChangeTimeRange({
from: start,
to: end,
interval: '>=1m',
});
};
private handleRefreshChange = ({ isPaused, refreshInterval }: OnRefreshChangeProps) => {

View file

@ -22,8 +22,8 @@ describe('useMetricsTime hook', () => {
const { act, getLastHookValue } = mountHook(() => useMetricsTime());
const timeRange = {
from: 12345,
to: 123456,
from: 'now-15m',
to: 'now',
interval: '>=2m',
};
@ -36,10 +36,6 @@ describe('useMetricsTime hook', () => {
});
describe('AutoReloading state', () => {
beforeEach(() => {
jest.useFakeTimers();
});
it('has a default value', () => {
const { getLastHookValue } = mountHook(() => useMetricsTime().isAutoReloading);
expect(getLastHookValue()).toBe(false);
@ -54,49 +50,5 @@ describe('useMetricsTime hook', () => {
expect(getLastHookValue().isAutoReloading).toBe(true);
});
it('sets up an interval when turned on', () => {
const { act } = mountHook(() => useMetricsTime());
const refreshInterval = 10000;
act(({ setAutoReload, setRefreshInterval }) => {
setRefreshInterval(refreshInterval);
setAutoReload(true);
jest.runOnlyPendingTimers();
});
expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), refreshInterval);
});
it('updates the time range by RANGE each interval', () => {
const { act, getLastHookValue } = mountHook(() => useMetricsTime());
const from = 100;
const to = 300;
const RANGE = 200;
act(({ setAutoReload, setTimeRange }) => {
setAutoReload(true);
setTimeRange({
from,
to,
interval: '>=1m',
});
});
act(() => {
jest.advanceTimersByTime(6000);
});
const timeRange = getLastHookValue().timeRange;
expect(timeRange.from).toBeGreaterThan(from);
expect(timeRange.to).toBeGreaterThan(to);
const newRange = timeRange.to - timeRange.from;
// The following two assertions allow 5ms of leniency, rather than expect(newRange).toBe(RANGE),
// due to failures in CI that don't happen locally.
expect(newRange).toBeGreaterThanOrEqual(RANGE);
expect(newRange).toBeLessThanOrEqual(RANGE + 5);
});
});
});

View file

@ -10,8 +10,8 @@ import {
InfraMetric,
InfraMetricData,
InfraNodeType,
InfraTimerangeInput,
MetricsQuery,
InfraTimerangeInput,
} from '../../graphql/types';
import { InfraMetricLayout } from '../../pages/metrics/layouts/types';
import { metricsQuery } from './metrics.gql_query';

View file

@ -5,57 +5,67 @@
*/
import createContainer from 'constate-latest';
import React, { useContext, useState, useMemo, useCallback } from 'react';
import { isNumber } from 'lodash';
import moment from 'moment';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { InfraTimerangeInput } from '../../graphql/types';
import { useInterval } from '../../hooks/use_interval';
import dateMath from '@elastic/datemath';
import * as rt from 'io-ts';
import { replaceStateKeyInQueryString, UrlStateContainer } from '../../utils/url_state';
import { InfraTimerangeInput } from '../../graphql/types';
export interface MetricsTimeInput {
from: string;
to: string;
interval: string;
}
interface MetricsTimeState {
timeRange: InfraTimerangeInput;
setTimeRange: (timeRange: InfraTimerangeInput) => void;
timeRange: MetricsTimeInput;
parsedTimeRange: InfraTimerangeInput;
setTimeRange: (timeRange: MetricsTimeInput) => void;
refreshInterval: number;
setRefreshInterval: (refreshInterval: number) => void;
isAutoReloading: boolean;
setAutoReload: (isAutoReloading: boolean) => void;
lastRefresh: number;
triggerRefresh: () => void;
}
export const useMetricsTime = () => {
const [isAutoReloading, setAutoReload] = useState(false);
const [refreshInterval, setRefreshInterval] = useState(5000);
const [lastRefresh, setLastRefresh] = useState<number>(moment().valueOf());
const [timeRange, setTimeRange] = useState({
from: moment()
.subtract(1, 'hour')
.valueOf(),
to: moment().valueOf(),
from: 'now-1h',
to: 'now',
interval: '>=1m',
});
const setTimeRangeToNow = useCallback(() => {
const range = timeRange.to - timeRange.from;
const nowInMs = moment().valueOf();
setTimeRange({
from: nowInMs - range,
to: nowInMs,
interval: '>=1m',
});
}, [timeRange.from, timeRange.to]);
useInterval(setTimeRangeToNow, isAutoReloading ? refreshInterval : null);
useEffect(() => {
if (isAutoReloading) {
setTimeRangeToNow();
}
}, [isAutoReloading]);
const parsedFrom = dateMath.parse(timeRange.from);
const parsedTo = dateMath.parse(timeRange.to, { roundUp: true });
const parsedTimeRange = useMemo(
() => ({
...timeRange,
from:
(parsedFrom && parsedFrom.valueOf()) ||
moment()
.subtract(1, 'hour')
.valueOf(),
to: (parsedTo && parsedTo.valueOf()) || moment().valueOf(),
}),
[parsedFrom, parsedTo, lastRefresh]
);
return {
timeRange,
setTimeRange,
parsedTimeRange,
refreshInterval,
setRefreshInterval,
isAutoReloading,
setAutoReload,
lastRefresh,
triggerRefresh: useCallback(() => setLastRefresh(moment().valueOf()), [setLastRefresh]),
};
};
@ -141,8 +151,23 @@ const mapToUrlState = (value: any): MetricsTimeUrlState | undefined =>
}
: undefined;
const mapToTimeUrlState = (value: any) =>
value && (typeof value.to === 'number' && typeof value.from === 'number') ? value : undefined;
const MetricsTimeRT = rt.type({
from: rt.union([rt.string, rt.number]),
to: rt.union([rt.string, rt.number]),
interval: rt.string,
});
const mapToTimeUrlState = (value: any) => {
const result = MetricsTimeRT.decode(value);
if (result.isRight()) {
const to = isNumber(result.value.to) ? moment(result.value.to).toISOString() : result.value.to;
const from = isNumber(result.value.from)
? moment(result.value.from).toISOString()
: result.value.from;
return { ...result.value, from, to };
}
return undefined;
};
const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined);
@ -155,7 +180,7 @@ export const replaceMetricTimeInQueryString = (from: number, to: number) =>
autoReload: false,
time: {
interval: '>=1m',
from,
to,
from: moment(from).toISOString(),
to: moment(to).toISOString(),
},
});

View file

@ -33,7 +33,7 @@ import {
WithMetricsTime,
WithMetricsTimeUrlState,
} from '../../containers/metrics/with_metrics_time';
import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types';
import { InfraNodeType } from '../../graphql/types';
import { Error, ErrorPageBody } from '../error';
import { layoutCreators } from './layouts';
import { InfraMetricLayoutSection } from './layouts/types';
@ -132,11 +132,13 @@ export const MetricDetail = withMetricPageProviders(
<WithMetricsTime>
{({
timeRange,
parsedTimeRange,
setTimeRange,
refreshInterval,
setRefreshInterval,
isAutoReloading,
setAutoReload,
triggerRefresh,
}) => (
<ColumnarPage>
<Header
@ -159,7 +161,7 @@ export const MetricDetail = withMetricPageProviders(
<WithMetrics
layouts={filteredLayouts}
sourceId={sourceId}
timerange={timeRange as InfraTimerangeInput}
timerange={parsedTimeRange}
nodeType={nodeType}
nodeId={nodeId}
cloudId={cloudId}
@ -223,6 +225,7 @@ export const MetricDetail = withMetricPageProviders(
setRefreshInterval={setRefreshInterval}
onChangeTimeRange={setTimeRange}
setAutoReload={setAutoReload}
onRefresh={triggerRefresh}
/>
</MetricsTitleTimeRangeContainer>
</EuiPageHeaderSection>