vmui: add support relative time (#2504)

* feat: add support relative time

* app/vmselect: `make vmui-update`

* docs/CHANGELOG.md: document the change

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2022-04-26 15:46:06 +03:00 committed by Aliaksandr Valialkin
parent aa82987d70
commit eae6f68be2
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
12 changed files with 92 additions and 56 deletions

View file

@ -1,7 +1,7 @@
{ {
"files": { "files": {
"main.css": "./static/css/main.d8362c27.css", "main.css": "./static/css/main.d8362c27.css",
"main.js": "./static/js/main.1754e6b5.js", "main.js": "./static/js/main.3e17cf70.js",
"static/js/362.1a2113d4.chunk.js": "./static/js/362.1a2113d4.chunk.js", "static/js/362.1a2113d4.chunk.js": "./static/js/362.1a2113d4.chunk.js",
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js", "static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
"static/media/README.md": "./static/media/README.5e5724daf3ee333540a3.md", "static/media/README.md": "./static/media/README.5e5724daf3ee333540a3.md",
@ -9,6 +9,6 @@
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.d8362c27.css", "static/css/main.d8362c27.css",
"static/js/main.1754e6b5.js" "static/js/main.3e17cf70.js"
] ]
} }

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script defer="defer" src="./static/js/main.1754e6b5.js"></script><link href="./static/css/main.d8362c27.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script defer="defer" src="./static/js/main.3e17cf70.js"></script><link href="./static/css/main.d8362c27.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,46 +1,20 @@
import React, {FC} from "preact/compat"; import React, {FC} from "preact/compat";
import List from "@mui/material/List"; import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem"; import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import dayjs from "dayjs"; import {relativeTimeOptions} from "../../../../utils/time";
interface TimeDurationSelector { interface TimeDurationSelector {
setDuration: (str: string, from: Date) => void; setDuration: ({duration, until, id}: {duration: string, until: Date, id: string}) => void;
} }
interface DurationOption {
duration: string,
title?: string,
from?: () => Date,
}
const durationOptions: DurationOption[] = [
{duration: "5m", title: "Last 5 minutes"},
{duration: "15m", title: "Last 15 minutes"},
{duration: "30m", title: "Last 30 minutes"},
{duration: "1h", title: "Last 1 hour"},
{duration: "3h", title: "Last 3 hours"},
{duration: "6h", title: "Last 6 hours"},
{duration: "12h", title: "Last 12 hours"},
{duration: "24h", title: "Last 24 hours"},
{duration: "2d", title: "Last 2 days"},
{duration: "7d", title: "Last 7 days"},
{duration: "30d", title: "Last 30 days"},
{duration: "90d", title: "Last 90 days"},
{duration: "180d", title: "Last 180 days"},
{duration: "1y", title: "Last 1 year"},
{duration: "1d", from: () => dayjs().subtract(1, "day").endOf("day").toDate(), title: "Yesterday"},
{duration: "1d", from: () => dayjs().endOf("day").toDate(), title: "Today"},
];
const TimeDurationSelector: FC<TimeDurationSelector> = ({setDuration}) => { const TimeDurationSelector: FC<TimeDurationSelector> = ({setDuration}) => {
// setDurationString("5m"))
return <List style={{maxHeight: "168px", overflow: "auto", paddingRight: "15px"}}> return <List style={{maxHeight: "168px", overflow: "auto", paddingRight: "15px"}}>
{durationOptions.map(d => {relativeTimeOptions.map(({id, duration, until, title}) =>
<ListItem key={d.duration} button onClick={() => setDuration(d.duration, d.from ? d.from() : new Date())}> <ListItemButton key={id} onClick={() => setDuration({duration, until: until(), id})}>
<ListItemText primary={d.title || d.duration}/> <ListItemText primary={title || duration}/>
</ListItem>)} </ListItemButton>)}
</List>; </List>;
}; };

View file

@ -41,7 +41,7 @@ export const TimeSelector: FC = () => {
const [until, setUntil] = useState<string>(); const [until, setUntil] = useState<string>();
const [from, setFrom] = useState<string>(); const [from, setFrom] = useState<string>();
const {time: {period: {end, start}}} = useAppState(); const {time: {period: {end, start}, relativeTime}} = useAppState();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
@ -52,10 +52,9 @@ export const TimeSelector: FC = () => {
setFrom(formatDateForNativeInput(dateFromSeconds(start))); setFrom(formatDateForNativeInput(dateFromSeconds(start)));
}, [start]); }, [start]);
const setDuration = (dur: string, from: Date) => { const setDuration = ({duration, until, id}: {duration: string, until: Date, id: string}) => {
dispatch({type: "SET_UNTIL", payload: from}); dispatch({type: "SET_RELATIVE_TIME", payload: {duration, until, id}});
setAnchorEl(null); setAnchorEl(null);
dispatch({type: "SET_DURATION", payload: dur});
}; };
const formatRange = useMemo(() => { const formatRange = useMemo(() => {
@ -80,7 +79,9 @@ export const TimeSelector: FC = () => {
}} }}
startIcon={<QueryBuilderIcon/>} startIcon={<QueryBuilderIcon/>}
onClick={(e) => setAnchorEl(e.currentTarget)}> onClick={(e) => setAnchorEl(e.currentTarget)}>
{formatRange.start} - {formatRange.end} {relativeTime
? relativeTime.replace(/_/g, " ")
: `${formatRange.start} - ${formatRange.end}`}
</Button> </Button>
</Tooltip> </Tooltip>
<Popper <Popper

View file

@ -7,7 +7,8 @@ import {
getDateNowUTC, getDateNowUTC,
getDurationFromPeriod, getDurationFromPeriod,
getTimeperiodForDuration, getTimeperiodForDuration,
getDurationFromMilliseconds getDurationFromMilliseconds,
getRelativeTime
} from "../../utils/time"; } from "../../utils/time";
import {getFromStorage} from "../../utils/storage"; import {getFromStorage} from "../../utils/storage";
import {getDefaultServer} from "../../utils/default-server-url"; import {getDefaultServer} from "../../utils/default-server-url";
@ -17,11 +18,12 @@ import dayjs from "dayjs";
export interface TimeState { export interface TimeState {
duration: string; duration: string;
period: TimeParams; period: TimeParams;
relativeTime?: string;
} }
export interface QueryHistory { export interface QueryHistory {
index: number, index: number;
values: string[] values: string[];
} }
export interface AppState { export interface AppState {
@ -44,6 +46,7 @@ export type Action =
| { type: "SET_QUERY_HISTORY_BY_INDEX", payload: {value: QueryHistory, queryNumber: number} } | { type: "SET_QUERY_HISTORY_BY_INDEX", payload: {value: QueryHistory, queryNumber: number} }
| { type: "SET_QUERY_HISTORY", payload: QueryHistory[] } | { type: "SET_QUERY_HISTORY", payload: QueryHistory[] }
| { type: "SET_DURATION", payload: string } | { type: "SET_DURATION", payload: string }
| { type: "SET_RELATIVE_TIME", payload: {id: string, duration: string, until: Date} }
| { type: "SET_UNTIL", payload: Date } | { type: "SET_UNTIL", payload: Date }
| { type: "SET_FROM", payload: Date } | { type: "SET_FROM", payload: Date }
| { type: "SET_PERIOD", payload: TimePeriod } | { type: "SET_PERIOD", payload: TimePeriod }
@ -53,8 +56,9 @@ export type Action =
| { type: "TOGGLE_AUTOCOMPLETE"} | { type: "TOGGLE_AUTOCOMPLETE"}
| { type: "NO_CACHE"} | { type: "NO_CACHE"}
const duration = getQueryStringValue("g0.range_input", "1h") as string; const {relativeDuration, relativeUntil, relativeTimeId} = getRelativeTime();
const endInput = formatDateToLocal(getQueryStringValue("g0.end_input", getDateNowUTC()) as Date); const duration = relativeDuration || getQueryStringValue("g0.range_input", "1h") as string;
const endInput = relativeUntil || formatDateToLocal(getQueryStringValue("g0.end_input", getDateNowUTC()) as Date);
const query = getQueryArray(); const query = getQueryArray();
export const initialState: AppState = { export const initialState: AppState = {
@ -64,7 +68,8 @@ export const initialState: AppState = {
queryHistory: query.map(q => ({index: 0, values: [q]})), queryHistory: query.map(q => ({index: 0, values: [q]})),
time: { time: {
duration, duration,
period: getTimeperiodForDuration(duration, new Date(endInput)) period: getTimeperiodForDuration(duration, new Date(endInput)),
relativeTime: relativeTimeId,
}, },
queryControls: { queryControls: {
autoRefresh: false, autoRefresh: false,
@ -107,7 +112,17 @@ export function reducer(state: AppState, action: Action): AppState {
time: { time: {
...state.time, ...state.time,
duration: action.payload, duration: action.payload,
period: getTimeperiodForDuration(action.payload, dateFromSeconds(state.time.period.end)) period: getTimeperiodForDuration(action.payload, dateFromSeconds(state.time.period.end)),
relativeTime: ""
}
};
case "SET_RELATIVE_TIME":
return {
...state,
time: {
...state.time,
period: getTimeperiodForDuration(action.payload.duration, new Date(action.payload.until)),
relativeTime: action.payload.id,
} }
}; };
case "SET_UNTIL": case "SET_UNTIL":
@ -115,7 +130,8 @@ export function reducer(state: AppState, action: Action): AppState {
...state, ...state,
time: { time: {
...state.time, ...state.time,
period: getTimeperiodForDuration(state.time.duration, action.payload) period: getTimeperiodForDuration(state.time.duration, action.payload),
relativeTime: ""
} }
}; };
case "SET_FROM": case "SET_FROM":
@ -130,7 +146,8 @@ export function reducer(state: AppState, action: Action): AppState {
time: { time: {
...state.time, ...state.time,
duration: durationFrom, duration: durationFrom,
period: getTimeperiodForDuration(durationFrom, dayjs(state.time.period.end*1000).toDate()) period: getTimeperiodForDuration(durationFrom, dayjs(state.time.period.end*1000).toDate()),
relativeTime: ""
} }
}; };
case "SET_PERIOD": case "SET_PERIOD":
@ -145,7 +162,8 @@ export function reducer(state: AppState, action: Action): AppState {
time: { time: {
...state.time, ...state.time,
duration, duration,
period: getTimeperiodForDuration(duration, action.payload.to) period: getTimeperiodForDuration(duration, action.payload.to),
relativeTime: ""
} }
}; };
case "TOGGLE_AUTOREFRESH": case "TOGGLE_AUTOREFRESH":

View file

@ -54,3 +54,10 @@ export interface DashboardSettings {
filename: string; filename: string;
rows: DashboardRow[]; rows: DashboardRow[];
} }
export interface RelativeTimeOption {
id: string,
duration: string,
until: () => Date,
title: string,
}

View file

@ -5,6 +5,7 @@ const stateToUrlParams = {
"time.duration": "range_input", "time.duration": "range_input",
"time.period.date": "end_input", "time.period.date": "end_input",
"time.period.step": "step_input", "time.period.step": "step_input",
"time.relativeTime": "relative_time",
"displayType": "tab" "displayType": "tab"
}; };

View file

@ -1,7 +1,8 @@
import {TimeParams, TimePeriod} from "../types"; import {RelativeTimeOption, TimeParams, TimePeriod} from "../types";
import dayjs, {UnitTypeShort} from "dayjs"; import dayjs, {UnitTypeShort} from "dayjs";
import duration from "dayjs/plugin/duration"; import duration from "dayjs/plugin/duration";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import {getQueryStringValue} from "./query-string";
dayjs.extend(duration); dayjs.extend(duration);
dayjs.extend(utc); dayjs.extend(utc);
@ -105,5 +106,38 @@ export const checkDurationLimit = (dur: string): string => {
return dur; return dur;
}; };
export const dateFromSeconds = (epochTimeInSeconds: number): Date => export const dateFromSeconds = (epochTimeInSeconds: number): Date => new Date(epochTimeInSeconds * 1000);
new Date(epochTimeInSeconds * 1000);
export const relativeTimeOptions: RelativeTimeOption[] = [
{title: "Last 5 minutes", duration: "5m"},
{title: "Last 15 minutes", duration: "15m"},
{title: "Last 30 minutes", duration: "30m"},
{title: "Last 1 hour", duration: "1h"},
{title: "Last 3 hours", duration: "3h"},
{title: "Last 6 hours", duration: "6h"},
{title: "Last 12 hours", duration: "12h"},
{title: "Last 24 hours", duration: "24h"},
{title: "Last 2 days", duration: "2d"},
{title: "Last 7 days", duration: "7d"},
{title: "Last 30 days", duration: "30d"},
{title: "Last 90 days", duration: "90d"},
{title: "Last 180 days", duration: "180d"},
{title: "Last 1 year", duration: "1y"},
{title: "Yesterday", duration: "1d", until: () => dayjs().subtract(1, "day").endOf("day").toDate()},
{title: "Today", duration: "1d", until: () => dayjs().endOf("day").toDate()},
].map(o => ({
id: o.title.replace(/\s/g, "_").toLocaleLowerCase(),
until: o.until ? o.until : () => dayjs().toDate(),
...o
}));
export const getRelativeTime = (relativeTimeId?: string) => {
const id = relativeTimeId || getQueryStringValue("g0.relative_time", "") as string;
const target = relativeTimeOptions.find(d => d.id === id);
if (!target) return {};
return {
relativeTimeId: id,
relativeDuration: target.duration,
relativeUntil: target.until()
};
};

