mirror of
https://github.com/prometheus/prometheus
synced 2024-12-26 00:23:18 +00: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 { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { createExpressionLink, parsePrometheusFloat, formatRange } from '../../utils/index';
|
||||
import { createExpressionLink, parsePrometheusFloat, formatDuration } from '../../utils/index';
|
||||
|
||||
interface CollapsibleAlertPanelProps {
|
||||
rule: Rule;
|
||||
@ -38,7 +38,7 @@ const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnot
|
||||
</div>
|
||||
{rule.duration > 0 && (
|
||||
<div>
|
||||
<div>for: {formatRange(rule.duration)}</div>
|
||||
<div>for: {formatDuration(rule.duration * 1000)}</div>
|
||||
</div>
|
||||
)}
|
||||
{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';
|
||||
|
||||
const defaultGraphControlProps = {
|
||||
range: 60 * 60 * 24,
|
||||
range: 60 * 60 * 24 * 1000,
|
||||
endTime: 1572100217898,
|
||||
resolution: 10,
|
||||
stacked: false,
|
||||
@ -81,7 +81,7 @@ describe('GraphControls', () => {
|
||||
const timeInput = controls.find(TimeInput);
|
||||
expect(timeInput).toHaveLength(1);
|
||||
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');
|
||||
});
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import TimeInput from './TimeInput';
|
||||
import { parseRange, formatRange } from '../../utils';
|
||||
import { parseDuration, formatDuration } from '../../utils';
|
||||
|
||||
interface GraphControlsProps {
|
||||
range: number;
|
||||
@ -43,10 +43,10 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||
56 * 24 * 60 * 60,
|
||||
365 * 24 * 60 * 60,
|
||||
730 * 24 * 60 * 60,
|
||||
];
|
||||
].map(s => s * 1000);
|
||||
|
||||
onChangeRangeInput = (rangeText: string): void => {
|
||||
const range = parseRange(rangeText);
|
||||
const range = parseDuration(rangeText);
|
||||
if (range === null) {
|
||||
this.changeRangeInput(this.props.range);
|
||||
} else {
|
||||
@ -55,7 +55,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||
};
|
||||
|
||||
changeRangeInput = (range: number): void => {
|
||||
this.rangeRef.current!.value = formatRange(range);
|
||||
this.rangeRef.current!.value = formatDuration(range);
|
||||
};
|
||||
|
||||
increaseRange = (): void => {
|
||||
@ -98,7 +98,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||
</InputGroupAddon>
|
||||
|
||||
<Input
|
||||
defaultValue={formatRange(this.props.range)}
|
||||
defaultValue={formatDuration(this.props.range)}
|
||||
innerRef={this.rangeRef}
|
||||
onBlur={() => this.onChangeRangeInput(this.rangeRef.current!.value)}
|
||||
/>
|
||||
|
@ -35,7 +35,7 @@ interface PanelState {
|
||||
export interface PanelOptions {
|
||||
expr: string;
|
||||
type: PanelType;
|
||||
range: number; // Range in seconds.
|
||||
range: number; // Range in milliseconds.
|
||||
endTime: number | null; // Timestamp in milliseconds.
|
||||
resolution: number | null; // Resolution in seconds.
|
||||
stacked: boolean;
|
||||
@ -49,7 +49,7 @@ export enum PanelType {
|
||||
export const PanelDefaultOptions: PanelOptions = {
|
||||
type: PanelType.Table,
|
||||
expr: '',
|
||||
range: 3600,
|
||||
range: 60 * 60 * 1000,
|
||||
endTime: null,
|
||||
resolution: null,
|
||||
stacked: false,
|
||||
@ -108,8 +108,8 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
|
||||
this.setState({ loading: true });
|
||||
|
||||
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 resolution = this.props.options.resolution || Math.max(Math.floor(this.props.options.range / 250), 1);
|
||||
const startTime = endTime - this.props.options.range / 1000;
|
||||
const resolution = this.props.options.resolution || Math.max(Math.floor(this.props.options.range / 250000), 1);
|
||||
const params: URLSearchParams = new URLSearchParams({
|
||||
query: expr,
|
||||
});
|
||||
|
@ -39,7 +39,7 @@ class TimeInput extends Component<TimeInputProps> {
|
||||
return this.props.time || moment().valueOf();
|
||||
};
|
||||
|
||||
calcShiftRange = () => (this.props.range * 1000) / 2;
|
||||
calcShiftRange = () => this.props.range / 2;
|
||||
|
||||
increaseTime = (): void => {
|
||||
const time = this.getBaseTime() + this.calcShiftRange();
|
||||
|
@ -3,7 +3,7 @@ import { RouteComponentProps } from '@reach/router';
|
||||
import { APIResponse } from '../../hooks/useFetch';
|
||||
import { Alert, Table, Badge } from 'reactstrap';
|
||||
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 { now } from 'moment';
|
||||
|
||||
@ -92,7 +92,7 @@ export const RulesContent: FC<RouteComponentProps & RulesContentProps> = ({ resp
|
||||
<GraphExpressionLink title="expr" text={r.query} expr={r.query} />
|
||||
{r.duration > 0 && (
|
||||
<div>
|
||||
<strong>for:</strong> {formatRange(r.duration)}
|
||||
<strong>for:</strong> {formatDuration(r.duration * 1000)}
|
||||
</div>
|
||||
)}
|
||||
{r.labels && Object.keys(r.labels).length > 0 && (
|
||||
|
@ -43,34 +43,75 @@ export const metricToSeriesName = (labels: { [key: string]: string }) => {
|
||||
return tsName;
|
||||
};
|
||||
|
||||
const rangeUnits: { [unit: string]: number } = {
|
||||
y: 60 * 60 * 24 * 365,
|
||||
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) {
|
||||
export const parseDuration = (durationStr: string): number | null => {
|
||||
if (durationStr === '') {
|
||||
return null;
|
||||
}
|
||||
const value = parseInt(matches[1]);
|
||||
const unit = matches[2];
|
||||
return value * rangeUnits[unit];
|
||||
}
|
||||
|
||||
export function formatRange(range: number): string {
|
||||
for (const unit of Object.keys(rangeUnits)) {
|
||||
if (range % rangeUnits[unit] === 0) {
|
||||
return range / rangeUnits[unit] + unit;
|
||||
}
|
||||
if (durationStr === '0') {
|
||||
// Allow 0 without a unit.
|
||||
return 0;
|
||||
}
|
||||
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 {
|
||||
return moment.utc(timeText).valueOf();
|
||||
@ -161,7 +202,7 @@ export const parseOption = (param: string): Partial<PanelOptions> => {
|
||||
return { stacked: decodedValue === '1' };
|
||||
|
||||
case 'range_input':
|
||||
const range = parseRange(decodedValue);
|
||||
const range = parseDuration(decodedValue);
|
||||
return isPresent(range) ? { range } : {};
|
||||
|
||||
case 'end_input':
|
||||
@ -187,7 +228,7 @@ export const toQueryString = ({ key, options }: PanelMeta) => {
|
||||
formatWithKey('expr', expr),
|
||||
formatWithKey('tab', type === PanelType.Graph ? 0 : 1),
|
||||
formatWithKey('stacked', stacked ? 1 : 0),
|
||||
formatWithKey('range_input', formatRange(range)),
|
||||
formatWithKey('range_input', formatDuration(range)),
|
||||
time ? `${formatWithKey('end_input', time)}&${formatWithKey('moment_input', time)}` : '',
|
||||
isPresent(resolution) ? formatWithKey('step_input', resolution) : '',
|
||||
];
|
||||
|
@ -5,8 +5,8 @@ import {
|
||||
metricToSeriesName,
|
||||
formatTime,
|
||||
parseTime,
|
||||
formatRange,
|
||||
parseRange,
|
||||
formatDuration,
|
||||
parseDuration,
|
||||
humanizeDuration,
|
||||
formatRelative,
|
||||
now,
|
||||
@ -67,25 +67,92 @@ describe('Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatRange', () => {
|
||||
it('returns a time string representing the time in seconds in one unit', () => {
|
||||
expect(formatRange(60 * 60 * 24 * 365)).toEqual('1y');
|
||||
expect(formatRange(60 * 60 * 24 * 7)).toEqual('1w');
|
||||
expect(formatRange(2 * 60 * 60 * 24)).toEqual('2d');
|
||||
expect(formatRange(60 * 60)).toEqual('1h');
|
||||
expect(formatRange(7 * 60)).toEqual('7m');
|
||||
expect(formatRange(63)).toEqual('63s');
|
||||
});
|
||||
});
|
||||
describe('parseDuration and formatDuration', () => {
|
||||
describe('should parse and format durations correctly', () => {
|
||||
const tests: { input: string; output: number; expectedString?: string }[] = [
|
||||
{
|
||||
input: '0',
|
||||
output: 0,
|
||||
expectedString: '0s',
|
||||
},
|
||||
{
|
||||
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', () => {
|
||||
it('returns a time string representing the time in seconds in one unit', () => {
|
||||
expect(parseRange('1y')).toEqual(60 * 60 * 24 * 365);
|
||||
expect(parseRange('1w')).toEqual(60 * 60 * 24 * 7);
|
||||
expect(parseRange('2d')).toEqual(2 * 60 * 60 * 24);
|
||||
expect(parseRange('1h')).toEqual(60 * 60);
|
||||
expect(parseRange('7m')).toEqual(7 * 60);
|
||||
expect(parseRange('63s')).toEqual(63);
|
||||
tests.forEach(t => {
|
||||
it(t.input, () => {
|
||||
const d = parseDuration(t.input);
|
||||
expect(d).toEqual(t.output);
|
||||
expect(formatDuration(d!)).toEqual(t.expectedString || t.input);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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: {
|
||||
endTime: 1572046620000,
|
||||
expr: 'rate(node_cpu_seconds_total{mode="system"}[1m])',
|
||||
range: 3600,
|
||||
range: 60 * 60 * 1000,
|
||||
resolution: null,
|
||||
stacked: false,
|
||||
type: PanelType.Graph,
|
||||
@ -152,7 +219,7 @@ describe('Utils', () => {
|
||||
options: {
|
||||
endTime: null,
|
||||
expr: 'node_filesystem_avail_bytes',
|
||||
range: 3600,
|
||||
range: 60 * 60 * 1000,
|
||||
resolution: null,
|
||||
stacked: false,
|
||||
type: PanelType.Table,
|
||||
@ -201,7 +268,7 @@ describe('Utils', () => {
|
||||
|
||||
describe('range_input', () => {
|
||||
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', () => {
|
||||
expect(parseOption('range_input=h')).toEqual({});
|
||||
@ -226,7 +293,7 @@ describe('Utils', () => {
|
||||
key: '0',
|
||||
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 New Issue
Block a user