mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: add custom start range (#1989)
* feat: add custom start range * app/vmselect/vmui: `make vmui-update` Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
ce333f28d8
commit
4b40acd964
10 changed files with 188 additions and 108 deletions
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.a33903a8.css",
|
||||
"main.js": "./static/js/main.23f635e5.js",
|
||||
"main.js": "./static/js/main.2587cf95.js",
|
||||
"static/js/27.85f0e2b0.chunk.js": "./static/js/27.85f0e2b0.chunk.js",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.a33903a8.css",
|
||||
"static/js/main.23f635e5.js"
|
||||
"static/js/main.2587cf95.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.23f635e5.js"></script><link href="./static/css/main.a33903a8.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.2587cf95.js"></script><link href="./static/css/main.a33903a8.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.2587cf95.js
Normal file
2
app/vmselect/vmui/static/js/main.2587cf95.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -20,7 +20,7 @@ export interface QueryConfiguratorProps {
|
|||
|
||||
const QueryConfigurator: FC<QueryConfiguratorProps> = ({error}) => {
|
||||
|
||||
const {serverUrl, query, queryHistory, time: {duration}, queryControls: {autocomplete}} = useAppState();
|
||||
const {serverUrl, query, queryHistory, queryControls: {autocomplete}} = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const queryContainer = useRef<HTMLDivElement>(null);
|
||||
|
@ -123,7 +123,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({error}) => {
|
|||
</Box>}
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<TimeSelector setDuration={onSetDuration} duration={duration}/>
|
||||
<TimeSelector setDuration={onSetDuration}/>
|
||||
</Grid>
|
||||
<Grid item xs={12} pt={1}>
|
||||
<AdditionalSettings/>
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import React, {FC, useEffect, useState} from "react";
|
||||
import {Box, Popover, TextField, Typography} from "@mui/material";
|
||||
import {checkDurationLimit} from "../../../../utils/time";
|
||||
import {TimeDurationPopover} from "./TimeDurationPopover";
|
||||
import {InlineBtn} from "../../../common/InlineBtn";
|
||||
import {useAppState} from "../../../../state/common/StateContext";
|
||||
|
||||
interface TimeDurationSelector {
|
||||
setDuration: (str: string) => void;
|
||||
}
|
||||
|
||||
const TimeDurationSelector: FC<TimeDurationSelector> = ({setDuration}) => {
|
||||
const {time: {duration}} = useAppState();
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<Element | null>(null);
|
||||
const [durationString, setDurationString] = useState<string>(duration);
|
||||
const [durationStringFocused, setFocused] = useState(false);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleDurationChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDurationString(event.target.value);
|
||||
};
|
||||
|
||||
const handlePopoverOpen = (event: React.MouseEvent<Element, MouseEvent>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key !== "Enter") return;
|
||||
const target = event.target as HTMLInputElement;
|
||||
target.blur();
|
||||
setDurationString(target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDurationString(duration);
|
||||
}, [duration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!durationStringFocused) {
|
||||
const value = checkDurationLimit(durationString);
|
||||
setDurationString(value);
|
||||
setDuration(value);
|
||||
}
|
||||
}, [durationString, durationStringFocused]);
|
||||
|
||||
return <>
|
||||
<Box>
|
||||
<TextField label="Duration" value={durationString} onChange={handleDurationChange}
|
||||
variant="standard"
|
||||
fullWidth={true}
|
||||
onKeyUp={onKeyUp}
|
||||
onBlur={() => {
|
||||
setFocused(false);
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
<Typography variant="body2">
|
||||
<span aria-owns={open ? "mouse-over-popover" : undefined}
|
||||
aria-haspopup="true"
|
||||
style={{cursor: "pointer"}}
|
||||
onMouseEnter={handlePopoverOpen}
|
||||
onMouseLeave={handlePopoverClose}>
|
||||
Possible options:
|
||||
</span>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
style={{pointerEvents: "none"}} // important
|
||||
onClose={handlePopoverClose}
|
||||
disableRestoreFocus
|
||||
>
|
||||
<TimeDurationPopover/>
|
||||
</Popover>
|
||||
<InlineBtn handler={() => setDurationString("5m")} text="5m"/>,
|
||||
<InlineBtn handler={() => setDurationString("1h")} text="1h"/>,
|
||||
<InlineBtn handler={() => setDurationString("1h 30m")} text="1h 30m"/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default TimeDurationSelector;
|
|
@ -1,141 +1,98 @@
|
|||
import React, {FC, useEffect, useState} from "react";
|
||||
import {Box, Popover, TextField, Typography} from "@mui/material";
|
||||
import {Box, TextField, Typography} from "@mui/material";
|
||||
import DateTimePicker from "@mui/lab/DateTimePicker";
|
||||
import {TimeDurationPopover} from "./TimeDurationPopover";
|
||||
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
|
||||
import {checkDurationLimit, dateFromSeconds, formatDateForNativeInput} from "../../../../utils/time";
|
||||
import {dateFromSeconds, formatDateForNativeInput} from "../../../../utils/time";
|
||||
import {InlineBtn} from "../../../common/InlineBtn";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import TimeDurationSelector from "./TimeDurationSelector";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface TimeSelectorProps {
|
||||
setDuration: (str: string) => void;
|
||||
duration: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "auto auto",
|
||||
gridTemplateColumns: "200px 1fr",
|
||||
gridGap: "20px",
|
||||
height: "100%",
|
||||
padding: "18px 14px",
|
||||
padding: "20px",
|
||||
borderRadius: "4px",
|
||||
borderColor: "#b9b9b9",
|
||||
borderStyle: "solid",
|
||||
borderWidth: "1px"
|
||||
}
|
||||
},
|
||||
timeControls: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr",
|
||||
gridTemplateRows: "auto 1fr",
|
||||
gridGap: "16px 0",
|
||||
},
|
||||
datePickers: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, 200px)",
|
||||
gridGap: "16px 0",
|
||||
},
|
||||
datePickerItem: {
|
||||
minWidth: "200px",
|
||||
},
|
||||
});
|
||||
|
||||
export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const [durationStringFocused, setFocused] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = React.useState<Element | null>(null);
|
||||
const [until, setUntil] = useState<string>();
|
||||
const [from, setFrom] = useState<string>();
|
||||
|
||||
const {time: {period: {end}, duration}} = useAppState();
|
||||
|
||||
const {time: {period: {end, start}}} = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [durationString, setDurationString] = useState<string>(duration);
|
||||
|
||||
useEffect(() => {
|
||||
setDurationString(duration);
|
||||
}, [duration]);
|
||||
|
||||
useEffect(() => {
|
||||
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
|
||||
}, [end]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!durationStringFocused) {
|
||||
const value = checkDurationLimit(durationString);
|
||||
setDurationString(value);
|
||||
setDuration(value);
|
||||
}
|
||||
}, [durationString, durationStringFocused]);
|
||||
|
||||
const handleDurationChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDurationString(event.target.value);
|
||||
};
|
||||
|
||||
const handlePopoverOpen = (event: React.MouseEvent<Element, MouseEvent>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key !== "Enter") return;
|
||||
const target = event.target as HTMLInputElement;
|
||||
target.blur();
|
||||
setDurationString(target.value);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
|
||||
}, [start]);
|
||||
|
||||
return <Box className={classes.container}>
|
||||
{/*setup duration*/}
|
||||
<Box px={1}>
|
||||
<Box>
|
||||
<TextField label="Duration" value={durationString} onChange={handleDurationChange}
|
||||
variant="standard"
|
||||
fullWidth={true}
|
||||
onKeyUp={onKeyUp}
|
||||
onBlur={() => {setFocused(false);}}
|
||||
onFocus={() => {setFocused(true);}}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
<Typography variant="body2">
|
||||
<span aria-owns={open ? "mouse-over-popover" : undefined}
|
||||
aria-haspopup="true"
|
||||
style={{cursor: "pointer"}}
|
||||
onMouseEnter={handlePopoverOpen}
|
||||
onMouseLeave={handlePopoverClose}>
|
||||
Possible options:
|
||||
</span>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
style={{pointerEvents: "none"}} // important
|
||||
onClose={handlePopoverClose}
|
||||
disableRestoreFocus
|
||||
>
|
||||
<TimeDurationPopover/>
|
||||
</Popover>
|
||||
<InlineBtn handler={() => setDurationString("5m")} text="5m"/>,
|
||||
<InlineBtn handler={() => setDurationString("1h")} text="1h"/>,
|
||||
<InlineBtn handler={() => setDurationString("1h 30m")} text="1h 30m"/>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<TimeDurationSelector setDuration={setDuration}/>
|
||||
</Box>
|
||||
{/*setup end time*/}
|
||||
<Box px={1}>
|
||||
<Box>
|
||||
<DateTimePicker
|
||||
label="Until"
|
||||
ampm={false}
|
||||
value={until}
|
||||
onChange={date => dispatch({type: "SET_UNTIL", payload: date as unknown as Date})}
|
||||
onError={console.log}
|
||||
inputFormat="DD/MM/YYYY HH:mm:ss"
|
||||
mask="__/__/____ __:__:__"
|
||||
renderInput={(params) => <TextField {...params} variant="standard"/>}
|
||||
/>
|
||||
<Box className={classes.timeControls}>
|
||||
<Box className={classes.datePickers}>
|
||||
<Box className={classes.datePickerItem}>
|
||||
<DateTimePicker
|
||||
label="From"
|
||||
ampm={false}
|
||||
value={from}
|
||||
onChange={date => dispatch({type: "SET_FROM", payload: date as unknown as Date})}
|
||||
onError={console.log}
|
||||
inputFormat="DD/MM/YYYY HH:mm:ss"
|
||||
mask="__/__/____ __:__:__"
|
||||
renderInput={(params) => <TextField {...params} variant="standard"/>}
|
||||
maxDate={dayjs(until)}
|
||||
/>
|
||||
</Box>
|
||||
<Box className={classes.datePickerItem}>
|
||||
<DateTimePicker
|
||||
label="Until"
|
||||
ampm={false}
|
||||
value={until}
|
||||
onChange={date => dispatch({type: "SET_UNTIL", payload: date as unknown as Date})}
|
||||
onError={console.log}
|
||||
inputFormat="DD/MM/YYYY HH:mm:ss"
|
||||
mask="__/__/____ __:__:__"
|
||||
renderInput={(params) => <TextField {...params} variant="standard"/>}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt={2}>
|
||||
<Box>
|
||||
<Typography variant="body2">
|
||||
Will be changed to current time for auto-refresh mode.
|
||||
<InlineBtn handler={() => dispatch({type: "RUN_QUERY_TO_NOW"})} text="Switch to now"/>
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
/* eslint max-lines: 0 */
|
||||
import {DisplayType} from "../../components/Home/Configurator/DisplayTypeSwitch";
|
||||
import {TimeParams, TimePeriod} from "../../types";
|
||||
import {dateFromSeconds, formatDateToLocal, getDateNowUTC, getDurationFromPeriod, getTimeperiodForDuration} from "../../utils/time";
|
||||
import {
|
||||
dateFromSeconds,
|
||||
formatDateToLocal,
|
||||
getDateNowUTC,
|
||||
getDurationFromPeriod,
|
||||
getTimeperiodForDuration,
|
||||
getDurationFromMilliseconds
|
||||
} from "../../utils/time";
|
||||
import {getFromStorage} from "../../utils/storage";
|
||||
import {getDefaultServer} from "../../utils/default-server-url";
|
||||
import {getQueryArray, getQueryStringValue} from "../../utils/query-string";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export interface TimeState {
|
||||
duration: string;
|
||||
|
@ -37,6 +45,7 @@ export type Action =
|
|||
| { type: "SET_QUERY_HISTORY", payload: QueryHistory[] }
|
||||
| { type: "SET_DURATION", payload: string }
|
||||
| { type: "SET_UNTIL", payload: Date }
|
||||
| { type: "SET_FROM", payload: Date }
|
||||
| { type: "SET_PERIOD", payload: TimePeriod }
|
||||
| { type: "RUN_QUERY"}
|
||||
| { type: "RUN_QUERY_TO_NOW"}
|
||||
|
@ -109,6 +118,21 @@ export function reducer(state: AppState, action: Action): AppState {
|
|||
period: getTimeperiodForDuration(state.time.duration, action.payload)
|
||||
}
|
||||
};
|
||||
case "SET_FROM":
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const durationFrom = getDurationFromMilliseconds(state.time.period.end*1000 - action.payload.valueOf());
|
||||
return {
|
||||
...state,
|
||||
queryControls: {
|
||||
...state.queryControls,
|
||||
autoRefresh: false // since we're considering this to action to be fired from period selection on chart
|
||||
},
|
||||
time: {
|
||||
...state.time,
|
||||
duration: durationFrom,
|
||||
period: getTimeperiodForDuration(durationFrom, dayjs(state.time.period.end*1000).toDate())
|
||||
}
|
||||
};
|
||||
case "SET_PERIOD":
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const duration = getDurationFromPeriod(action.payload);
|
||||
|
|
|
@ -75,7 +75,7 @@ export const formatDateForNativeInput = (date: Date): string => dayjs(date).form
|
|||
|
||||
export const getDateNowUTC = (): Date => new Date(dayjs().utc().format(dateIsoFormat));
|
||||
|
||||
const getDurationFromMilliseconds = (ms: number): string => {
|
||||
export const getDurationFromMilliseconds = (ms: number): string => {
|
||||
const milliseconds = Math.floor(ms % 1000);
|
||||
const seconds = Math.floor((ms / 1000) % 60);
|
||||
const minutes = Math.floor((ms / 1000 / 60) % 60);
|
||||
|
|
Loading…
Reference in a new issue