Show error alert when data is not available

This commit is contained in:
Ken Hibino 2021-01-09 13:48:49 -08:00
parent 933127cc0e
commit 251b262798
13 changed files with 230 additions and 123 deletions

View File

@ -1,3 +1,4 @@
import { Dispatch } from "redux";
import { import {
deleteQueue, deleteQueue,
listQueues, listQueues,
@ -5,11 +6,12 @@ import {
pauseQueue, pauseQueue,
resumeQueue, resumeQueue,
} from "../api"; } from "../api";
import { Dispatch } from "redux"; import { toErrorString } from "../utils";
// List of queue related action types. // List of queue related action types.
export const LIST_QUEUES_BEGIN = "LIST_QUEUES_BEGIN"; export const LIST_QUEUES_BEGIN = "LIST_QUEUES_BEGIN";
export const LIST_QUEUES_SUCCESS = "LIST_QUEUES_SUCCESS"; export const LIST_QUEUES_SUCCESS = "LIST_QUEUES_SUCCESS";
export const LIST_QUEUES_ERROR = "LIST_QUEUES_ERROR";
export const DELETE_QUEUE_BEGIN = "DELETE_QUEUE_BEGIN"; export const DELETE_QUEUE_BEGIN = "DELETE_QUEUE_BEGIN";
export const DELETE_QUEUE_SUCCESS = "DELETE_QUEUE_SUCCESS"; export const DELETE_QUEUE_SUCCESS = "DELETE_QUEUE_SUCCESS";
export const DELETE_QUEUE_ERROR = "DELETE_QUEUE_ERROR"; export const DELETE_QUEUE_ERROR = "DELETE_QUEUE_ERROR";
@ -29,6 +31,11 @@ interface ListQueuesSuccessAction {
payload: ListQueuesResponse; payload: ListQueuesResponse;
} }
interface ListQueuesErrorAction {
type: typeof LIST_QUEUES_ERROR;
error: string;
}
interface DeleteQueueBeginAction { interface DeleteQueueBeginAction {
type: typeof DELETE_QUEUE_BEGIN; type: typeof DELETE_QUEUE_BEGIN;
queue: string; // name of the queue queue: string; // name of the queue
@ -81,6 +88,7 @@ interface ResumeQueueErrorAction {
export type QueuesActionTypes = export type QueuesActionTypes =
| ListQueuesBeginAction | ListQueuesBeginAction
| ListQueuesSuccessAction | ListQueuesSuccessAction
| ListQueuesErrorAction
| DeleteQueueBeginAction | DeleteQueueBeginAction
| DeleteQueueSuccessAction | DeleteQueueSuccessAction
| DeleteQueueErrorAction | DeleteQueueErrorAction
@ -94,12 +102,19 @@ export type QueuesActionTypes =
export function listQueuesAsync() { export function listQueuesAsync() {
return async (dispatch: Dispatch<QueuesActionTypes>) => { return async (dispatch: Dispatch<QueuesActionTypes>) => {
dispatch({ type: LIST_QUEUES_BEGIN }); dispatch({ type: LIST_QUEUES_BEGIN });
// TODO: try/catch and dispatch error action on failure try {
const response = await listQueues(); const response = await listQueues();
dispatch({ dispatch({
type: LIST_QUEUES_SUCCESS, type: LIST_QUEUES_SUCCESS,
payload: response, payload: response,
}); });
} catch (error) {
console.error(`listQueuesAsync: ${toErrorString(error)}`);
dispatch({
type: LIST_QUEUES_ERROR,
error: error.response.data,
});
}
}; };
} }

View File

@ -1,5 +1,6 @@
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { getRedisInfo, RedisInfoResponse } from "../api"; import { getRedisInfo, RedisInfoResponse } from "../api";
import { toErrorString } from "../utils";
// List of redis-info related action types. // List of redis-info related action types.
export const GET_REDIS_INFO_BEGIN = "GET_REDIS_INFO_BEGIN"; export const GET_REDIS_INFO_BEGIN = "GET_REDIS_INFO_BEGIN";
@ -33,10 +34,10 @@ export function getRedisInfoAsync() {
const response = await getRedisInfo(); const response = await getRedisInfo();
dispatch({ type: GET_REDIS_INFO_SUCCESS, payload: response }); dispatch({ type: GET_REDIS_INFO_SUCCESS, payload: response });
} catch (error) { } catch (error) {
console.error("getRedisInfoAsync: ", error); console.error(`getRedisInfoAsync: ${toErrorString(error)}`);
dispatch({ dispatch({
type: GET_REDIS_INFO_BEGIN, type: GET_REDIS_INFO_ERROR,
error: "Could not fetch redis info", error: error.response.data,
}); });
} }
}; };