View file

@ -25,6 +25,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
* FEATURE: allow specifying TLS cipher suites for incoming https requests via `-tlsCipherSuites` command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2404). * FEATURE: allow specifying TLS cipher suites for incoming https requests via `-tlsCipherSuites` command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2404).
* FEATURE: allow specifying TLS cipher suites for mTLS connections between cluster components via `-cluster.tlsCipherSuites` command-line flag. See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#mtls-protection). * FEATURE: allow specifying TLS cipher suites for mTLS connections between cluster components via `-cluster.tlsCipherSuites` command-line flag. See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#mtls-protection).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): shown an empty graph on the selected time range when there is no data on it. Previously `No data to show` placeholder was shown instead of the graph in this case. This prevented from zooming and scrolling of such a graph. * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): shown an empty graph on the selected time range when there is no data on it. Previously `No data to show` placeholder was shown instead of the graph in this case. This prevented from zooming and scrolling of such a graph.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): show the selected `last N minutes/hours/days` in the top right corner. Previously the `start - end` duration was shown instead, which could be hard to interpret. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2402).
* FEATURE: expose `vm_indexdb_items_added_total` and `vm_indexdb_items_added_size_bytes_total` counters at `/metrics` page, which can be used for monitoring the rate for addition of new entries in `indexdb` (aka `inverted index`) alongside the total size in bytes for the added entries. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2471). * FEATURE: expose `vm_indexdb_items_added_total` and `vm_indexdb_items_added_size_bytes_total` counters at `/metrics` page, which can be used for monitoring the rate for addition of new entries in `indexdb` (aka `inverted index`) alongside the total size in bytes for the added entries. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2471).
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): add `drop_common_labels()` function, which drops common `label="name"` pairs from the passed time series. See [these docs](https://docs.victoriametrics.com/MetricsQL.html#drop_common_labels). * FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): add `drop_common_labels()` function, which drops common `label="name"` pairs from the passed time series. See [these docs](https://docs.victoriametrics.com/MetricsQL.html#drop_common_labels).