import React from "react"; import { connect, ConnectedProps } from "react-redux"; import { makeStyles, Theme } from "@material-ui/core/styles"; import Button, { ButtonProps } from "@material-ui/core/Button"; import ButtonGroup from "@material-ui/core/ButtonGroup"; import IconButton from "@material-ui/core/IconButton"; import Popover from "@material-ui/core/Popover"; import Radio from "@material-ui/core/Radio"; import RadioGroup from "@material-ui/core/RadioGroup"; import Checkbox from "@material-ui/core/Checkbox"; import FormControlLabel from "@material-ui/core/FormControlLabel"; import FormControl from "@material-ui/core/FormControl"; import FormGroup from "@material-ui/core/FormGroup"; import FormLabel from "@material-ui/core/FormLabel"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; import ArrowLeftIcon from "@material-ui/icons/ArrowLeft"; import ArrowRightIcon from "@material-ui/icons/ArrowRight"; import FilterListIcon from "@material-ui/icons/FilterList"; import dayjs from "dayjs"; import { currentUnixtime, parseDuration } from "../utils"; import { AppState } from "../store"; import { isDarkTheme } from "../theme"; function mapStateToProps(state: AppState) { return { pollInterval: state.settings.pollInterval }; } const connector = connect(mapStateToProps); type ReduxProps = ConnectedProps; interface Props extends ReduxProps { // Specifies the endtime in Unix time seconds. endTimeSec: number; onEndTimeChange: (t: number, isEndTimeFixed: boolean) => void; // Specifies the duration in seconds. durationSec: number; onDurationChange: (d: number, isEndTimeFixed: boolean) => void; // All available queues. queues: string[]; // Selected queues. selectedQueues: string[]; addQueue: (qname: string) => void; removeQueue: (qname: string) => void; } interface State { endTimeOption: EndTimeOption; durationOption: DurationOption; customEndTime: string; // text shown in input field customDuration: string; // text shown in input field customEndTimeError: string; customDurationError: string; } type EndTimeOption = "real_time" | "freeze_at_now" | "custom"; type DurationOption = "1h" | "6h" | "1d" | "8d" | "30d" | "custom"; const useStyles = makeStyles((theme) => ({ root: { display: "flex", alignItems: "center", }, endTimeCaption: { marginRight: theme.spacing(1), }, shiftButtons: { marginLeft: theme.spacing(1), }, buttonGroupRoot: { height: 29, position: "relative", top: 1, }, endTimeShiftControls: { padding: theme.spacing(1), display: "flex", alignItems: "center", justifyContent: "center", borderBottomColor: theme.palette.divider, borderBottomWidth: 1, borderBottomStyle: "solid", }, leftShiftButtons: { display: "flex", alignItems: "center", marginRight: theme.spacing(2), }, rightShiftButtons: { display: "flex", alignItems: "center", marginLeft: theme.spacing(2), }, controlsContainer: { display: "flex", justifyContent: "flex-end", }, controlSelectorBox: { display: "flex", minWidth: 490, padding: theme.spacing(2), }, controlEndTimeSelector: { width: "50%", }, controlDurationSelector: { width: "50%", }, radioButtonRoot: { paddingTop: theme.spacing(0.5), paddingBottom: theme.spacing(0.5), paddingLeft: theme.spacing(1), paddingRight: theme.spacing(1), }, formControlLabel: { fontSize: 14, }, buttonLabel: { textTransform: "none", fontSize: 12, }, formControlRoot: { width: "100%", margin: 0, }, formLabel: { fontSize: 14, fontWeight: 500, marginBottom: theme.spacing(1), }, customInputField: { marginTop: theme.spacing(1), }, filterButton: { marginLeft: theme.spacing(1), }, queueFilters: { padding: theme.spacing(2), maxHeight: 400, }, checkbox: { padding: 6, }, })); // minute, hour, day in seconds const minute = 60; const hour = 60 * minute; const day = 24 * hour; function getInitialState(endTimeSec: number, durationSec: number): State { let endTimeOption: EndTimeOption = "real_time"; let customEndTime = ""; let durationOption: DurationOption = "1h"; let customDuration = ""; const now = currentUnixtime(); // Account for 1s difference, may just happen to elapse 1s // between the parent component's render and this component's render. if (now <= endTimeSec && endTimeSec <= now + 1) { endTimeOption = "real_time"; } else { endTimeOption = "custom"; customEndTime = new Date(endTimeSec * 1000).toISOString(); } switch (durationSec) { case 1 * hour: durationOption = "1h"; break; case 6 * hour: durationOption = "6h"; break; case 1 * day: durationOption = "1d"; break; case 8 * day: durationOption = "8d"; break; case 30 * day: durationOption = "30d"; break; default: durationOption = "custom"; customDuration = durationSec + "s"; } return { endTimeOption, customEndTime, customEndTimeError: "", durationOption, customDuration, customDurationError: "", }; } function MetricsFetchControls(props: Props) { const classes = useStyles(); const [state, setState] = React.useState( getInitialState(props.endTimeSec, props.durationSec) ); const [timePopoverAnchorElem, setTimePopoverAnchorElem] = React.useState(null); const [queuePopoverAnchorElem, setQueuePopoverAnchorElem] = React.useState(null); const handleEndTimeOptionChange = ( event: React.ChangeEvent ) => { const selectedOpt = (event.target as HTMLInputElement) .value as EndTimeOption; setState((prevState) => ({ ...prevState, endTimeOption: selectedOpt, customEndTime: "", customEndTimeError: "", })); switch (selectedOpt) { case "real_time": props.onEndTimeChange(currentUnixtime(), /*isEndTimeFixed=*/ false); break; case "freeze_at_now": props.onEndTimeChange(currentUnixtime(), /*isEndTimeFixed=*/ true); break; case "custom": // No-op } }; const handleDurationOptionChange = ( event: React.ChangeEvent ) => { const selectedOpt = (event.target as HTMLInputElement) .value as DurationOption; setState((prevState) => ({ ...prevState, durationOption: selectedOpt, customDuration: "", customDurationError: "", })); const isEndTimeFixed = state.endTimeOption !== "real_time"; switch (selectedOpt) { case "1h": props.onDurationChange(1 * hour, isEndTimeFixed); break; case "6h": props.onDurationChange(6 * hour, isEndTimeFixed); break; case "1d": props.onDurationChange(1 * day, isEndTimeFixed); break; case "8d": props.onDurationChange(8 * day, isEndTimeFixed); break; case "30d": props.onDurationChange(30 * day, isEndTimeFixed); break; case "custom": // No-op } }; const handleCustomDurationChange = ( event: React.ChangeEvent ) => { event.persist(); // https://reactjs.org/docs/legacy-event-pooling.html setState((prevState) => ({ ...prevState, customDuration: event.target.value, })); }; const handleCustomEndTimeChange = ( event: React.ChangeEvent ) => { event.persist(); // https://reactjs.org/docs/legacy-event-pooling.html setState((prevState) => ({ ...prevState, customEndTime: event.target.value, })); }; const handleCustomDurationKeyDown = ( event: React.KeyboardEvent ) => { if (event.key === "Enter") { try { const d = parseDuration(state.customDuration); setState((prevState) => ({ ...prevState, durationOption: "custom", customDurationError: "", })); props.onDurationChange(d, state.endTimeOption !== "real_time"); } catch (error) { setState((prevState) => ({ ...prevState, customDurationError: "Duration invalid", })); } } }; const handleCustomEndTimeKeyDown = ( event: React.KeyboardEvent ) => { if (event.key === "Enter") { const timeUsecOrNaN = Date.parse(state.customEndTime); if (isNaN(timeUsecOrNaN)) { setState((prevState) => ({ ...prevState, customEndTimeError: "End time invalid", })); return; } setState((prevState) => ({ ...prevState, endTimeOption: "custom", customEndTimeError: "", })); props.onEndTimeChange( Math.floor(timeUsecOrNaN / 1000), /* isEndTimeFixed= */ true ); } }; const handleOpenTimePopover = ( event: React.MouseEvent ) => { setTimePopoverAnchorElem(event.currentTarget); }; const handleCloseTimePopover = () => { setTimePopoverAnchorElem(null); }; const handleOpenQueuePopover = ( event: React.MouseEvent ) => { setQueuePopoverAnchorElem(event.currentTarget); }; const handleCloseQueuePopover = () => { setQueuePopoverAnchorElem(null); }; const isTimePopoverOpen = Boolean(timePopoverAnchorElem); const isQueuePopoverOpen = Boolean(queuePopoverAnchorElem); React.useEffect(() => { if (state.endTimeOption === "real_time") { const id = setInterval(() => { props.onEndTimeChange(currentUnixtime(), /*isEndTimeFixed=*/ false); }, props.pollInterval * 1000); return () => clearInterval(id); } }); const shiftBy = (deltaSec: number) => { return () => { const now = currentUnixtime(); const endTime = props.endTimeSec + deltaSec; if (now <= endTime) { setState((prevState) => ({ ...prevState, customEndTime: "", endTimeOption: "real_time", })); props.onEndTimeChange(now, /*isEndTimeFixed=*/ false); return; } setState((prevState) => ({ ...prevState, endTimeOption: "custom", customEndTime: new Date(endTime * 1000).toISOString(), })); props.onEndTimeChange(endTime, /*isEndTimeFixed=*/ true); }; }; return (
{formatTime(props.endTimeSec)}
End Time
Duration
Queues {props.queues.map((qname) => ( { if (props.selectedQueues.includes(qname)) { props.removeQueue(qname); } else { props.addQueue(qname); } }} name={qname} className={classes.checkbox} /> } label={qname} classes={{ label: classes.formControlLabel }} /> ))}
); } /****************** Helper functions/components *******************/ function formatTime(unixtime: number): string { const tz = new Date(unixtime * 1000) .toLocaleTimeString("en-us", { timeZoneName: "short" }) .split(" ")[2]; return dayjs.unix(unixtime).format("ddd, DD MMM YYYY HH:mm:ss ") + tz; } interface RadioInputProps { value: string; label: string; } function RadioInput(props: RadioInputProps) { const classes = useStyles(); return ( } label={props.label} /> ); } interface ShiftButtonProps extends ButtonProps { text: string; onClick: () => void; direction: "left" | "right"; dense?: boolean; } const useShiftButtonStyles = makeStyles((theme: Theme) => ({ root: { minWidth: 40, fontWeight: (props: ShiftButtonProps) => (props.dense ? 400 : 500), }, label: { fontSize: 12, textTransform: "none" }, iconRoot: { marginRight: (props: ShiftButtonProps) => props.direction === "left" ? (props.dense ? -8 : -4) : 0, marginLeft: (props: ShiftButtonProps) => props.direction === "right" ? (props.dense ? -8 : -4) : 0, color: (props: ShiftButtonProps) => props.color ? props.color : theme.palette.grey[isDarkTheme(theme) ? 200 : 700], }, })); function ShiftButton(props: ShiftButtonProps) { const classes = useShiftButtonStyles(props); return ( ); } ShiftButton.defaultProps = { dense: false, }; export default connect(mapStateToProps)(MetricsFetchControls);