View File

@ -5,6 +5,7 @@ import {
listSchedulerEntries, listSchedulerEntries,
ListSchedulerEntriesResponse, ListSchedulerEntriesResponse,
} from "../api"; } from "../api";
import { toErrorString } from "../utils";
// List of scheduler-entry related action types. // List of scheduler-entry related action types.
export const LIST_SCHEDULER_ENTRIES_BEGIN = "LIST_SCHEDULER_ENTRIES_BEGIN"; export const LIST_SCHEDULER_ENTRIES_BEGIN = "LIST_SCHEDULER_ENTRIES_BEGIN";
@ -67,10 +68,10 @@ export function listSchedulerEntriesAsync() {
payload: response, payload: response,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(`listSchedulerEnqueueEventsAsync: ${toErrorString(error)}`);
dispatch({ dispatch({
type: LIST_SCHEDULER_ENTRIES_ERROR, type: LIST_SCHEDULER_ENTRIES_ERROR,
error: "Could not retrieve scheduler entries", error: error.response.data,
}); });
} }
}; };

View File

@ -1,5 +1,6 @@
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { listServers, ListServersResponse } from "../api"; import { listServers, ListServersResponse } from "../api";
import { toErrorString } from "../utils";
// List of server related action types. // List of server related action types.
export const LIST_SERVERS_BEGIN = "LIST_SERVERS_BEGIN"; export const LIST_SERVERS_BEGIN = "LIST_SERVERS_BEGIN";
@ -34,10 +35,10 @@ export function listServersAsync() {
payload: response, payload: response,
}); });
} catch (error) { } catch (error) {
console.error("listServersAsync: ", error); console.error(`listServersAsync: ${toErrorString(error)}`);
dispatch({ dispatch({
type: LIST_SERVERS_ERROR, type: LIST_SERVERS_ERROR,
error: "Could not retrieve servers info", error: error.response.data,
}); });
} }
}; };

View File

