mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: adding a time picker for selecting relative time. (#33689)
* adding placeholder for relative time range. * fixed story. * added basic structure to handle open/close of time range picker. * removed section from TimeOptions since it isn't used any where. * adding mapper and tests * move relativetimepicker to its own dir * added some simple tests. * changed test. * use relativetimerangeinput * redo state management * refactored the tests. * replace timerange with relativetimerange * wip * wip * did some refactoring. * refactored time option formatting. * added proper formatting and display of time range. * add relative time description, slight refactor of height * fixed incorrect import. * added validator and changed formatting. * removed unused dep. * reverted back to internal function. * fixed display of relative time range picker. * fixed failing tests. * fixed parsing issue. * fixed position of time range picker. * some more refactorings. * fixed validation of really big values. * added another test. Co-authored-by: Peter Holmberg <peter.hlmbrg@gmail.com>
This commit is contained in:
parent
2459a0ceb5
commit
07ef4060a3
@ -1,6 +1,6 @@
|
||||
import { each, groupBy, has } from 'lodash';
|
||||
import { each, has } from 'lodash';
|
||||
|
||||
import { RawTimeRange, TimeRange, TimeZone, IntervalValues, RelativeTimeRange } from '../types/time';
|
||||
import { RawTimeRange, TimeRange, TimeZone, IntervalValues, RelativeTimeRange, TimeOption } from '../types/time';
|
||||
|
||||
import * as dateMath from './datemath';
|
||||
import { isDateTime, DateTime, dateTime } from './moment_wrapper';
|
||||
@ -17,69 +17,67 @@ const spans: { [key: string]: { display: string; section?: number } } = {
|
||||
y: { display: 'year' },
|
||||
};
|
||||
|
||||
const rangeOptions = [
|
||||
{ from: 'now/d', to: 'now/d', display: 'Today', section: 2 },
|
||||
{ from: 'now/d', to: 'now', display: 'Today so far', section: 2 },
|
||||
{ from: 'now/w', to: 'now/w', display: 'This week', section: 2 },
|
||||
{ from: 'now/w', to: 'now', display: 'This week so far', section: 2 },
|
||||
{ from: 'now/M', to: 'now/M', display: 'This month', section: 2 },
|
||||
{ from: 'now/M', to: 'now', display: 'This month so far', section: 2 },
|
||||
{ from: 'now/y', to: 'now/y', display: 'This year', section: 2 },
|
||||
{ from: 'now/y', to: 'now', display: 'This year so far', section: 2 },
|
||||
const rangeOptions: TimeOption[] = [
|
||||
{ from: 'now/d', to: 'now/d', display: 'Today' },
|
||||
{ from: 'now/d', to: 'now', display: 'Today so far' },
|
||||
{ from: 'now/w', to: 'now/w', display: 'This week' },
|
||||
{ from: 'now/w', to: 'now', display: 'This week so far' },
|
||||
{ from: 'now/M', to: 'now/M', display: 'This month' },
|
||||
{ from: 'now/M', to: 'now', display: 'This month so far' },
|
||||
{ from: 'now/y', to: 'now/y', display: 'This year' },
|
||||
{ from: 'now/y', to: 'now', display: 'This year so far' },
|
||||
|
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday', section: 1 },
|
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday' },
|
||||
{
|
||||
from: 'now-2d/d',
|
||||
to: 'now-2d/d',
|
||||
display: 'Day before yesterday',
|
||||
section: 1,
|
||||
},
|
||||
{
|
||||
from: 'now-7d/d',
|
||||
to: 'now-7d/d',
|
||||
display: 'This day last week',
|
||||
section: 1,
|
||||
},
|
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week', section: 1 },
|
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month', section: 1 },
|
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year', section: 1 },
|
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week' },
|
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month' },
|
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year' },
|
||||
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 },
|
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3 },
|
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3 },
|
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 },
|
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 },
|
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 },
|
||||
{ from: 'now-2d', to: 'now', display: 'Last 2 days', section: 0 },
|
||||
{ from: 'now-7d', to: 'now', display: 'Last 7 days', section: 0 },
|
||||
{ from: 'now-30d', to: 'now', display: 'Last 30 days', section: 0 },
|
||||
{ from: 'now-90d', to: 'now', display: 'Last 90 days', section: 0 },
|
||||
{ from: 'now-6M', to: 'now', display: 'Last 6 months', section: 0 },
|
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year', section: 0 },
|
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years', section: 0 },
|
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 },
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes' },
|
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour' },
|
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours' },
|
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours' },
|
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours' },
|
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours' },
|
||||
{ from: 'now-2d', to: 'now', display: 'Last 2 days' },
|
||||
{ from: 'now-7d', to: 'now', display: 'Last 7 days' },
|
||||
{ from: 'now-30d', to: 'now', display: 'Last 30 days' },
|
||||
{ from: 'now-90d', to: 'now', display: 'Last 90 days' },
|
||||
{ from: 'now-6M', to: 'now', display: 'Last 6 months' },
|
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year' },
|
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
|
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years' },
|
||||
];
|
||||
|
||||
const hiddenRangeOptions = [
|
||||
{ from: 'now', to: 'now+1m', display: 'Next minute', section: 3 },
|
||||
{ from: 'now', to: 'now+5m', display: 'Next 5 minutes', section: 3 },
|
||||
{ from: 'now', to: 'now+15m', display: 'Next 15 minutes', section: 3 },
|
||||
{ from: 'now', to: 'now+30m', display: 'Next 30 minutes', section: 3 },
|
||||
{ from: 'now', to: 'now+1h', display: 'Next hour', section: 3 },
|
||||
{ from: 'now', to: 'now+3h', display: 'Next 3 hours', section: 3 },
|
||||
{ from: 'now', to: 'now+6h', display: 'Next 6 hours', section: 3 },
|
||||
{ from: 'now', to: 'now+12h', display: 'Next 12 hours', section: 3 },
|
||||
{ from: 'now', to: 'now+24h', display: 'Next 24 hours', section: 3 },
|
||||
{ from: 'now', to: 'now+2d', display: 'Next 2 days', section: 0 },
|
||||
{ from: 'now', to: 'now+7d', display: 'Next 7 days', section: 0 },
|
||||
{ from: 'now', to: 'now+30d', display: 'Next 30 days', section: 0 },
|
||||
{ from: 'now', to: 'now+90d', display: 'Next 90 days', section: 0 },
|
||||
{ from: 'now', to: 'now+6M', display: 'Next 6 months', section: 0 },
|
||||
{ from: 'now', to: 'now+1y', display: 'Next year', section: 0 },
|
||||
{ from: 'now', to: 'now+2y', display: 'Next 2 years', section: 0 },
|
||||
{ from: 'now', to: 'now+5y', display: 'Next 5 years', section: 0 },
|
||||
const hiddenRangeOptions: TimeOption[] = [
|
||||
{ from: 'now', to: 'now+1m', display: 'Next minute' },
|
||||
{ from: 'now', to: 'now+5m', display: 'Next 5 minutes' },
|
||||
{ from: 'now', to: 'now+15m', display: 'Next 15 minutes' },
|
||||
{ from: 'now', to: 'now+30m', display: 'Next 30 minutes' },
|
||||
{ from: 'now', to: 'now+1h', display: 'Next hour' },
|
||||
{ from: 'now', to: 'now+3h', display: 'Next 3 hours' },
|
||||
{ from: 'now', to: 'now+6h', display: 'Next 6 hours' },
|
||||
{ from: 'now', to: 'now+12h', display: 'Next 12 hours' },
|
||||
{ from: 'now', to: 'now+24h', display: 'Next 24 hours' },
|
||||
{ from: 'now', to: 'now+2d', display: 'Next 2 days' },
|
||||
{ from: 'now', to: 'now+7d', display: 'Next 7 days' },
|
||||
{ from: 'now', to: 'now+30d', display: 'Next 30 days' },
|
||||
{ from: 'now', to: 'now+90d', display: 'Next 90 days' },
|
||||
{ from: 'now', to: 'now+6M', display: 'Next 6 months' },
|
||||
{ from: 'now', to: 'now+1y', display: 'Next year' },
|
||||
{ from: 'now', to: 'now+2y', display: 'Next 2 years' },
|
||||
{ from: 'now', to: 'now+5y', display: 'Next 5 years' },
|
||||
];
|
||||
|
||||
const rangeIndex: any = {};
|
||||
@ -90,22 +88,6 @@ each(hiddenRangeOptions, (frame: any) => {
|
||||
rangeIndex[frame.from + ' to ' + frame.to] = frame;
|
||||
});
|
||||
|
||||
export function getRelativeTimesList(timepickerSettings: any, currentDisplay: any) {
|
||||
const groups = groupBy(rangeOptions, (option: any) => {
|
||||
option.active = option.display === currentDisplay;
|
||||
return option.section;
|
||||
});
|
||||
|
||||
// _.each(timepickerSettings.time_options, (duration: string) => {
|
||||
// let info = describeTextRange(duration);
|
||||
// if (info.section) {
|
||||
// groups[info.section].push(info);
|
||||
// }
|
||||
// });
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
// handles expressions like
|
||||
// 5m
|
||||
// 5m to now/d
|
||||
|
@ -40,7 +40,6 @@ export interface TimeOption {
|
||||
from: string;
|
||||
to: string;
|
||||
display: string;
|
||||
section: number;
|
||||
}
|
||||
|
||||
export interface TimeOptions {
|
||||
|
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { UseState } from '../../../utils/storybook/UseState';
|
||||
import { withCenteredStory } from '../../../utils/storybook/withCenteredStory';
|
||||
import { RelativeTimeRangePicker } from './RelativeTimeRangePicker';
|
||||
|
||||
export default {
|
||||
title: 'Pickers and Editors/TimePickers/RelativeTimeRangePicker',
|
||||
component: RelativeTimeRangePicker,
|
||||
decorators: [withCenteredStory],
|
||||
parameters: {
|
||||
docs: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const basic = () => {
|
||||
return (
|
||||
<UseState
|
||||
initialState={{
|
||||
from: 900,
|
||||
to: 0,
|
||||
}}
|
||||
>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<RelativeTimeRangePicker
|
||||
onChange={(newValue) => {
|
||||
action('on selected')(newValue);
|
||||
updateValue(newValue);
|
||||
}}
|
||||
timeRange={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import React, { useState } from 'react';
|
||||
import { render, fireEvent, RenderResult } from '@testing-library/react';
|
||||
import { RelativeTimeRangePicker } from './RelativeTimeRangePicker';
|
||||
import { RelativeTimeRange } from '@grafana/data';
|
||||
|
||||
function setup(initial: RelativeTimeRange = { from: 900, to: 0 }): RenderResult {
|
||||
const StatefulPicker: React.FC<{}> = () => {
|
||||
const [value, setValue] = useState<RelativeTimeRange>(initial);
|
||||
return <RelativeTimeRangePicker timeRange={value} onChange={setValue} />;
|
||||
};
|
||||
|
||||
return render(<StatefulPicker />);
|
||||
}
|
||||
|
||||
describe('RelativeTimePicker', () => {
|
||||
it('should render the picker button with an user friendly text', () => {
|
||||
const { getByText } = setup({ from: 900, to: 0 });
|
||||
expect(getByText('now-15m to now')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open the picker when clicking the button', () => {
|
||||
const { getByText } = setup({ from: 900, to: 0 });
|
||||
|
||||
fireEvent.click(getByText('now-15m to now'));
|
||||
|
||||
expect(getByText('Specify time range')).toBeInTheDocument();
|
||||
expect(getByText('Example time ranges')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not have open picker without clicking the button', () => {
|
||||
const { queryByText } = setup({ from: 900, to: 0 });
|
||||
expect(queryByText('Specify time range')).toBeNull();
|
||||
expect(queryByText('Example time ranges')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not be able to apply range via quick options', () => {
|
||||
const { getByText, queryByText } = setup({ from: 900, to: 0 });
|
||||
|
||||
fireEvent.click(getByText('now-15m to now')); // open the picker
|
||||
fireEvent.click(getByText('Last 30 minutes')); // select the quick range, should close picker.
|
||||
|
||||
expect(queryByText('Specify time range')).toBeNull();
|
||||
expect(queryByText('Example time ranges')).toBeNull();
|
||||
|
||||
expect(getByText('now-30m to now')).toBeInTheDocument(); // new text on picker button
|
||||
});
|
||||
});
|
@ -0,0 +1,191 @@
|
||||
import React, { FormEvent, ReactElement, useCallback, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { RelativeTimeRange, GrafanaTheme2, TimeOption } from '@grafana/data';
|
||||
import { Tooltip } from '../../Tooltip/Tooltip';
|
||||
import { useStyles2 } from '../../../themes';
|
||||
import { Button, ButtonGroup, ToolbarButton } from '../../Button';
|
||||
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
import { TimeRangeList } from '../TimeRangePicker/TimeRangeList';
|
||||
import { quickOptions } from '../rangeOptions';
|
||||
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
|
||||
import { TimePickerTitle } from '../TimeRangePicker/TimePickerTitle';
|
||||
import { isRangeValid, isRelativeFormat, mapOptionToRelativeTimeRange, mapRelativeTimeRangeToOption } from './utils';
|
||||
import { Field } from '../../Forms/Field';
|
||||
import { Input } from '../../Input/Input';
|
||||
import { InputState } from '../TimeRangePicker/TimeRangeForm';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface RelativeTimeRangePickerProps {
|
||||
timeRange: RelativeTimeRange;
|
||||
onChange: (timeRange: RelativeTimeRange) => void;
|
||||
}
|
||||
|
||||
const errorMessage = 'Value not in relative time format.';
|
||||
const validOptions = quickOptions.filter((o) => isRelativeFormat(o.from));
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps): ReactElement | null {
|
||||
const { timeRange, onChange } = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const onClose = useCallback(() => setIsOpen(false), []);
|
||||
const timeOption = mapRelativeTimeRangeToOption(timeRange);
|
||||
const [from, setFrom] = useState<InputState>({ value: timeOption.from, invalid: !isRangeValid(timeOption.from) });
|
||||
const [to, setTo] = useState<InputState>({ value: timeOption.to, invalid: !isRangeValid(timeOption.to) });
|
||||
|
||||
const styles = useStyles2(getStyles(from.invalid, to.invalid));
|
||||
|
||||
const onChangeTimeOption = (option: TimeOption) => {
|
||||
const relativeTimeRange = mapOptionToRelativeTimeRange(option);
|
||||
if (!relativeTimeRange) {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
setFrom({ ...from, value: option.from });
|
||||
setTo({ ...to, value: option.to });
|
||||
onChange(relativeTimeRange);
|
||||
};
|
||||
|
||||
const onOpen = useCallback(
|
||||
(event: FormEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setIsOpen(!isOpen);
|
||||
},
|
||||
[isOpen]
|
||||
);
|
||||
|
||||
const onApply = (event: FormEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (to.invalid || from.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeRange = mapOptionToRelativeTimeRange({
|
||||
from: from.value,
|
||||
to: to.value,
|
||||
display: '',
|
||||
});
|
||||
|
||||
if (!timeRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(timeRange);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ButtonGroup className={styles.container}>
|
||||
<Tooltip content="Choose time range" placement="bottom">
|
||||
<ToolbarButton aria-label="TimePicker Open Button" onClick={onOpen} icon="clock-nine" isOpen={isOpen}>
|
||||
<span data-testid="picker-button-label" className={styles.container}>
|
||||
{timeOption.from} to {timeOption.to}
|
||||
</span>
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{isOpen && (
|
||||
<ClickOutsideWrapper includeButtonPress={false} onClick={onClose}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.body}>
|
||||
<CustomScrollbar className={styles.leftSide} hideHorizontalTrack>
|
||||
<TimeRangeList
|
||||
title="Example time ranges"
|
||||
options={validOptions}
|
||||
onChange={onChangeTimeOption}
|
||||
value={timeOption}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
<div className={styles.rightSide}>
|
||||
<div className={styles.title}>
|
||||
<TimePickerTitle>Specify time range</TimePickerTitle>
|
||||
<div className={styles.description}>
|
||||
Specify a relative time range, for more information see{' '}
|
||||
<a href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/">
|
||||
docs <Icon name="external-link-alt" />
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<Field label="From" invalid={from.invalid} error={errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onBlur={() => setFrom({ ...from, invalid: !isRangeValid(from.value) })}
|
||||
onChange={(event) => setFrom({ ...from, value: event.currentTarget.value })}
|
||||
value={from.value}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="To" invalid={to.invalid} error={errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onBlur={() => setTo({ ...to, invalid: !isRangeValid(to.value) })}
|
||||
onChange={(event) => setTo({ ...to, value: event.currentTarget.value })}
|
||||
value={to.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button aria-label="TimePicker submit button" onClick={onApply}>
|
||||
Apply time range
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClickOutsideWrapper>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (fromInvalid: boolean, toInvalid: boolean) => (theme: GrafanaTheme2) => {
|
||||
let bodyHeight = 250;
|
||||
const errorHeight = theme.spacing.gridSize * 4;
|
||||
|
||||
if (fromInvalid && toInvalid) {
|
||||
bodyHeight += errorHeight * 2;
|
||||
} else if (fromInvalid || toInvalid) {
|
||||
bodyHeight += errorHeight;
|
||||
}
|
||||
|
||||
return {
|
||||
container: css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
vertical-align: middle;
|
||||
`,
|
||||
content: css`
|
||||
background: ${theme.colors.background.primary};
|
||||
box-shadow: ${theme.shadows.z3};
|
||||
position: absolute;
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
width: 500px;
|
||||
top: 116%;
|
||||
border-radius: 2px;
|
||||
border: 1px solid ${theme.colors.border.weak};
|
||||
left: 0;
|
||||
white-space: normal;
|
||||
`,
|
||||
body: css`
|
||||
display: flex;
|
||||
height: ${bodyHeight}px;
|
||||
`,
|
||||
description: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
leftSide: css`
|
||||
width: 50% !important;
|
||||
border-right: 1px solid ${theme.colors.border.medium};
|
||||
`,
|
||||
rightSide: css`
|
||||
width: 50%;
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
title: css`
|
||||
margin-bottom: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
};
|
@ -0,0 +1,108 @@
|
||||
import { isRangeValid, isRelativeFormat, mapOptionToRelativeTimeRange, mapRelativeTimeRangeToOption } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('mapRelativeTimeRangeToOption', () => {
|
||||
it('should map relative time range from minutes to time option', () => {
|
||||
const relativeTimeRange = { from: 600, to: 0 };
|
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange);
|
||||
|
||||
expect(timeOption).toEqual({ from: 'now-10m', to: 'now', display: 'now-10m to now' });
|
||||
});
|
||||
|
||||
it('should map relative time range from one hour to time option', () => {
|
||||
const relativeTimeRange = { from: 3600, to: 0 };
|
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange);
|
||||
|
||||
expect(timeOption).toEqual({ from: 'now-1h', to: 'now', display: 'now-1h to now' });
|
||||
});
|
||||
|
||||
it('should map relative time range from hours to time option', () => {
|
||||
const relativeTimeRange = { from: 7200, to: 0 };
|
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange);
|
||||
|
||||
expect(timeOption).toEqual({ from: 'now-2h', to: 'now', display: 'now-2h to now' });
|
||||
});
|
||||
|
||||
it('should handle two relative ranges', () => {
|
||||
const relativeTimeRange = { from: 600, to: 300 };
|
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange);
|
||||
|
||||
expect(timeOption).toEqual({ from: 'now-10m', to: 'now-5m', display: 'now-10m to now-5m' });
|
||||
});
|
||||
|
||||
it('should handle two relative ranges with single/multiple units', () => {
|
||||
const relativeTimeRange = { from: 6000, to: 300 };
|
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange);
|
||||
|
||||
expect(timeOption).toEqual({
|
||||
from: 'now-100m',
|
||||
to: 'now-5m',
|
||||
display: 'now-100m to now-5m',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapOptionToRelativeTimeRange', () => {
|
||||
it('should map simple case', () => {
|
||||
const timeOption = { from: 'now-10m', to: 'now', display: 'asdfasdf' };
|
||||
const relativeTimeRange = mapOptionToRelativeTimeRange(timeOption);
|
||||
|
||||
expect(relativeTimeRange).toEqual({ from: 600, to: 0 });
|
||||
});
|
||||
|
||||
it('should map advanced case', () => {
|
||||
const timeOption = { from: 'now-1d', to: 'now-12h', display: 'asdfasdf' };
|
||||
const relativeTimeRange = mapOptionToRelativeTimeRange(timeOption);
|
||||
|
||||
expect(relativeTimeRange).toEqual({ from: 86400, to: 43200 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRelativeFormat', () => {
|
||||
it('should consider now as a relative format', () => {
|
||||
expect(isRelativeFormat('now')).toBe(true);
|
||||
});
|
||||
|
||||
it('should consider now-10s as a relative format', () => {
|
||||
expect(isRelativeFormat('now-10s')).toBe(true);
|
||||
});
|
||||
|
||||
it('should consider now-2000m as a relative format', () => {
|
||||
expect(isRelativeFormat('now-2000m')).toBe(true);
|
||||
});
|
||||
|
||||
it('should consider now-112334h as a relative format', () => {
|
||||
expect(isRelativeFormat('now-112334h')).toBe(true);
|
||||
});
|
||||
|
||||
it('should consider now-12d as a relative format', () => {
|
||||
expect(isRelativeFormat('now-12d')).toBe(true);
|
||||
});
|
||||
|
||||
it('should consider now-53w as a relative format', () => {
|
||||
expect(isRelativeFormat('now-53w')).toBe(true);
|
||||
});
|
||||
|
||||
it('should consider 123123123 as a relative format', () => {
|
||||
expect(isRelativeFormat('123123123')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRangeValid', () => {
|
||||
it('should consider now as a valid relative format', () => {
|
||||
expect(isRangeValid('now')).toBe(true);
|
||||
});
|
||||
|
||||
it('should consider now-90d as a valid relative format', () => {
|
||||
expect(isRangeValid('now-90d')).toBe(true);
|
||||
});
|
||||
|
||||
it('should consider now-90000000d as an invalid relative format', () => {
|
||||
expect(isRangeValid('now-90000000d')).toBe(false);
|
||||
});
|
||||
|
||||
it('should consider now-11111111111s as an invalid relative format', () => {
|
||||
expect(isRangeValid('now-11111111111s')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,83 @@
|
||||
import { RelativeTimeRange, TimeOption } from '@grafana/data';
|
||||
|
||||
const regex = /^now$|^now\-(\d{1,10})([wdhms])$/;
|
||||
|
||||
export const mapOptionToRelativeTimeRange = (option: TimeOption): RelativeTimeRange | undefined => {
|
||||
return {
|
||||
from: relativeToSeconds(option.from),
|
||||
to: relativeToSeconds(option.to),
|
||||
};
|
||||
};
|
||||
|
||||
export const mapRelativeTimeRangeToOption = (range: RelativeTimeRange): TimeOption => {
|
||||
const from = secondsToRelativeFormat(range.from);
|
||||
const to = secondsToRelativeFormat(range.to);
|
||||
|
||||
return { from, to, display: `${from} to ${to}` };
|
||||
};
|
||||
|
||||
export const isRangeValid = (relative: string, now = Date.now()): boolean => {
|
||||
if (!isRelativeFormat(relative)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const seconds = relativeToSeconds(relative);
|
||||
|
||||
if (seconds > Math.ceil(now / 1000)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isRelativeFormat = (format: string): boolean => {
|
||||
return regex.test(format);
|
||||
};
|
||||
|
||||
const relativeToSeconds = (relative: string): number => {
|
||||
const match = regex.exec(relative);
|
||||
|
||||
if (!match || match.length !== 3) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const [, value, unit] = match;
|
||||
const parsed = parseInt(value, 10);
|
||||
|
||||
if (isNaN(parsed)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parsed * units[unit];
|
||||
};
|
||||
|
||||
const units: Record<string, number> = {
|
||||
w: 604800,
|
||||
d: 86400,
|
||||
h: 3600,
|
||||
m: 60,
|
||||
s: 1,
|
||||
};
|
||||
|
||||
const secondsToRelativeFormat = (seconds: number): string => {
|
||||
if (seconds <= 0) {
|
||||
return 'now';
|
||||
}
|
||||
|
||||
if (seconds >= units.w && seconds % units.w === 0) {
|
||||
return `now-${seconds / units.w}w`;
|
||||
}
|
||||
|
||||
if (seconds >= units.d && seconds % units.d === 0) {
|
||||
return `now-${seconds / units.d}d`;
|
||||
}
|
||||
|
||||
if (seconds >= units.h && seconds % units.h === 0) {
|
||||
return `now-${seconds / units.h}h`;
|
||||
}
|
||||
|
||||
if (seconds >= units.m && seconds % units.m === 0) {
|
||||
return `now-${seconds / units.m}m`;
|
||||
}
|
||||
|
||||
return `now-${seconds}s`;
|
||||
};
|
@ -1,11 +1,11 @@
|
||||
import { GrafanaTheme2, isDateTime, TimeOption, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { GrafanaTheme2, isDateTime, rangeUtil, RawTimeRange, TimeOption, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { memo, useState } from 'react';
|
||||
import React, { memo, useMemo, useState } from 'react';
|
||||
import { useMedia } from 'react-use';
|
||||
import { stylesFactory, useTheme2 } from '../../../themes';
|
||||
import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
import { mapRangeToTimeOption } from './mapper';
|
||||
import { mapOptionToTimeRange, mapRangeToTimeOption } from './mapper';
|
||||
import { TimePickerTitle } from './TimePickerTitle';
|
||||
import { TimeRangeForm } from './TimeRangeForm';
|
||||
import { TimeRangeList } from './TimeRangeList';
|
||||
@ -156,6 +156,11 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, isReversed, hideQuickRanges, isContainerTall);
|
||||
const historyOptions = mapToHistoryOptions(history, timeZone);
|
||||
const timeOption = useTimeOption(value.raw, otherOptions, quickOptions);
|
||||
|
||||
const onChangeTimeOption = (timeOption: TimeOption) => {
|
||||
return onChange(mapOptionToTimeRange(timeOption));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container, className)}>
|
||||
@ -173,17 +178,15 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
||||
<TimeRangeList
|
||||
title="Relative time ranges"
|
||||
options={quickOptions}
|
||||
onSelect={onChange}
|
||||
value={value}
|
||||
timeZone={timeZone}
|
||||
onChange={onChangeTimeOption}
|
||||
value={timeOption}
|
||||
/>
|
||||
<div className={styles.spacing} />
|
||||
<TimeRangeList
|
||||
title="Other quick ranges"
|
||||
options={otherOptions}
|
||||
onSelect={onChange}
|
||||
value={value}
|
||||
timeZone={timeZone}
|
||||
onChange={onChangeTimeOption}
|
||||
value={timeOption}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -210,6 +213,10 @@ const NarrowScreenForm: React.FC<FormProps> = (props) => {
|
||||
const [collapsedFlag, setCollapsedFlag] = useState(!isAbsolute);
|
||||
const collapsed = hideQuickRanges ? false : collapsedFlag;
|
||||
|
||||
const onChangeTimeOption = (timeOption: TimeOption) => {
|
||||
return onChange(mapOptionToTimeRange(timeOption));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -233,10 +240,8 @@ const NarrowScreenForm: React.FC<FormProps> = (props) => {
|
||||
<TimeRangeList
|
||||
title="Recently used absolute ranges"
|
||||
options={historyOptions}
|
||||
onSelect={onChange}
|
||||
value={value}
|
||||
onChange={onChangeTimeOption}
|
||||
placeholderEmpty={null}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -246,8 +251,12 @@ const NarrowScreenForm: React.FC<FormProps> = (props) => {
|
||||
};
|
||||
|
||||
const FullScreenForm: React.FC<FormProps> = (props) => {
|
||||
const { onChange } = props;
|
||||
const theme = useTheme2();
|
||||
const styles = getFullScreenStyles(theme, props.hideQuickRanges);
|
||||
const onChangeTimeOption = (timeOption: TimeOption) => {
|
||||
return onChange(mapOptionToTimeRange(timeOption));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -268,10 +277,8 @@ const FullScreenForm: React.FC<FormProps> = (props) => {
|
||||
<TimeRangeList
|
||||
title="Recently used absolute ranges"
|
||||
options={props.historyOptions || []}
|
||||
onSelect={props.onChange}
|
||||
value={props.value}
|
||||
onChange={onChangeTimeOption}
|
||||
placeholderEmpty={<EmptyRecentList />}
|
||||
timeZone={props.timeZone}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -313,3 +320,24 @@ function mapToHistoryOptions(ranges?: TimeRange[], timeZone?: TimeZone): TimeOpt
|
||||
}
|
||||
|
||||
EmptyRecentList.displayName = 'EmptyRecentList';
|
||||
|
||||
const useTimeOption = (
|
||||
raw: RawTimeRange,
|
||||
quickOptions: TimeOption[],
|
||||
otherOptions: TimeOption[]
|
||||
): TimeOption | undefined => {
|
||||
return useMemo(() => {
|
||||
if (!rangeUtil.isRelativeTimeRange(raw)) {
|
||||
return;
|
||||
}
|
||||
const quickOption = quickOptions.find((option) => {
|
||||
return option.from === raw.from && option.to === raw.to;
|
||||
});
|
||||
if (quickOption) {
|
||||
return quickOption;
|
||||
}
|
||||
return otherOptions.find((option) => {
|
||||
return option.from === raw.from && option.to === raw.to;
|
||||
});
|
||||
}, [raw, otherOptions, quickOptions]);
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ interface Props {
|
||||
isReversed?: boolean;
|
||||
}
|
||||
|
||||
interface InputState {
|
||||
export interface InputState {
|
||||
value: string;
|
||||
invalid: boolean;
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { TimeRange, TimeOption, TimeZone } from '@grafana/data';
|
||||
import { TimeOption } from '@grafana/data';
|
||||
import { TimePickerTitle } from './TimePickerTitle';
|
||||
import { TimeRangeOption } from './TimeRangeOption';
|
||||
import { mapOptionToTimeRange } from './mapper';
|
||||
import { stylesFactory } from '../../../themes';
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
@ -29,10 +28,9 @@ const getOptionsStyles = stylesFactory(() => {
|
||||
interface Props {
|
||||
title?: string;
|
||||
options: TimeOption[];
|
||||
value?: TimeRange;
|
||||
onSelect: (option: TimeRange) => void;
|
||||
value?: TimeOption;
|
||||
onChange: (option: TimeOption) => void;
|
||||
placeholderEmpty?: ReactNode;
|
||||
timeZone?: TimeZone;
|
||||
}
|
||||
|
||||
export const TimeRangeList: React.FC<Props> = (props) => {
|
||||
@ -57,7 +55,7 @@ export const TimeRangeList: React.FC<Props> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Options: React.FC<Props> = ({ options, value, onSelect, timeZone }) => {
|
||||
const Options: React.FC<Props> = ({ options, value, onChange }) => {
|
||||
const styles = getOptionsStyles();
|
||||
|
||||
return (
|
||||
@ -68,11 +66,11 @@ const Options: React.FC<Props> = ({ options, value, onSelect, timeZone }) => {
|
||||
key={keyForOption(option, index)}
|
||||
value={option}
|
||||
selected={isEqual(option, value)}
|
||||
onSelect={(option) => onSelect(mapOptionToTimeRange(option, timeZone))}
|
||||
onSelect={onChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.grow}></div>
|
||||
<div className={styles.grow} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -81,9 +79,9 @@ function keyForOption(option: TimeOption, index: number): string {
|
||||
return `${option.from}-${option.to}-${index}`;
|
||||
}
|
||||
|
||||
function isEqual(x: TimeOption, y?: TimeRange): boolean {
|
||||
function isEqual(x: TimeOption, y?: TimeOption): boolean {
|
||||
if (!y || !x) {
|
||||
return false;
|
||||
}
|
||||
return y.raw.from === x.from && y.raw.to === x.to;
|
||||
return y.from === x.from && y.to === x.to;
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): Tim
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
section: 3,
|
||||
display: `${from} to ${to}`,
|
||||
};
|
||||
};
|
||||
|
@ -1,37 +1,37 @@
|
||||
import { TimeOption } from '@grafana/data';
|
||||
|
||||
export const quickOptions: TimeOption[] = [
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 },
|
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3 },
|
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3 },
|
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 },
|
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 },
|
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 },
|
||||
{ from: 'now-2d', to: 'now', display: 'Last 2 days', section: 3 },
|
||||
{ from: 'now-7d', to: 'now', display: 'Last 7 days', section: 3 },
|
||||
{ from: 'now-30d', to: 'now', display: 'Last 30 days', section: 3 },
|
||||
{ from: 'now-90d', to: 'now', display: 'Last 90 days', section: 3 },
|
||||
{ from: 'now-6M', to: 'now', display: 'Last 6 months', section: 3 },
|
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year', section: 3 },
|
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years', section: 3 },
|
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 3 },
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes' },
|
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour' },
|
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours' },
|
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours' },
|
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours' },
|
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours' },
|
||||
{ from: 'now-2d', to: 'now', display: 'Last 2 days' },
|
||||
{ from: 'now-7d', to: 'now', display: 'Last 7 days' },
|
||||
{ from: 'now-30d', to: 'now', display: 'Last 30 days' },
|
||||
{ from: 'now-90d', to: 'now', display: 'Last 90 days' },
|
||||
{ from: 'now-6M', to: 'now', display: 'Last 6 months' },
|
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year' },
|
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
|
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years' },
|
||||
];
|
||||
|
||||
export const otherOptions: TimeOption[] = [
|
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday', section: 3 },
|
||||
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday', section: 3 },
|
||||
{ from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week', section: 3 },
|
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week', section: 3 },
|
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month', section: 3 },
|
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year', section: 3 },
|
||||
{ from: 'now/d', to: 'now/d', display: 'Today', section: 3 },
|
||||
{ from: 'now/d', to: 'now', display: 'Today so far', section: 3 },
|
||||
{ from: 'now/w', to: 'now/w', display: 'This week', section: 3 },
|
||||
{ from: 'now/w', to: 'now', display: 'This week so far', section: 3 },
|
||||
{ from: 'now/M', to: 'now/M', display: 'This month', section: 3 },
|
||||
{ from: 'now/M', to: 'now', display: 'This month so far', section: 3 },
|
||||
{ from: 'now/y', to: 'now/y', display: 'This year', section: 3 },
|
||||
{ from: 'now/y', to: 'now', display: 'This year so far', section: 3 },
|
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday' },
|
||||
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday' },
|
||||
{ from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week' },
|
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week' },
|
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month' },
|
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year' },
|
||||
{ from: 'now/d', to: 'now/d', display: 'Today' },
|
||||
{ from: 'now/d', to: 'now', display: 'Today so far' },
|
||||
{ from: 'now/w', to: 'now/w', display: 'This week' },
|
||||
{ from: 'now/w', to: 'now', display: 'This week so far' },
|
||||
{ from: 'now/M', to: 'now/M', display: 'This month' },
|
||||
{ from: 'now/M', to: 'now', display: 'This month so far' },
|
||||
{ from: 'now/y', to: 'now/y', display: 'This year' },
|
||||
{ from: 'now/y', to: 'now', display: 'This year so far' },
|
||||
];
|
||||
|
@ -190,6 +190,7 @@ export { Checkbox } from './Forms/Checkbox';
|
||||
export { TextArea } from './TextArea/TextArea';
|
||||
export { FileUpload } from './FileUpload/FileUpload';
|
||||
export { TimeRangeInput } from './TimePicker/TimeRangeInput';
|
||||
export { RelativeTimeRangePicker } from './TimePicker/RelativeTimeRangePicker/RelativeTimeRangePicker';
|
||||
export { Card, Props as CardProps, getCardStyles } from './Card/Card';
|
||||
export { CardContainer, CardContainerProps } from './Card/CardContainer';
|
||||
export { FormattedValueDisplay } from './FormattedValueDisplay/FormattedValueDisplay';
|
||||
|
@ -1,15 +1,6 @@
|
||||
import { rangeUtil, dateTime } from '@grafana/data';
|
||||
import { keys } from 'lodash';
|
||||
|
||||
describe('rangeUtil', () => {
|
||||
describe('Can get range grouped list of ranges', () => {
|
||||
it('when custom settings should return default range list', () => {
|
||||
const groups: any = rangeUtil.getRelativeTimesList({ time_options: [] }, 'Last 5 minutes');
|
||||
expect(keys(groups).length).toBe(4);
|
||||
expect(groups[3][0].active).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Can get range text described', () => {
|
||||
it('should handle simple old expression with only amount and unit', () => {
|
||||
const info = rangeUtil.describeTextRange('5m');
|
||||
|
@ -1,10 +1,17 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
import { DataQuery, DataSourceInstanceSettings, rangeUtil, PanelData, TimeRange } from '@grafana/data';
|
||||
import {
|
||||
DataQuery,
|
||||
DataSourceInstanceSettings,
|
||||
getDefaultRelativeTimeRange,
|
||||
PanelData,
|
||||
RelativeTimeRange,
|
||||
} from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
import { RelativeTimeRangePicker } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
// The query configuration
|
||||
@ -30,7 +37,7 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
this.props.onQueriesChange(this.props.queries.filter((item) => item.model !== query));
|
||||
};
|
||||
|
||||
onChangeTimeRange(timeRange: TimeRange, index: number) {
|
||||
onChangeTimeRange(timeRange: RelativeTimeRange, index: number) {
|
||||
const { queries, onQueriesChange } = this.props;
|
||||
onQueriesChange(
|
||||
queries.map((item, itemIndex) => {
|
||||
@ -39,7 +46,7 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
relativeTimeRange: rangeUtil.timeRangeToRelative(timeRange),
|
||||
relativeTimeRange: timeRange,
|
||||
};
|
||||
})
|
||||
);
|
||||
@ -84,6 +91,7 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
refId: query.refId,
|
||||
model: {
|
||||
...item.model,
|
||||
...query,
|
||||
@ -155,16 +163,7 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
data={data}
|
||||
query={query.model}
|
||||
onChange={(query) => this.onChangeQuery(query, index)}
|
||||
timeRange={
|
||||
!isExpressionQuery(query.model) && query.relativeTimeRange
|
||||
? rangeUtil.relativeToTimeRange(query.relativeTimeRange)
|
||||
: undefined
|
||||
}
|
||||
onChangeTimeRange={
|
||||
!isExpressionQuery(query.model)
|
||||
? (timeRange) => this.onChangeTimeRange(timeRange, index)
|
||||
: undefined
|
||||
}
|
||||
renderHeaderExtras={() => this.renderTimePicker(query, index)}
|
||||
onRemoveQuery={this.onRemoveQuery}
|
||||
onAddQuery={(duplicate) => this.onDuplicateQuery(duplicate, query)}
|
||||
onRunQuery={this.props.onRunQueries}
|
||||
@ -180,4 +179,17 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
</DragDropContext>
|
||||
);
|
||||
}
|
||||
|
||||
renderTimePicker(query: GrafanaQuery, index: number): ReactNode {
|
||||
if (isExpressionQuery(query.model)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RelativeTimeRangePicker
|
||||
timeRange={query.relativeTimeRange ?? getDefaultRelativeTimeRange()}
|
||||
onChange={(range) => this.onChangeTimeRange(range, index)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { has, cloneDeep } from 'lodash';
|
||||
// Utils & Services
|
||||
@ -35,10 +35,9 @@ interface Props {
|
||||
queries: DataQuery[];
|
||||
id: string;
|
||||
index: number;
|
||||
timeRange?: TimeRange;
|
||||
dataSource: DataSourceInstanceSettings;
|
||||
onChangeDataSource?: (dsSettings: DataSourceInstanceSettings) => void;
|
||||
onChangeTimeRange?: (timeRange: TimeRange) => void;
|
||||
renderHeaderExtras?: () => ReactNode;
|
||||
onAddQuery: (query: DataQuery) => void;
|
||||
onRemoveQuery: (query: DataQuery) => void;
|
||||
onChange: (query: DataQuery) => void;
|
||||
@ -304,20 +303,19 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderHeader = (props: QueryOperationRowRenderProps) => {
|
||||
const { query, dataSource, onChangeDataSource, onChange, queries, onChangeTimeRange, timeRange } = this.props;
|
||||
const { query, dataSource, onChangeDataSource, onChange, queries, renderHeaderExtras } = this.props;
|
||||
|
||||
return (
|
||||
<QueryEditorRowHeader
|
||||
query={query}
|
||||
queries={queries}
|
||||
onChangeTimeRange={onChangeTimeRange}
|
||||
timeRange={timeRange}
|
||||
onChangeDataSource={onChangeDataSource}
|
||||
dataSource={dataSource}
|
||||
disabled={query.hide}
|
||||
onClick={(e) => this.onToggleEditMode(e, props)}
|
||||
onChange={onChange}
|
||||
collapsedText={!props.isOpen ? this.renderCollapsedText() : null}
|
||||
renderExtras={renderHeaderExtras}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Props, QueryEditorRowHeader } from './QueryEditorRowHeader';
|
||||
import { DataSourceInstanceSettings, dateTime } from '@grafana/data';
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
|
||||
@ -58,41 +58,6 @@ describe('QueryEditorRowHeader', () => {
|
||||
|
||||
expect(screen.queryByLabelText(selectors.components.DataSourcePicker.container)).toBeNull();
|
||||
});
|
||||
|
||||
it('should show time range picker when callback and value is passed', async () => {
|
||||
renderScenario({
|
||||
onChangeTimeRange: () => {},
|
||||
timeRange: {
|
||||
from: dateTime(),
|
||||
to: dateTime(),
|
||||
raw: { from: 'now', to: 'now' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByLabelText(selectors.components.TimePicker.openButton)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should not show time range picker when no value is passed', async () => {
|
||||
renderScenario({
|
||||
onChangeTimeRange: () => {},
|
||||
timeRange: undefined,
|
||||
});
|
||||
|
||||
expect(screen.queryByLabelText(selectors.components.DataSourcePicker.container)).toBeNull();
|
||||
});
|
||||
|
||||
it('should not show time range picker when no callback is passed', async () => {
|
||||
renderScenario({
|
||||
onChangeTimeRange: undefined,
|
||||
timeRange: {
|
||||
from: dateTime(),
|
||||
to: dateTime(),
|
||||
raw: { from: 'now', to: 'now' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByLabelText(selectors.components.DataSourcePicker.container)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function renderScenario(overrides: Partial<Props>) {
|
||||
|
@ -1,26 +1,24 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { DataQuery, DataSourceInstanceSettings, GrafanaTheme, TimeRange } from '@grafana/data';
|
||||
import { DataQuery, DataSourceInstanceSettings, GrafanaTheme } from '@grafana/data';
|
||||
import { DataSourcePicker } from '@grafana/runtime';
|
||||
import { Icon, Input, FieldValidationMessage, TimeRangeInput, useStyles } from '@grafana/ui';
|
||||
import { Icon, Input, FieldValidationMessage, useStyles } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { ExpressionDatasourceUID } from '../../expressions/ExpressionDatasource';
|
||||
|
||||
export interface Props {
|
||||
query: DataQuery;
|
||||
queries: DataQuery[];
|
||||
disabled?: boolean;
|
||||
timeRange?: TimeRange;
|
||||
dataSource: DataSourceInstanceSettings;
|
||||
renderExtras?: () => ReactNode;
|
||||
onChangeDataSource?: (settings: DataSourceInstanceSettings) => void;
|
||||
onChangeTimeRange?: (timeRange: TimeRange) => void;
|
||||
onChange: (query: DataQuery) => void;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
collapsedText: string | null;
|
||||
}
|
||||
|
||||
export const QueryEditorRowHeader: React.FC<Props> = (props) => {
|
||||
const { dataSource, onChangeDataSource, disabled, query, queries, onClick, onChange, collapsedText } = props;
|
||||
const { query, queries, onClick, onChange, collapsedText, renderExtras, disabled } = props;
|
||||
|
||||
const styles = useStyles(getStyles);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
@ -112,9 +110,9 @@ export const QueryEditorRowHeader: React.FC<Props> = (props) => {
|
||||
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
|
||||
</>
|
||||
)}
|
||||
<PickerRenderer {...props} />
|
||||
{dataSource && !onChangeDataSource && <em className={styles.contextInfo}> ({dataSource.name})</em>}
|
||||
{disabled && <em className={styles.contextInfo}> Disabled</em>}
|
||||
{renderDataSource(props, styles)}
|
||||
{renderExtras && <div className={styles.itemWrapper}>{renderExtras()}</div>}
|
||||
{disabled && <em className={styles.contextInfo}>Disabled</em>}
|
||||
|
||||
{collapsedText && (
|
||||
<div className={styles.collapsedText} onClick={onClick}>
|
||||
@ -125,22 +123,16 @@ export const QueryEditorRowHeader: React.FC<Props> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PickerRenderer: React.FC<Props> = (props) => {
|
||||
const { onChangeTimeRange, timeRange, onChangeDataSource, dataSource } = props;
|
||||
const styles = useStyles(getStyles);
|
||||
const renderDataSource = (props: Props, styles: ReturnType<typeof getStyles>): ReactNode => {
|
||||
const { dataSource, onChangeDataSource } = props;
|
||||
|
||||
if (!onChangeTimeRange && !onChangeDataSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dataSource.uid === ExpressionDatasourceUID) {
|
||||
return null;
|
||||
if (!onChangeDataSource) {
|
||||
return <em className={styles.contextInfo}>({dataSource.name})</em>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.pickerWrapper}>
|
||||
{onChangeDataSource && <DataSourcePicker current={dataSource.name} onChange={onChangeDataSource} />}
|
||||
{onChangeTimeRange && timeRange && <TimeRangeInput onChange={onChangeTimeRange} value={timeRange} />}
|
||||
<div className={styles.itemWrapper}>
|
||||
<DataSourcePicker current={dataSource.name} onChange={onChangeDataSource} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -222,9 +214,9 @@ const getStyles = (theme: GrafanaTheme) => {
|
||||
color: ${theme.colors.textWeak};
|
||||
padding-left: 10px;
|
||||
`,
|
||||
pickerWrapper: css`
|
||||
itemWrapper: css`
|
||||
display: flex;
|
||||
margin-left: 8px;
|
||||
margin-left: 4px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user