mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Support new duration format in graph range input (#7833)
* Support new duration format in graph range input This is to make the duration parsing and formatting in the graph range input field consistent with the new duration formatting introduced for the configuration and PromQL (https://github.com/prometheus/prometheus/pull/7713). Ranges were previously handled in seconds - these are now handled in milliseconds everywhere, as this makes things nicer / easier. Signed-off-by: Julius Volz <julius.volz@gmail.com> * Fixups Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
7ec647dbe9
commit
a1601274ba
|
@ -5,7 +5,7 @@ import { RuleStatus } from './AlertContents';
|
||||||
import { Rule } from '../../types/types';
|
import { Rule } from '../../types/types';
|
||||||
import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { createExpressionLink, parsePrometheusFloat, formatRange } from '../../utils/index';
|
import { createExpressionLink, parsePrometheusFloat, formatDuration } from '../../utils/index';
|
||||||
|
|
||||||
interface CollapsibleAlertPanelProps {
|
interface CollapsibleAlertPanelProps {
|
||||||
rule: Rule;
|
rule: Rule;
|
||||||
|
@ -38,7 +38,7 @@ const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnot
|
||||||
</div>
|
</div>
|
||||||
{rule.duration > 0 && (
|
{rule.duration > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div>for: {formatRange(rule.duration)}</div>
|
<div>for: {formatDuration(rule.duration * 1000)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{rule.labels && Object.keys(rule.labels).length > 0 && (
|
{rule.labels && Object.keys(rule.labels).length > 0 && (
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-sol
|
||||||
import TimeInput from './TimeInput';
|
import TimeInput from './TimeInput';
|
||||||
|
|
||||||
const defaultGraphControlProps = {
|
const defaultGraphControlProps = {
|
||||||
range: 60 * 60 * 24,
|
range: 60 * 60 * 24 * 1000,
|
||||||
endTime: 1572100217898,
|
endTime: 1572100217898,
|
||||||
resolution: 10,
|
resolution: 10,
|
||||||
stacked: false,
|
stacked: false,
|
||||||
|
@ -81,7 +81,7 @@ describe('GraphControls', () => {
|
||||||
const timeInput = controls.find(TimeInput);
|
const timeInput = controls.find(TimeInput);
|
||||||
expect(timeInput).toHaveLength(1);
|
expect(timeInput).toHaveLength(1);
|
||||||
expect(timeInput.prop('time')).toEqual(1572100217898);
|
expect(timeInput.prop('time')).toEqual(1572100217898);
|
||||||
expect(timeInput.prop('range')).toEqual(86400);
|
expect(timeInput.prop('range')).toEqual(86400000);
|
||||||
expect(timeInput.prop('placeholder')).toEqual('End time');
|
expect(timeInput.prop('placeholder')).toEqual('End time');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons';
|
import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import TimeInput from './TimeInput';
|
import TimeInput from './TimeInput';
|
||||||
import { parseRange, formatRange } from '../../utils';
|
import { parseDuration, formatDuration } from '../../utils';
|
||||||
|
|
||||||
interface GraphControlsProps {
|
interface GraphControlsProps {
|
||||||
range: number;
|
range: number;
|
||||||
|
@ -43,10 +43,10 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||||
56 * 24 * 60 * 60,
|
56 * 24 * 60 * 60,
|
||||||
365 * 24 * 60 * 60,
|
365 * 24 * 60 * 60,
|
||||||
730 * 24 * 60 * 60,
|
730 * 24 * 60 * 60,
|
||||||
];
|
].map(s => s * 1000);
|
||||||
|
|
||||||
onChangeRangeInput = (rangeText: string): void => {
|
onChangeRangeInput = (rangeText: string): void => {
|
||||||
const range = parseRange(rangeText);
|
const range = parseDuration(rangeText);
|
||||||
if (range === null) {
|
if (range === null) {
|
||||||
this.changeRangeInput(this.props.range);
|
this.changeRangeInput(this.props.range);
|
||||||
} else {
|
} else {
|
||||||
|
@ -55,7 +55,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||||
};
|
};
|
||||||
|
|
||||||
changeRangeInput = (range: number): void => {
|
changeRangeInput = (range: number): void => {
|
||||||
this.rangeRef.current!.value = formatRange(range);
|
this.rangeRef.current!.value = formatDuration(range);
|
||||||
};
|
};
|
||||||
|
|
||||||
increaseRange = (): void => {
|
increaseRange = (): void => {
|
||||||
|
@ -98,7 +98,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
defaultValue={formatRange(this.props.range)}
|
defaultValue={formatDuration(this.props.range)}
|
||||||
innerRef={this.rangeRef}
|
innerRef={this.rangeRef}
|
||||||
onBlur={() => this.onChangeRangeInput(this.rangeRef.current!.value)}
|
onBlur={() => this.onChangeRangeInput(this.rangeRef.current!.value)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -35,7 +35,7 @@ interface PanelState {
|
||||||
export interface PanelOptions {
|
export interface PanelOptions {
|
||||||
expr: string;
|
expr: string;
|
||||||
type: PanelType;
|
type: PanelType;
|
||||||
range: number; // Range in seconds.
|
range: number; // Range in milliseconds.
|
||||||
endTime: number | null; // Timestamp in milliseconds.
|
endTime: number | null; // Timestamp in milliseconds.
|
||||||
resolution: number | null; // Resolution in seconds.
|
resolution: number | null; // Resolution in seconds.
|
||||||
stacked: boolean;
|
stacked: boolean;
|
||||||
|
@ -49,7 +49,7 @@ export enum PanelType {
|
||||||
export const PanelDefaultOptions: PanelOptions = {
|
export const PanelDefaultOptions: PanelOptions = {
|
||||||
type: PanelType.Table,
|
type: PanelType.Table,
|
||||||
expr: '',
|
expr: '',
|
||||||
range: 3600,
|
range: 60 * 60 * 1000,
|
||||||
endTime: null,
|
endTime: null,
|
||||||
resolution: null,
|
resolution: null,
|
||||||
stacked: false,
|
stacked: false,
|
||||||
|
@ -108,8 +108,8 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
|
|
||||||
const endTime = this.getEndTime().valueOf() / 1000; // TODO: shouldn't valueof only work when it's a moment?
|
const endTime = this.getEndTime().valueOf() / 1000; // TODO: shouldn't valueof only work when it's a moment?
|
||||||
const startTime = endTime - this.props.options.range;
|
const startTime = endTime - this.props.options.range / 1000;
|
||||||
const resolution = this.props.options.resolution || Math.max(Math.floor(this.props.options.range / 250), 1);
|
const resolution = this.props.options.resolution || Math.max(Math.floor(this.props.options.range / 250000), 1);
|
||||||
const params: URLSearchParams = new URLSearchParams({
|
const params: URLSearchParams = new URLSearchParams({
|
||||||
query: expr,
|
query: expr,
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,7 +39,7 @@ class TimeInput extends Component<TimeInputProps> {
|
||||||
return this.props.time || moment().valueOf();
|
return this.props.time || moment().valueOf();
|
||||||
};
|
};
|
||||||
|
|
||||||
calcShiftRange = () => (this.props.range * 1000) / 2;
|
calcShiftRange = () => this.props.range / 2;
|
||||||
|
|
||||||
increaseTime = (): void => {
|
increaseTime = (): void => {
|
||||||
const time = this.getBaseTime() + this.calcShiftRange();
|
const time = this.getBaseTime() + this.calcShiftRange();
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { RouteComponentProps } from '@reach/router';
|
||||||
import { APIResponse } from '../../hooks/useFetch';
|
import { APIResponse } from '../../hooks/useFetch';
|
||||||
import { Alert, Table, Badge } from 'reactstrap';
|
import { Alert, Table, Badge } from 'reactstrap';
|
||||||
import { Link } from '@reach/router';
|
import { Link } from '@reach/router';
|
||||||
import { formatRelative, createExpressionLink, humanizeDuration, formatRange } from '../../utils';
|
import { formatRelative, createExpressionLink, humanizeDuration, formatDuration } from '../../utils';
|
||||||
import { Rule } from '../../types/types';
|
import { Rule } from '../../types/types';
|
||||||
import { now } from 'moment';
|
import { now } from 'moment';
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ export const RulesContent: FC<RouteComponentProps & RulesContentProps> = ({ resp
|
||||||
<GraphExpressionLink title="expr" text={r.query} expr={r.query} />
|
<GraphExpressionLink title="expr" text={r.query} expr={r.query} />
|
||||||
{r.duration > 0 && (
|
{r.duration > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<strong>for:</strong> {formatRange(r.duration)}
|
<strong>for:</strong> {formatDuration(r.duration * 1000)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{r.labels && Object.keys(r.labels).length > 0 && (
|
{r.labels && Object.keys(r.labels).length > 0 && (
|
||||||
|
|
|
@ -43,34 +43,75 @@ export const metricToSeriesName = (labels: { [key: string]: string }) => {
|
||||||
return tsName;
|
return tsName;
|
||||||
};
|
};
|
||||||
|
|
||||||
const rangeUnits: { [unit: string]: number } = {
|
export const parseDuration = (durationStr: string): number | null => {
|
||||||
y: 60 * 60 * 24 * 365,
|
if (durationStr === '') {
|
||||||
w: 60 * 60 * 24 * 7,
|
|
||||||
d: 60 * 60 * 24,
|
|
||||||
h: 60 * 60,
|
|
||||||
m: 60,
|
|
||||||
s: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function parseRange(rangeText: string): number | null {
|
|
||||||
const rangeRE = new RegExp('^([0-9]+)([ywdhms]+)$');
|
|
||||||
const matches = rangeText.match(rangeRE);
|
|
||||||
if (!matches || matches.length !== 3) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const value = parseInt(matches[1]);
|
if (durationStr === '0') {
|
||||||
const unit = matches[2];
|
// Allow 0 without a unit.
|
||||||
return value * rangeUnits[unit];
|
return 0;
|
||||||
}
|
|
||||||
|
|
||||||
export function formatRange(range: number): string {
|
|
||||||
for (const unit of Object.keys(rangeUnits)) {
|
|
||||||
if (range % rangeUnits[unit] === 0) {
|
|
||||||
return range / rangeUnits[unit] + unit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return range + 's';
|
|
||||||
}
|
const durationRE = new RegExp('^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$');
|
||||||
|
const matches = durationStr.match(durationRE);
|
||||||
|
if (!matches) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dur = 0;
|
||||||
|
|
||||||
|
// Parse the match at pos `pos` in the regex and use `mult` to turn that
|
||||||
|
// into ms, then add that value to the total parsed duration.
|
||||||
|
const m = (pos: number, mult: number) => {
|
||||||
|
if (matches[pos] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = parseInt(matches[pos]);
|
||||||
|
dur += n * mult;
|
||||||
|
};
|
||||||
|
|
||||||
|
m(2, 1000 * 60 * 60 * 24 * 365); // y
|
||||||
|
m(4, 1000 * 60 * 60 * 24 * 7); // w
|
||||||
|
m(6, 1000 * 60 * 60 * 24); // d
|
||||||
|
m(8, 1000 * 60 * 60); // h
|
||||||
|
m(10, 1000 * 60); // m
|
||||||
|
m(12, 1000); // s
|
||||||
|
m(14, 1); // ms
|
||||||
|
|
||||||
|
return dur;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDuration = (d: number): string => {
|
||||||
|
let ms = d;
|
||||||
|
let r = '';
|
||||||
|
if (ms === 0) {
|
||||||
|
return '0s';
|
||||||
|
}
|
||||||
|
|
||||||
|
const f = (unit: string, mult: number, exact: boolean) => {
|
||||||
|
if (exact && ms % mult !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const v = Math.floor(ms / mult);
|
||||||
|
if (v > 0) {
|
||||||
|
r += `${v}${unit}`;
|
||||||
|
ms -= v * mult;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only format years and weeks if the remainder is zero, as it is often
|
||||||
|
// easier to read 90d than 12w6d.
|
||||||
|
f('y', 1000 * 60 * 60 * 24 * 365, true);
|
||||||
|
f('w', 1000 * 60 * 60 * 24 * 7, true);
|
||||||
|
|
||||||
|
f('d', 1000 * 60 * 60 * 24, false);
|
||||||
|
f('h', 1000 * 60 * 60, false);
|
||||||
|
f('m', 1000 * 60, false);
|
||||||
|
f('s', 1000, false);
|
||||||
|
f('ms', 1, false);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
export function parseTime(timeText: string): number {
|
export function parseTime(timeText: string): number {
|
||||||
return moment.utc(timeText).valueOf();
|
return moment.utc(timeText).valueOf();
|
||||||
|
@ -161,7 +202,7 @@ export const parseOption = (param: string): Partial<PanelOptions> => {
|
||||||
return { stacked: decodedValue === '1' };
|
return { stacked: decodedValue === '1' };
|
||||||
|
|
||||||
case 'range_input':
|
case 'range_input':
|
||||||
const range = parseRange(decodedValue);
|
const range = parseDuration(decodedValue);
|
||||||
return isPresent(range) ? { range } : {};
|
return isPresent(range) ? { range } : {};
|
||||||
|
|
||||||
case 'end_input':
|
case 'end_input':
|
||||||
|
@ -187,7 +228,7 @@ export const toQueryString = ({ key, options }: PanelMeta) => {
|
||||||
formatWithKey('expr', expr),
|
formatWithKey('expr', expr),
|
||||||
formatWithKey('tab', type === PanelType.Graph ? 0 : 1),
|
formatWithKey('tab', type === PanelType.Graph ? 0 : 1),
|
||||||
formatWithKey('stacked', stacked ? 1 : 0),
|
formatWithKey('stacked', stacked ? 1 : 0),
|
||||||
formatWithKey('range_input', formatRange(range)),
|
formatWithKey('range_input', formatDuration(range)),
|
||||||
time ? `${formatWithKey('end_input', time)}&${formatWithKey('moment_input', time)}` : '',
|
time ? `${formatWithKey('end_input', time)}&${formatWithKey('moment_input', time)}` : '',
|
||||||
isPresent(resolution) ? formatWithKey('step_input', resolution) : '',
|
isPresent(resolution) ? formatWithKey('step_input', resolution) : '',
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,8 +5,8 @@ import {
|
||||||
metricToSeriesName,
|
metricToSeriesName,
|
||||||
formatTime,
|
formatTime,
|
||||||
parseTime,
|
parseTime,
|
||||||
formatRange,
|
formatDuration,
|
||||||
parseRange,
|
parseDuration,
|
||||||
humanizeDuration,
|
humanizeDuration,
|
||||||
formatRelative,
|
formatRelative,
|
||||||
now,
|
now,
|
||||||
|
@ -67,25 +67,92 @@ describe('Utils', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('formatRange', () => {
|
describe('parseDuration and formatDuration', () => {
|
||||||
it('returns a time string representing the time in seconds in one unit', () => {
|
describe('should parse and format durations correctly', () => {
|
||||||
expect(formatRange(60 * 60 * 24 * 365)).toEqual('1y');
|
const tests: { input: string; output: number; expectedString?: string }[] = [
|
||||||
expect(formatRange(60 * 60 * 24 * 7)).toEqual('1w');
|
{
|
||||||
expect(formatRange(2 * 60 * 60 * 24)).toEqual('2d');
|
input: '0',
|
||||||
expect(formatRange(60 * 60)).toEqual('1h');
|
output: 0,
|
||||||
expect(formatRange(7 * 60)).toEqual('7m');
|
expectedString: '0s',
|
||||||
expect(formatRange(63)).toEqual('63s');
|
},
|
||||||
});
|
{
|
||||||
});
|
input: '0w',
|
||||||
|
output: 0,
|
||||||
|
expectedString: '0s',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '0s',
|
||||||
|
output: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '324ms',
|
||||||
|
output: 324,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '3s',
|
||||||
|
output: 3 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '5m',
|
||||||
|
output: 5 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '1h',
|
||||||
|
output: 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '4d',
|
||||||
|
output: 4 * 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '4d1h',
|
||||||
|
output: 4 * 24 * 60 * 60 * 1000 + 1 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '14d',
|
||||||
|
output: 14 * 24 * 60 * 60 * 1000,
|
||||||
|
expectedString: '2w',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '3w',
|
||||||
|
output: 3 * 7 * 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '3w2d1h',
|
||||||
|
output: 3 * 7 * 24 * 60 * 60 * 1000 + 2 * 24 * 60 * 60 * 1000 + 60 * 60 * 1000,
|
||||||
|
expectedString: '23d1h',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '1y2w3d4h5m6s7ms',
|
||||||
|
output:
|
||||||
|
1 * 365 * 24 * 60 * 60 * 1000 +
|
||||||
|
2 * 7 * 24 * 60 * 60 * 1000 +
|
||||||
|
3 * 24 * 60 * 60 * 1000 +
|
||||||
|
4 * 60 * 60 * 1000 +
|
||||||
|
5 * 60 * 1000 +
|
||||||
|
6 * 1000 +
|
||||||
|
7,
|
||||||
|
expectedString: '382d4h5m6s7ms',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe('parseRange', () => {
|
tests.forEach(t => {
|
||||||
it('returns a time string representing the time in seconds in one unit', () => {
|
it(t.input, () => {
|
||||||
expect(parseRange('1y')).toEqual(60 * 60 * 24 * 365);
|
const d = parseDuration(t.input);
|
||||||
expect(parseRange('1w')).toEqual(60 * 60 * 24 * 7);
|
expect(d).toEqual(t.output);
|
||||||
expect(parseRange('2d')).toEqual(2 * 60 * 60 * 24);
|
expect(formatDuration(d!)).toEqual(t.expectedString || t.input);
|
||||||
expect(parseRange('1h')).toEqual(60 * 60);
|
});
|
||||||
expect(parseRange('7m')).toEqual(7 * 60);
|
});
|
||||||
expect(parseRange('63s')).toEqual(63);
|
});
|
||||||
|
|
||||||
|
describe('should fail to parse invalid durations', () => {
|
||||||
|
const tests = ['1', '1y1m1d', '-1w', '1.5d', 'd', ''];
|
||||||
|
|
||||||
|
tests.forEach(t => {
|
||||||
|
it(t, () => {
|
||||||
|
expect(parseDuration(t)).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -141,7 +208,7 @@ describe('Utils', () => {
|
||||||
options: {
|
options: {
|
||||||
endTime: 1572046620000,
|
endTime: 1572046620000,
|
||||||
expr: 'rate(node_cpu_seconds_total{mode="system"}[1m])',
|
expr: 'rate(node_cpu_seconds_total{mode="system"}[1m])',
|
||||||
range: 3600,
|
range: 60 * 60 * 1000,
|
||||||
resolution: null,
|
resolution: null,
|
||||||
stacked: false,
|
stacked: false,
|
||||||
type: PanelType.Graph,
|
type: PanelType.Graph,
|
||||||
|
@ -152,7 +219,7 @@ describe('Utils', () => {
|
||||||
options: {
|
options: {
|
||||||
endTime: null,
|
endTime: null,
|
||||||
expr: 'node_filesystem_avail_bytes',
|
expr: 'node_filesystem_avail_bytes',
|
||||||
range: 3600,
|
range: 60 * 60 * 1000,
|
||||||
resolution: null,
|
resolution: null,
|
||||||
stacked: false,
|
stacked: false,
|
||||||
type: PanelType.Table,
|
type: PanelType.Table,
|
||||||
|
@ -201,7 +268,7 @@ describe('Utils', () => {
|
||||||
|
|
||||||
describe('range_input', () => {
|
describe('range_input', () => {
|
||||||
it('should return range parsed if its not null', () => {
|
it('should return range parsed if its not null', () => {
|
||||||
expect(parseOption('range_input=2h')).toEqual({ range: 7200 });
|
expect(parseOption('range_input=2h')).toEqual({ range: 2 * 60 * 60 * 1000 });
|
||||||
});
|
});
|
||||||
it('should return empty object for invalid value', () => {
|
it('should return empty object for invalid value', () => {
|
||||||
expect(parseOption('range_input=h')).toEqual({});
|
expect(parseOption('range_input=h')).toEqual({});
|
||||||
|
@ -226,7 +293,7 @@ describe('Utils', () => {
|
||||||
key: '0',
|
key: '0',
|
||||||
options: { expr: 'foo', type: PanelType.Graph, stacked: true, range: 0, endTime: null, resolution: 1 },
|
options: { expr: 'foo', type: PanelType.Graph, stacked: true, range: 0, endTime: null, resolution: 1 },
|
||||||
})
|
})
|
||||||
).toEqual('g0.expr=foo&g0.tab=0&g0.stacked=1&g0.range_input=0y&g0.step_input=1');
|
).toEqual('g0.expr=foo&g0.tab=0&g0.stacked=1&g0.range_input=0s&g0.step_input=1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue