mirror of
https://github.com/hibiken/asynqmon.git
synced 2025-10-26 00:06:13 +08:00
WIP: Add select checkboxes for queue filters
This commit is contained in:
@@ -146,7 +146,7 @@ func extractMetricsFetchOptions(r *http.Request) (*metricsFetchOptions, error) {
|
|||||||
}
|
}
|
||||||
opts.duration = time.Duration(val) * time.Second
|
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)
|
val, err := strconv.Atoi(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid value provided for end_time: %q", t)
|
return nil, fmt.Errorf("invalid value provided for end_time: %q", t)
|
||||||
|
|||||||
@@ -27,17 +27,15 @@ export type MetricsActionTypes =
|
|||||||
| GetMetricsSuccessAction
|
| GetMetricsSuccessAction
|
||||||
| GetMetricsErrorAction;
|
| GetMetricsErrorAction;
|
||||||
|
|
||||||
export function getMetricsAsync(endTime: number, duration: number) {
|
export function getMetricsAsync(
|
||||||
|
endTime: number,
|
||||||
|
duration: number,
|
||||||
|
queues: string[]
|
||||||
|
) {
|
||||||
return async (dispatch: Dispatch<MetricsActionTypes>) => {
|
return async (dispatch: Dispatch<MetricsActionTypes>) => {
|
||||||
dispatch({ type: GET_METRICS_BEGIN });
|
dispatch({ type: GET_METRICS_BEGIN });
|
||||||
try {
|
try {
|
||||||
console.log(
|
const response = await getMetrics(endTime, duration, queues);
|
||||||
"DEBUG: fetching with endtime=",
|
|
||||||
endTime,
|
|
||||||
" duration=",
|
|
||||||
duration
|
|
||||||
);
|
|
||||||
const response = await getMetrics(endTime, duration);
|
|
||||||
dispatch({ type: GET_METRICS_SUCCESS, payload: response });
|
dispatch({ type: GET_METRICS_SUCCESS, payload: response });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`getMetricsAsync: ${toErrorStringWithHttpStatus(error)}`);
|
console.error(`getMetricsAsync: ${toErrorStringWithHttpStatus(error)}`);
|
||||||
|
|||||||
@@ -894,13 +894,27 @@ export async function getRedisInfo(): Promise<RedisInfoResponse> {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MetricsEndpointParams {
|
||||||
|
endtime: number;
|
||||||
|
duration: number;
|
||||||
|
queues?: string; // comma-separated list of queues
|
||||||
|
}
|
||||||
|
|
||||||
export async function getMetrics(
|
export async function getMetrics(
|
||||||
endTime: number,
|
endTime: number,
|
||||||
duration: number
|
duration: number,
|
||||||
|
queues: string[]
|
||||||
): Promise<MetricsResponse> {
|
): Promise<MetricsResponse> {
|
||||||
|
let params: MetricsEndpointParams = {
|
||||||
|
endtime: endTime,
|
||||||
|
duration: duration,
|
||||||
|
};
|
||||||
|
if (queues && queues.length > 0) {
|
||||||
|
params.queues = queues.join(",");
|
||||||
|
}
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: `${BASE_URL}/metrics?end_time=${endTime}&duration=${duration}`,
|
url: `${BASE_URL}/metrics?${queryString.stringify(params)}`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,20 @@ import { connect, ConnectedProps } from "react-redux";
|
|||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import Button, { ButtonProps } from "@material-ui/core/Button";
|
import Button, { ButtonProps } from "@material-ui/core/Button";
|
||||||
import ButtonGroup from "@material-ui/core/ButtonGroup";
|
import ButtonGroup from "@material-ui/core/ButtonGroup";
|
||||||
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
import Popover from "@material-ui/core/Popover";
|
import Popover from "@material-ui/core/Popover";
|
||||||
import Radio from "@material-ui/core/Radio";
|
import Radio from "@material-ui/core/Radio";
|
||||||
import RadioGroup from "@material-ui/core/RadioGroup";
|
import RadioGroup from "@material-ui/core/RadioGroup";
|
||||||
|
import Checkbox from "@material-ui/core/Checkbox";
|
||||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||||
import FormControl from "@material-ui/core/FormControl";
|
import FormControl from "@material-ui/core/FormControl";
|
||||||
|
import FormGroup from "@material-ui/core/FormGroup";
|
||||||
import FormLabel from "@material-ui/core/FormLabel";
|
import FormLabel from "@material-ui/core/FormLabel";
|
||||||
import TextField from "@material-ui/core/TextField";
|
import TextField from "@material-ui/core/TextField";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
import ArrowLeftIcon from "@material-ui/icons/ArrowLeft";
|
import ArrowLeftIcon from "@material-ui/icons/ArrowLeft";
|
||||||
import ArrowRightIcon from "@material-ui/icons/ArrowRight";
|
import ArrowRightIcon from "@material-ui/icons/ArrowRight";
|
||||||
|
import FilterListIcon from "@material-ui/icons/FilterList";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { currentUnixtime, parseDuration } from "../utils";
|
import { currentUnixtime, parseDuration } from "../utils";
|
||||||
import { AppState } from "../store";
|
import { AppState } from "../store";
|
||||||
@@ -32,6 +36,13 @@ interface Props extends ReduxProps {
|
|||||||
// Specifies the duration in seconds.
|
// Specifies the duration in seconds.
|
||||||
durationSec: number;
|
durationSec: number;
|
||||||
onDurationChange: (d: number, isEndTimeFixed: boolean) => void;
|
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 {
|
interface State {
|
||||||
@@ -121,6 +132,12 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
customInputField: {
|
customInputField: {
|
||||||
marginTop: theme.spacing(1),
|
marginTop: theme.spacing(1),
|
||||||
},
|
},
|
||||||
|
filterButton: {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
queueFilters: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// minute, hour, day in seconds
|
// minute, hour, day in seconds
|
||||||
@@ -181,9 +198,11 @@ function MetricsFetchControls(props: Props) {
|
|||||||
const [state, setState] = React.useState<State>(
|
const [state, setState] = React.useState<State>(
|
||||||
getInitialState(props.endTimeSec, props.durationSec)
|
getInitialState(props.endTimeSec, props.durationSec)
|
||||||
);
|
);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
|
const [timePopoverAnchorElem, setTimePopoverAnchorElem] =
|
||||||
null
|
React.useState<HTMLButtonElement | null>(null);
|
||||||
);
|
|
||||||
|
const [queuePopoverAnchorElem, setQueuePopoverAnchorElem] =
|
||||||
|
React.useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const handleEndTimeOptionChange = (
|
const handleEndTimeOptionChange = (
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
@@ -306,16 +325,28 @@ function MetricsFetchControls(props: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleOpenTimePopover = (
|
||||||
setAnchorEl(event.currentTarget);
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
|
setTimePopoverAnchorElem(event.currentTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleCloseTimePopover = () => {
|
||||||
setAnchorEl(null);
|
setTimePopoverAnchorElem(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = Boolean(anchorEl);
|
const handleOpenQueuePopover = (
|
||||||
const id = open ? "control-popover" : undefined;
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
|
setQueuePopoverAnchorElem(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseQueuePopover = () => {
|
||||||
|
setQueuePopoverAnchorElem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTimePopoverOpen = Boolean(timePopoverAnchorElem);
|
||||||
|
const isQueuePopoverOpen = Boolean(queuePopoverAnchorElem);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (state.endTimeOption === "real_time") {
|
if (state.endTimeOption === "real_time") {
|
||||||
@@ -359,10 +390,10 @@ function MetricsFetchControls(props: Props) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
aria-describedby={id}
|
aria-describedby={isTimePopoverOpen ? "time-popover" : undefined}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleButtonClick}
|
onClick={handleOpenTimePopover}
|
||||||
size="small"
|
size="small"
|
||||||
classes={{
|
classes={{
|
||||||
label: classes.buttonLabel,
|
label: classes.buttonLabel,
|
||||||
@@ -374,10 +405,10 @@ function MetricsFetchControls(props: Props) {
|
|||||||
: state.durationOption}
|
: state.durationOption}
|
||||||
</Button>
|
</Button>
|
||||||
<Popover
|
<Popover
|
||||||
id={id}
|
id={isTimePopoverOpen ? "time-popover" : undefined}
|
||||||
open={open}
|
open={isTimePopoverOpen}
|
||||||
anchorEl={anchorEl}
|
anchorEl={timePopoverAnchorElem}
|
||||||
onClose={handleClose}
|
onClose={handleCloseTimePopover}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: "bottom",
|
vertical: "bottom",
|
||||||
horizontal: "center",
|
horizontal: "center",
|
||||||
@@ -561,6 +592,54 @@ function MetricsFetchControls(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import WarningIcon from "@material-ui/icons/Warning";
|
|||||||
import InfoIcon from "@material-ui/icons/Info";
|
import InfoIcon from "@material-ui/icons/Info";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { getMetricsAsync } from "../actions/metricsActions";
|
import { getMetricsAsync } from "../actions/metricsActions";
|
||||||
|
import { listQueuesAsync } from "../actions/queuesActions";
|
||||||
import { AppState } from "../store";
|
import { AppState } from "../store";
|
||||||
import QueueMetricsChart from "../components/QueueMetricsChart";
|
import QueueMetricsChart from "../components/QueueMetricsChart";
|
||||||
import Tooltip from "../components/Tooltip";
|
import Tooltip from "../components/Tooltip";
|
||||||
@@ -62,10 +63,14 @@ function mapStateToProps(state: AppState) {
|
|||||||
error: state.metrics.error,
|
error: state.metrics.error,
|
||||||
data: state.metrics.data,
|
data: state.metrics.data,
|
||||||
pollInterval: state.settings.pollInterval,
|
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>;
|
type Props = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
const ENDTIME_URL_PARAM_KEY = "end";
|
const ENDTIME_URL_PARAM_KEY = "end";
|
||||||
@@ -82,10 +87,11 @@ function MetricsView(props: Props) {
|
|||||||
const durationStr = query.get(DURATION_URL_PARAM_KEY);
|
const durationStr = query.get(DURATION_URL_PARAM_KEY);
|
||||||
const duration = durationStr ? parseFloat(durationStr) : 60 * 60; // default to 1h
|
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 [endTimeSec, setEndTimeSec] = React.useState(endTime);
|
||||||
const [durationSec, setDurationSec] = React.useState(duration);
|
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 handleEndTimeChange = (endTime: number, isEndTimeFixed: boolean) => {
|
||||||
const urlQuery = isEndTimeFixed
|
const urlQuery = isEndTimeFixed
|
||||||
@@ -119,9 +125,35 @@ function MetricsView(props: Props) {
|
|||||||
setDurationSec(duration);
|
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(() => {
|
React.useEffect(() => {
|
||||||
getMetricsAsync(endTimeSec, durationSec);
|
listQueuesAsync();
|
||||||
}, [pollInterval, getMetricsAsync, durationSec, endTimeSec]);
|
}, [listQueuesAsync]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
getMetricsAsync(endTimeSec, durationSec, selectedQueues);
|
||||||
|
}, [pollInterval, getMetricsAsync, durationSec, endTimeSec, selectedQueues]);
|
||||||
|
|
||||||
|
console.log("DEBUG: selectedQueues", selectedQueues);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="lg" className={classes.container}>
|
<Container maxWidth="lg" className={classes.container}>
|
||||||
@@ -131,6 +163,13 @@ function MetricsView(props: Props) {
|
|||||||
onEndTimeChange={handleEndTimeChange}
|
onEndTimeChange={handleEndTimeChange}
|
||||||
durationSec={durationSec}
|
durationSec={durationSec}
|
||||||
onDurationChange={handleDurationChange}
|
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>
|
</div>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
|
|||||||
Reference in New Issue
Block a user