WIP: Add select checkboxes for queue filters

This commit is contained in:
Ken Hibino
2021-12-18 14:09:48 -08:00
parent eabe96eb48
commit ece5f0a6bb
5 changed files with 160 additions and 30 deletions

View File

@@ -146,7 +146,7 @@ func extractMetricsFetchOptions(r *http.Request) (*metricsFetchOptions, error) {
}
opts.duration = time.Duration(val) * time.Second
}
if t := q.Get("end_time"); t != "" {
if t := q.Get("endtime"); t != "" {
val, err := strconv.Atoi(t)
if err != nil {
return nil, fmt.Errorf("invalid value provided for end_time: %q", t)

View File

@@ -27,17 +27,15 @@ export type MetricsActionTypes =
| GetMetricsSuccessAction
| GetMetricsErrorAction;
export function getMetricsAsync(endTime: number, duration: number) {
export function getMetricsAsync(
endTime: number,
duration: number,
queues: string[]
) {
return async (dispatch: Dispatch<MetricsActionTypes>) => {
dispatch({ type: GET_METRICS_BEGIN });
try {
console.log(
"DEBUG: fetching with endtime=",
endTime,
" duration=",
duration
);
const response = await getMetrics(endTime, duration);
const response = await getMetrics(endTime, duration, queues);
dispatch({ type: GET_METRICS_SUCCESS, payload: response });
} catch (error) {
console.error(`getMetricsAsync: ${toErrorStringWithHttpStatus(error)}`);

View File

@@ -894,13 +894,27 @@ export async function getRedisInfo(): Promise<RedisInfoResponse> {
return resp.data;
}
interface MetricsEndpointParams {
endtime: number;
duration: number;
queues?: string; // comma-separated list of queues
}
export async function getMetrics(
endTime: number,
duration: number
duration: number,
queues: string[]
): Promise<MetricsResponse> {
let params: MetricsEndpointParams = {
endtime: endTime,
duration: duration,
};
if (queues && queues.length > 0) {
params.queues = queues.join(",");
}
const resp = await axios({
method: "get",
url: `${BASE_URL}/metrics?end_time=${endTime}&duration=${duration}`,
url: `${BASE_URL}/metrics?${queryString.stringify(params)}`,
});
return resp.data;
}

View File

@@ -3,16 +3,20 @@ import { connect, ConnectedProps } from "react-redux";
import { makeStyles } 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";
@@ -32,6 +36,13 @@ interface Props extends ReduxProps {
// 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 {
@@ -121,6 +132,12 @@ const useStyles = makeStyles((theme) => ({
customInputField: {
marginTop: theme.spacing(1),
},
filterButton: {
marginLeft: theme.spacing(1),
},
queueFilters: {
padding: theme.spacing(2),
},
}));
// minute, hour, day in seconds
@@ -181,9 +198,11 @@ function MetricsFetchControls(props: Props) {
const [state, setState] = React.useState<State>(
getInitialState(props.endTimeSec, props.durationSec)
);
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
null
);
const [timePopoverAnchorElem, setTimePopoverAnchorElem] =
React.useState<HTMLButtonElement | null>(null);
const [queuePopoverAnchorElem, setQueuePopoverAnchorElem] =
React.useState<HTMLButtonElement | null>(null);
const handleEndTimeOptionChange = (
event: React.ChangeEvent<HTMLInputElement>
@@ -306,16 +325,28 @@ function MetricsFetchControls(props: Props) {
}
};
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
const handleOpenTimePopover = (
event: React.MouseEvent<HTMLButtonElement>
) => {
setTimePopoverAnchorElem(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
const handleCloseTimePopover = () => {
setTimePopoverAnchorElem(null);
};
const open = Boolean(anchorEl);
const id = open ? "control-popover" : undefined;
const handleOpenQueuePopover = (
event: React.MouseEvent<HTMLButtonElement>
) => {
setQueuePopoverAnchorElem(event.currentTarget);
};
const handleCloseQueuePopover = () => {
setQueuePopoverAnchorElem(null);
};
const isTimePopoverOpen = Boolean(timePopoverAnchorElem);
const isQueuePopoverOpen = Boolean(queuePopoverAnchorElem);
React.useEffect(() => {
if (state.endTimeOption === "real_time") {
@@ -359,10 +390,10 @@ function MetricsFetchControls(props: Props) {
</Typography>
<div>
<Button
aria-describedby={id}
aria-describedby={isTimePopoverOpen ? "time-popover" : undefined}
variant="outlined"
color="primary"
onClick={handleButtonClick}
onClick={handleOpenTimePopover}
size="small"
classes={{
label: classes.buttonLabel,
@@ -374,10 +405,10 @@ function MetricsFetchControls(props: Props) {
: state.durationOption}
</Button>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
id={isTimePopoverOpen ? "time-popover" : undefined}
open={isTimePopoverOpen}
anchorEl={timePopoverAnchorElem}
onClose={handleCloseTimePopover}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
@@ -561,6 +592,54 @@ function MetricsFetchControls(props: Props) {
/>
</ButtonGroup>
</div>
<div className={classes.filterButton}>
<IconButton
aria-label="filter"
size="small"
onClick={handleOpenQueuePopover}
>
<FilterListIcon />
</IconButton>
<Popover
id={isQueuePopoverOpen ? "queue-popover" : undefined}
open={isQueuePopoverOpen}
anchorEl={queuePopoverAnchorElem}
onClose={handleCloseQueuePopover}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
>
<FormControl className={classes.queueFilters}>
<FormLabel>Select Queues</FormLabel>
<FormGroup>
{props.queues.map((qname) => (
<FormControlLabel
key={qname}
control={
<Checkbox
checked={props.selectedQueues.includes(qname)}
onChange={() => {
if (props.selectedQueues.includes(qname)) {
props.removeQueue(qname);
} else {
props.addQueue(qname);
}
}}
name={qname}
/>
}
label={qname}
/>
))}
</FormGroup>
</FormControl>
</Popover>
</div>
</div>
);
}

View File

@@ -10,6 +10,7 @@ import WarningIcon from "@material-ui/icons/Warning";
import InfoIcon from "@material-ui/icons/Info";
import prettyBytes from "pretty-bytes";
import { getMetricsAsync } from "../actions/metricsActions";
import { listQueuesAsync } from "../actions/queuesActions";
import { AppState } from "../store";
import QueueMetricsChart from "../components/QueueMetricsChart";
import Tooltip from "../components/Tooltip";
@@ -62,10 +63,14 @@ function mapStateToProps(state: AppState) {
error: state.metrics.error,
data: state.metrics.data,
pollInterval: state.settings.pollInterval,
queues: state.queues.data.map((q) => q.name),
};
}
const connector = connect(mapStateToProps, { getMetricsAsync });
const connector = connect(mapStateToProps, {
getMetricsAsync,
listQueuesAsync,
});
type Props = ConnectedProps<typeof connector>;
const ENDTIME_URL_PARAM_KEY = "end";
@@ -82,10 +87,11 @@ function MetricsView(props: Props) {
const durationStr = query.get(DURATION_URL_PARAM_KEY);
const duration = durationStr ? parseFloat(durationStr) : 60 * 60; // default to 1h
const { pollInterval, getMetricsAsync, data } = props;
const { pollInterval, getMetricsAsync, listQueuesAsync, data } = props;
const [endTimeSec, setEndTimeSec] = React.useState(endTime);
const [durationSec, setDurationSec] = React.useState(duration);
const [selectedQueues, setSelectedQueues] = React.useState<string[]>([]); // TODO: initialize from URL param if any.
const handleEndTimeChange = (endTime: number, isEndTimeFixed: boolean) => {
const urlQuery = isEndTimeFixed
@@ -119,9 +125,35 @@ function MetricsView(props: Props) {
setDurationSec(duration);
};
const handleAddQueue = (qname: string) => {
if (selectedQueues.includes(qname)) {
return;
}
setSelectedQueues(selectedQueues.concat(qname));
};
const handleRemoveQueue = (qname: string) => {
if (selectedQueues.length === 1) {
return; // ensure that selected queues doesn't go down to zero once user selected
}
if (selectedQueues.length === 0) {
// when user first select filter (remove once of the queues),
// we need to lazily initialize the selectedQueues with the rest (all queues but the selected one).
setSelectedQueues(props.queues.filter((q) => q !== qname));
return;
}
setSelectedQueues(selectedQueues.filter((q) => q !== qname));
};
React.useEffect(() => {
getMetricsAsync(endTimeSec, durationSec);
}, [pollInterval, getMetricsAsync, durationSec, endTimeSec]);
listQueuesAsync();
}, [listQueuesAsync]);
React.useEffect(() => {
getMetricsAsync(endTimeSec, durationSec, selectedQueues);
}, [pollInterval, getMetricsAsync, durationSec, endTimeSec, selectedQueues]);
console.log("DEBUG: selectedQueues", selectedQueues);
return (
<Container maxWidth="lg" className={classes.container}>
@@ -131,6 +163,13 @@ function MetricsView(props: Props) {
onEndTimeChange={handleEndTimeChange}
durationSec={durationSec}
onDurationChange={handleDurationChange}
queues={props.queues}
selectedQueues={
// If none are selected (e.g. initial state), no filters should apply.
selectedQueues.length === 0 ? props.queues : selectedQueues
}
addQueue={handleAddQueue}
removeQueue={handleRemoveQueue}
/>
</div>
<Grid container spacing={3}>