mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-03-11 15:34:56 +00:00
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:
parent
aa82987d70
commit
eae6f68be2
12 changed files with 92 additions and 56 deletions
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -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
2
app/vmselect/vmui/static/js/main.3e17cf70.js
Normal file
2
app/vmselect/vmui/static/js/main.3e17cf70.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -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).
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue