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:
Yury Molodov 2021-12-21 21:19:33 +03:00 committed by GitHub
parent ce333f28d8
commit 4b40acd964
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 188 additions and 108 deletions

View file

@ -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"
]
}

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.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

File diff suppressed because one or more lines are too long

View file

@ -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/>

View file

@ -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:&nbsp;
</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"/>,&nbsp;
<InlineBtn handler={() => setDurationString("1h")} text="1h"/>,&nbsp;
<InlineBtn handler={() => setDurationString("1h 30m")} text="1h 30m"/>
</Typography>
</Box>
</>;
};
export default TimeDurationSelector;

View file

@ -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:&nbsp;
</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"/>,&nbsp;
<InlineBtn handler={() => setDurationString("1h")} text="1h"/>,&nbsp;
<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.&nbsp;
<InlineBtn handler={() => dispatch({type: "RUN_QUERY_TO_NOW"})} text="Switch to now"/>

View file

@ -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);

View file

@ -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);