@ -11,6 +11,7 @@ import {
DELETE_QUEUE_BEGIN, DELETE_QUEUE_BEGIN,
DELETE_QUEUE_ERROR, DELETE_QUEUE_ERROR,
DELETE_QUEUE_SUCCESS, DELETE_QUEUE_SUCCESS,
LIST_QUEUES_ERROR,
} from "../actions/queuesActions"; } from "../actions/queuesActions";
import { import {
BATCH_DELETE_DEAD_TASKS_SUCCESS, BATCH_DELETE_DEAD_TASKS_SUCCESS,
@ -49,6 +50,7 @@ import { Queue } from "../api";
interface QueuesState { interface QueuesState {
loading: boolean; loading: boolean;
data: QueueInfo[]; data: QueueInfo[];
error: string;
} }
export interface QueueInfo { export interface QueueInfo {
@ -57,7 +59,7 @@ export interface QueueInfo {
requestPending: boolean; // indicates pause/resume/delete action is pending on this queue requestPending: boolean; // indicates pause/resume/delete action is pending on this queue
} }
const initialState: QueuesState = { data: [], loading: false }; const initialState: QueuesState = { data: [], loading: false, error: "" };
function queuesReducer( function queuesReducer(
state = initialState, state = initialState,
@ -72,6 +74,7 @@ function queuesReducer(
return { return {
...state, ...state,
loading: false, loading: false,
error: "",
data: queues.map((q: Queue) => ({ data: queues.map((q: Queue) => ({
name: q.queue, name: q.queue,
currentStats: q, currentStats: q,
@ -79,6 +82,13 @@ function queuesReducer(
})), })),
}; };
case LIST_QUEUES_ERROR:
return {
...state,
loading: false,
error: action.error,
};
case DELETE_QUEUE_BEGIN: case DELETE_QUEUE_BEGIN:
case PAUSE_QUEUE_BEGIN: case PAUSE_QUEUE_BEGIN:
case RESUME_QUEUE_BEGIN: { case RESUME_QUEUE_BEGIN: {

View File

@ -8,6 +8,7 @@ import { RedisInfo } from "../api";
interface RedisInfoState { interface RedisInfoState {
loading: boolean; loading: boolean;
error: string;
address: string; address: string;
data: RedisInfo | null; data: RedisInfo | null;
rawData: string | null; rawData: string | null;
@ -15,6 +16,7 @@ interface RedisInfoState {
const initialState: RedisInfoState = { const initialState: RedisInfoState = {
loading: false, loading: false,
error: "",
address: "", address: "",
data: null, data: null,
rawData: null, rawData: null,
@ -35,11 +37,13 @@ export default function redisInfoReducer(
return { return {
...state, ...state,
loading: false, loading: false,
error: action.error,
}; };
case GET_REDIS_INFO_SUCCESS: case GET_REDIS_INFO_SUCCESS:
return { return {
loading: false, loading: false,
error: "",
address: action.payload.address, address: action.payload.address,
data: action.payload.info, data: action.payload.info,
rawData: action.payload.raw_info, rawData: action.payload.raw_info,

View File

@ -51,7 +51,6 @@ function schedulerEntriesReducer(
data: action.payload.entries, data: action.payload.entries,
}; };
case LIST_SCHEDULER_ENTRIES_ERROR: case LIST_SCHEDULER_ENTRIES_ERROR:
// TODO: set error state
return { return {
...state, ...state,
loading: false, loading: false,

View File

@ -8,11 +8,13 @@ import { ServerInfo } from "../api";
interface ServersState { interface ServersState {
loading: boolean; loading: boolean;
error: string;
data: ServerInfo[]; data: ServerInfo[];
} }
const initialState: ServersState = { const initialState: ServersState = {
loading: false, loading: false,
error: "",
data: [], data: [],
}; };
@ -30,12 +32,14 @@ export default function serversReducer(
case LIST_SERVERS_SUCCESS: case LIST_SERVERS_SUCCESS:
return { return {
loading: false, loading: false,
error: "",
data: action.payload.servers, data: action.payload.servers,
}; };
case LIST_SERVERS_ERROR: case LIST_SERVERS_ERROR:
return { return {
...state, ...state,
error: action.error,
loading: false, loading: false,
}; };

View File

@ -1,3 +1,14 @@
import { AxiosError } from "axios";
// toErrorString returns a string representaion of axios error.
export function toErrorString(error: AxiosError<string>): string {
const { response } = error;
if (!response) {
return "error: no error response data available";
}
return `${response.status} (${response.statusText}): ${response.data}`;
}
interface Duration { interface Duration {
hour: number; hour: number;
minute: number; minute: number;

View File

@ -6,6 +6,8 @@ import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import InfoIcon from "@material-ui/icons/Info"; import InfoIcon from "@material-ui/icons/Info";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import { import {
listQueuesAsync, listQueuesAsync,
pauseQueueAsync, pauseQueueAsync,
@ -67,6 +69,7 @@ function mapStateToProps(state: AppState) {
...q.currentStats, ...q.currentStats,
requestPending: q.requestPending, requestPending: q.requestPending,
})), })),
error: state.queues.error,
pollInterval: state.settings.pollInterval, pollInterval: state.settings.pollInterval,
queueStats: state.queueStats.data, queueStats: state.queueStats.data,
}; };
@ -115,6 +118,15 @@ function DashboardView(props: Props) {
return ( return (
<Container maxWidth="lg" className={classes.container}> <Container maxWidth="lg" className={classes.container}>
<Grid container spacing={3}> <Grid container spacing={3}>
{props.error.length > 0 && (
<Grid item xs={12}>
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
Could not retreive queues live data {" "}
<strong>See the logs for details</strong>
</Alert>
</Grid>
)}
<Grid item xs={6}> <Grid item xs={6}>
<Paper className={classes.paper} variant="outlined"> <Paper className={classes.paper} variant="outlined">
<div className={classes.chartHeader}> <div className={classes.chartHeader}>

View File

@ -6,6 +6,8 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import SyntaxHighlighter from "react-syntax-highlighter"; import SyntaxHighlighter from "react-syntax-highlighter";
import syntaxHighlightStyle from "react-syntax-highlighter/dist/esm/styles/hljs/github"; import syntaxHighlightStyle from "react-syntax-highlighter/dist/esm/styles/hljs/github";
import { getRedisInfoAsync } from "../actions/redisInfoActions"; import { getRedisInfoAsync } from "../actions/redisInfoActions";
@ -23,6 +25,7 @@ const useStyles = makeStyles((theme) => ({
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState) {
return { return {
loading: state.redis.loading, loading: state.redis.loading,
error: state.redis.error,
redisInfo: state.redis.data, redisInfo: state.redis.data,
redisAddress: state.redis.address, redisAddress: state.redis.address,
redisInfoRaw: state.redis.rawData, redisInfoRaw: state.redis.rawData,
@ -50,102 +53,122 @@ function RedisInfoView(props: Props) {
return ( return (
<Container maxWidth="lg" className={classes.container}> <Container maxWidth="lg" className={classes.container}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> {props.error === "" ? (
<Typography variant="h5">Redis Info</Typography>
<Typography variant="subtitle1" color="textSecondary">
Connected to: {props.redisAddress}
</Typography>
</Grid>
{redisInfo !== null && (
<> <>
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="h6" color="textSecondary"> <Typography variant="h5">Redis Info</Typography>
Server <Typography variant="subtitle1" color="textSecondary">
Connected to: {props.redisAddress}
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={3}> {redisInfo !== null && (
<MetricCard title="Version" content={redisInfo.redis_version} /> <>
</Grid> <Grid item xs={12}>
<Grid item xs={3}> <Typography variant="h6" color="textSecondary">
<MetricCard Server
title="Uptime" </Typography>
content={`${redisInfo.uptime_in_days} days`} </Grid>
/> <Grid item xs={3}>
</Grid> <MetricCard
<Grid item xs={6} /> title="Version"
<Grid item xs={12}> content={redisInfo.redis_version}
<Typography variant="h6" color="textSecondary"> />
Memory </Grid>
</Typography> <Grid item xs={3}>
</Grid> <MetricCard
<Grid item xs={3}> title="Uptime"
<MetricCard content={`${redisInfo.uptime_in_days} days`}
title="Used Memory" />
content={redisInfo.used_memory_human} </Grid>
/> <Grid item xs={6} />
</Grid> <Grid item xs={12}>
<Grid item xs={3}> <Typography variant="h6" color="textSecondary">
<MetricCard Memory
title="Peak Memory Used" </Typography>
content={redisInfo.used_memory_peak_human} </Grid>
/> <Grid item xs={3}>
</Grid> <MetricCard
<Grid item xs={3}> title="Used Memory"
<MetricCard content={redisInfo.used_memory_human}
title="Memory Fragmentation Ratio" />
content={redisInfo.mem_fragmentation_ratio} </Grid>
/> <Grid item xs={3}>
</Grid> <MetricCard
<Grid item xs={3} /> title="Peak Memory Used"
<Grid item xs={12}> content={redisInfo.used_memory_peak_human}
<Typography variant="h6" color="textSecondary"> />
Connections </Grid>
</Typography> <Grid item xs={3}>
</Grid> <MetricCard
<Grid item xs={3}> title="Memory Fragmentation Ratio"
<MetricCard content={redisInfo.mem_fragmentation_ratio}
title="Connected Clients" />
content={redisInfo.connected_clients} </Grid>
/> <Grid item xs={3} />
</Grid> <Grid item xs={12}>
<Grid item xs={3}> <Typography variant="h6" color="textSecondary">
<MetricCard Connections
title="Connected Replicas" </Typography>
content={redisInfo.connected_slaves} </Grid>
/> <Grid item xs={3}>
</Grid> <MetricCard
<Grid item xs={6} /> title="Connected Clients"
<Grid item xs={12}> content={redisInfo.connected_clients}
<Typography variant="h6" color="textSecondary"> />
Persistence </Grid>
</Typography> <Grid item xs={3}>
</Grid> <MetricCard
<Grid item xs={3}> title="Connected Replicas"
<MetricCard content={redisInfo.connected_slaves}
title="Last Save to Disk" />
content={timeAgoUnix(parseInt(redisInfo.rdb_last_save_time))} </Grid>
/> <Grid item xs={6} />
</Grid> <Grid item xs={12}>
<Grid item xs={3}> <Typography variant="h6" color="textSecondary">
<MetricCard Persistence
title="Number of Changes Since Last Dump" </Typography>
content={redisInfo.rdb_changes_since_last_save} </Grid>
/> <Grid item xs={3}>
</Grid> <MetricCard
<Grid item xs={6} /> title="Last Save to Disk"
</> content={timeAgoUnix(
)} parseInt(redisInfo.rdb_last_save_time)
{redisInfoRaw !== null && ( )}
<> />
<Grid item xs={6}> </Grid>
<Typography variant="h6" color="textSecondary"> <Grid item xs={3}>
INFO Command Output <MetricCard
</Typography> title="Number of Changes Since Last Dump"
<SyntaxHighlighter language="yaml" style={syntaxHighlightStyle}> content={redisInfo.rdb_changes_since_last_save}
{redisInfoRaw} />
</SyntaxHighlighter> </Grid>
</Grid> <Grid item xs={6} />
</>
)}
{redisInfoRaw !== null && (
<>
<Grid item xs={6}>
<Typography variant="h6" color="textSecondary">
INFO Command Output
</Typography>
<SyntaxHighlighter
language="yaml"
style={syntaxHighlightStyle}
>
{redisInfoRaw}
</SyntaxHighlighter>
</Grid>
</>
)}
</> </>
) : (
<Grid item xs={12}>
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
Could not retreive redis live data {" "}
<strong>See the logs for details</strong>
</Alert>
</Grid>
)} )}
</Grid> </Grid>
</Container> </Container>

View File

@ -6,6 +6,8 @@ import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import SchedulerEntriesTable from "../components/SchedulerEntriesTable"; import SchedulerEntriesTable from "../components/SchedulerEntriesTable";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import { AppState } from "../store"; import { AppState } from "../store";
import { listSchedulerEntriesAsync } from "../actions/schedulerEntriesActions"; import { listSchedulerEntriesAsync } from "../actions/schedulerEntriesActions";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
@ -30,6 +32,7 @@ const useStyles = makeStyles((theme) => ({
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState) {
return { return {
loading: state.schedulerEntries.loading, loading: state.schedulerEntries.loading,
error: state.schedulerEntries.error,
entries: state.schedulerEntries.data, entries: state.schedulerEntries.data,
pollInterval: state.settings.pollInterval, pollInterval: state.settings.pollInterval,
}; };
@ -48,14 +51,24 @@ function SchedulersView(props: Props) {
return ( return (
<Container maxWidth="lg" className={classes.container}> <Container maxWidth="lg" className={classes.container}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> {props.error === "" ? (
<Paper className={classes.paper} variant="outlined"> <Grid item xs={12}>
<Typography variant="h6" className={classes.heading}> <Paper className={classes.paper} variant="outlined">
Scheduler Entries <Typography variant="h6" className={classes.heading}>
</Typography> Scheduler Entries
<SchedulerEntriesTable entries={props.entries} /> </Typography>
</Paper> <SchedulerEntriesTable entries={props.entries} />
</Grid> </Paper>
</Grid>
) : (
<Grid item xs={12}>
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
Could not retreive scheduler entries live data {" "}
<strong>See the logs for details</strong>
</Alert>
</Grid>
)}
</Grid> </Grid>
</Container> </Container>
); );

View File

@ -5,6 +5,8 @@ import { makeStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import ServersTable from "../components/ServersTable"; import ServersTable from "../components/ServersTable";
import { listServersAsync } from "../actions/serversActions"; import { listServersAsync } from "../actions/serversActions";
import { AppState } from "../store"; import { AppState } from "../store";
@ -30,6 +32,7 @@ const useStyles = makeStyles((theme) => ({
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState) {
return { return {
loading: state.servers.loading, loading: state.servers.loading,
error: state.servers.error,
servers: state.servers.data, servers: state.servers.data,
pollInterval: state.settings.pollInterval, pollInterval: state.settings.pollInterval,
}; };
@ -48,14 +51,24 @@ function ServersView(props: Props) {
return ( return (
<Container maxWidth="lg" className={classes.container}> <Container maxWidth="lg" className={classes.container}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> {props.error === "" ? (
<Paper className={classes.paper} variant="outlined"> <Grid item xs={12}>
<Typography variant="h6" className={classes.heading}> <Paper className={classes.paper} variant="outlined">
Servers <Typography variant="h6" className={classes.heading}>
</Typography> Servers
<ServersTable servers={props.servers} /> </Typography>
</Paper> <ServersTable servers={props.servers} />
</Grid> </Paper>
</Grid>
) : (
<Grid item xs={12}>
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
Could not retreive servers live data {" "}
<strong>See the logs for details</strong>
</Alert>
</Grid>
)}
</Grid> </Grid>
</Container> </Container>
); );