From ece5f0a6bbf73882593b23d0bc1b7008193cd5a7 Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Sat, 18 Dec 2021 14:09:48 -0800 Subject: [PATCH] WIP: Add select checkboxes for queue filters --- metrics_handler.go | 2 +- ui/src/actions/metricsActions.ts | 14 ++- ui/src/api.ts | 18 +++- ui/src/components/MetricsFetchControls.tsx | 109 ++++++++++++++++++--- ui/src/views/MetricsView.tsx | 47 ++++++++- 5 files changed, 160 insertions(+), 30 deletions(-) diff --git a/metrics_handler.go b/metrics_handler.go index f05e52f..23e78f8 100644 --- a/metrics_handler.go +++ b/metrics_handler.go @@ -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) diff --git a/ui/src/actions/metricsActions.ts b/ui/src/actions/metricsActions.ts index 19960fe..3c5e840 100644 --- a/ui/src/actions/metricsActions.ts +++ b/ui/src/actions/metricsActions.ts @@ -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) => { 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)}`); diff --git a/ui/src/api.ts b/ui/src/api.ts index e17df88..e3dfb08 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -894,13 +894,27 @@ export async function getRedisInfo(): Promise { 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 { + 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; } diff --git a/ui/src/components/MetricsFetchControls.tsx b/ui/src/components/MetricsFetchControls.tsx index ba3d2b6..d6f8928 100644 --- a/ui/src/components/MetricsFetchControls.tsx +++ b/ui/src/components/MetricsFetchControls.tsx @@ -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( getInitialState(props.endTimeSec, props.durationSec) ); - const [anchorEl, setAnchorEl] = React.useState( - null - ); + const [timePopoverAnchorElem, setTimePopoverAnchorElem] = + React.useState(null); + + const [queuePopoverAnchorElem, setQueuePopoverAnchorElem] = + React.useState(null); const handleEndTimeOptionChange = ( event: React.ChangeEvent @@ -306,16 +325,28 @@ function MetricsFetchControls(props: Props) { } }; - const handleButtonClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); + const handleOpenTimePopover = ( + event: React.MouseEvent + ) => { + 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 + ) => { + 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) {
+
+ + + + + + Select Queues + + {props.queues.map((qname) => ( + { + if (props.selectedQueues.includes(qname)) { + props.removeQueue(qname); + } else { + props.addQueue(qname); + } + }} + name={qname} + /> + } + label={qname} + /> + ))} + + + +
); } diff --git a/ui/src/views/MetricsView.tsx b/ui/src/views/MetricsView.tsx index 0d6cf91..e876c11 100644 --- a/ui/src/views/MetricsView.tsx +++ b/ui/src/views/MetricsView.tsx @@ -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; 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([]); // 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 ( @@ -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} />