From 753970471fb7ef9a50372dbab00b5b69c4684c5c Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Sat, 28 Nov 2020 08:34:12 -0800 Subject: [PATCH] Create DeleteQueueConfirmationDialog component --- main.go | 5 +- ui/src/actions/queuesActions.ts | 50 ++++++++- ui/src/api.ts | 7 ++ .../DeleteQueueConfirmationDialog.tsx | 101 ++++++++++++++++++ ui/src/components/QueuesOverviewTable.tsx | 48 ++------- ui/src/reducers/queuesReducer.ts | 28 +++-- ui/src/views/DashboardView.tsx | 5 +- 7 files changed, 196 insertions(+), 48 deletions(-) create mode 100644 ui/src/components/DeleteQueueConfirmationDialog.tsx diff --git a/main.go b/main.go index 590a058..546bfa9 100644 --- a/main.go +++ b/main.go @@ -91,7 +91,10 @@ func main() { fs := &staticFileServer{staticPath: "ui/build", indexPath: "index.html"} router.PathPrefix("/").Handler(fs) - handler := cors.Default().Handler(router) + c := cors.New(cors.Options{ + AllowedMethods: []string{"GET", "POST", "DELETE"}, + }) + handler := c.Handler(router) srv := &http.Server{ Handler: handler, diff --git a/ui/src/actions/queuesActions.ts b/ui/src/actions/queuesActions.ts index 788f1f3..8462336 100644 --- a/ui/src/actions/queuesActions.ts +++ b/ui/src/actions/queuesActions.ts @@ -1,4 +1,5 @@ import { + deleteQueue, getQueue, GetQueueResponse, listQueues, @@ -14,6 +15,9 @@ export const LIST_QUEUES_SUCCESS = "LIST_QUEUES_SUCCESS"; export const GET_QUEUE_BEGIN = "GET_QUEUE_BEGIN"; export const GET_QUEUE_SUCCESS = "GET_QUEUE_SUCCESS"; export const GET_QUEUE_ERROR = "GET_QUEUE_ERROR"; +export const DELETE_QUEUE_BEGIN = "DELETE_QUEUE_BEGIN"; +export const DELETE_QUEUE_SUCCESS = "DELETE_QUEUE_SUCCESS"; +export const DELETE_QUEUE_ERROR = "DELETE_QUEUE_ERROR"; export const PAUSE_QUEUE_BEGIN = "PAUSE_QUEUE_BEGIN"; export const PAUSE_QUEUE_SUCCESS = "PAUSE_QUEUE_SUCCESS"; export const PAUSE_QUEUE_ERROR = "PAUSE_QUEUE_ERROR"; @@ -47,6 +51,22 @@ interface GetQueueErrorAction { error: string; // error description } +interface DeleteQueueBeginAction { + type: typeof DELETE_QUEUE_BEGIN; + queue: string; // name of the queue +} + +interface DeleteQueueSuccessAction { + type: typeof DELETE_QUEUE_SUCCESS; + queue: string; // name of the queue +} + +interface DeleteQueueErrorAction { + type: typeof DELETE_QUEUE_ERROR; + queue: string; // name of the queue + error: string; // error description +} + interface PauseQueueBeginAction { type: typeof PAUSE_QUEUE_BEGIN; queue: string; // name of the queue @@ -86,6 +106,9 @@ export type QueuesActionTypes = | GetQueueBeginAction | GetQueueSuccessAction | GetQueueErrorAction + | DeleteQueueBeginAction + | DeleteQueueSuccessAction + | DeleteQueueErrorAction | PauseQueueBeginAction | PauseQueueSuccessAction | PauseQueueErrorAction @@ -115,7 +138,8 @@ export function getQueueAsync(qname: string) { queue: qname, payload: response, }); - } catch { + } catch (error) { + console.error(error); dispatch({ type: GET_QUEUE_ERROR, queue: qname, @@ -125,6 +149,30 @@ export function getQueueAsync(qname: string) { }; } +export function deleteQueueAsync(qname: string) { + return async (dispatch: Dispatch) => { + dispatch({ + type: DELETE_QUEUE_BEGIN, + queue: qname, + }); + try { + await deleteQueue(qname); + // FIXME: this action doesn't get dispatched when server stalls + dispatch({ + type: DELETE_QUEUE_SUCCESS, + queue: qname, + }); + } catch (error) { + console.error(error); + dispatch({ + type: DELETE_QUEUE_ERROR, + queue: qname, + error: `Could not delete queue: ${qname}`, + }); + } + }; +} + export function pauseQueueAsync(qname: string) { return async (dispatch: Dispatch) => { dispatch({ type: PAUSE_QUEUE_BEGIN, queue: qname }); diff --git a/ui/src/api.ts b/ui/src/api.ts index 4c8ff42..4c5704b 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -118,6 +118,13 @@ export async function getQueue(qname: string): Promise { return resp.data; } +export async function deleteQueue(qname: string): Promise { + await axios({ + method: "delete", + url: `${BASE_URL}/queues/${qname}`, + }); +} + export async function pauseQueue(qname: string): Promise { await axios({ method: "post", diff --git a/ui/src/components/DeleteQueueConfirmationDialog.tsx b/ui/src/components/DeleteQueueConfirmationDialog.tsx new file mode 100644 index 0000000..90dad55 --- /dev/null +++ b/ui/src/components/DeleteQueueConfirmationDialog.tsx @@ -0,0 +1,101 @@ +import React from "react"; +import { connect, ConnectedProps } from "react-redux"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import { Queue } from "../api"; +import { AppState } from "../store"; +import { deleteQueueAsync } from "../actions/queuesActions"; + +interface Props { + queue: Queue | null; // queue to delete + onClose: () => void; +} + +function mapStateToProps(state: AppState, ownProps: Props) { + let requestPending = false; + if (ownProps.queue !== null) { + const q = state.queues.data.find((q) => q.name === ownProps.queue?.queue); + if (q !== undefined) { + requestPending = q.requestPending; + } + } + return { + requestPending, + }; +} + +const connector = connect(mapStateToProps, { deleteQueueAsync }); + +type ReduxProps = ConnectedProps; + +function DeleteQueueConfirmationDialog(props: Props & ReduxProps) { + const handleDeleteClick = () => { + if (!props.queue) { + return; + } + props.deleteQueueAsync(props.queue.queue); + props.onClose(); + }; + return ( + + {props.queue !== null && + (props.queue.size > 0 ? ( + <> + + Queue is not empty + + + + You are trying to delete a non-emtpy queue "{props.queue.queue} + ". Please empty the queue first before deleting. + + + + + + + ) : ( + <> + + Are you sure you want to delete "{props.queue.queue}"? + + + + You can't undo this action. + + + + + + + + ))} + + ); +} + +export default connector(DeleteQueueConfirmationDialog); diff --git a/ui/src/components/QueuesOverviewTable.tsx b/ui/src/components/QueuesOverviewTable.tsx index fd53c84..585d9b9 100644 --- a/ui/src/components/QueuesOverviewTable.tsx +++ b/ui/src/components/QueuesOverviewTable.tsx @@ -2,12 +2,6 @@ import React, { useState } from "react"; import clsx from "clsx"; import { Link } from "react-router-dom"; import { makeStyles } from "@material-ui/core/styles"; -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogContentText from "@material-ui/core/DialogContentText"; -import DialogTitle from "@material-ui/core/DialogTitle"; import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; @@ -21,6 +15,7 @@ import PauseCircleFilledIcon from "@material-ui/icons/PauseCircleFilled"; import PlayCircleFilledIcon from "@material-ui/icons/PlayCircleFilled"; import DeleteIcon from "@material-ui/icons/Delete"; import MoreHorizIcon from "@material-ui/icons/MoreHoriz"; +import DeleteQueueConfirmationDialog from "./DeleteQueueConfirmationDialog"; import { Queue } from "../api"; import { queueDetailsPath } from "../paths"; @@ -53,13 +48,14 @@ const useStyles = makeStyles((theme) => ({ })); interface QueueWithMetadata extends Queue { - pauseRequestPending: boolean; // indicates pause/resume request is pending for the queue. + requestPending: boolean; // indicates pause/resume/delete request is pending for the queue. } interface Props { queues: QueueWithMetadata[]; onPauseClick: (qname: string) => Promise; onResumeClick: (qname: string) => Promise; + onDeleteClick: (qname: string) => Promise; } enum SortBy { @@ -118,7 +114,9 @@ export default function QueuesOverviewTable(props: Props) { const [sortBy, setSortBy] = useState(SortBy.Queue); const [sortDir, setSortDir] = useState(SortDirection.Asc); const [activeRowIndex, setActiveRowIndex] = useState(-1); - const [queueToDelete, setQueueToDelete] = useState(null); + const [queueToDelete, setQueueToDelete] = useState( + null + ); const total = getAggregateCounts(props.queues); const createSortClickHandler = (sortKey: SortBy) => (e: React.MouseEvent) => { @@ -274,7 +272,7 @@ export default function QueuesOverviewTable(props: Props) { props.onResumeClick(q.queue)} - disabled={q.pauseRequestPending} + disabled={q.requestPending} > @@ -282,7 +280,7 @@ export default function QueuesOverviewTable(props: Props) { props.onPauseClick(q.queue)} - disabled={q.pauseRequestPending} + disabled={q.requestPending} > @@ -330,34 +328,10 @@ export default function QueuesOverviewTable(props: Props) { - - {queueToDelete !== null && ( - <> - - Are you sure you want to delete "{queueToDelete.queue}"? - - - - All of the tasks in the queue will be deleted. You can't undo - this action. - - - - - - - - )} - + queue={queueToDelete} + /> ); } diff --git a/ui/src/reducers/queuesReducer.ts b/ui/src/reducers/queuesReducer.ts index 79d7030..4372959 100644 --- a/ui/src/reducers/queuesReducer.ts +++ b/ui/src/reducers/queuesReducer.ts @@ -9,6 +9,9 @@ import { RESUME_QUEUE_ERROR, RESUME_QUEUE_SUCCESS, GET_QUEUE_SUCCESS, + DELETE_QUEUE_BEGIN, + DELETE_QUEUE_ERROR, + DELETE_QUEUE_SUCCESS, } from "../actions/queuesActions"; import { LIST_ACTIVE_TASKS_SUCCESS, @@ -29,7 +32,7 @@ export interface QueueInfo { name: string; // name of the queue. currentStats: Queue; history: DailyStat[]; - pauseRequestPending: boolean; // indicates pause/resume action is pending on this queue + requestPending: boolean; // indicates pause/resume/delete action is pending on this queue } const initialState: QueuesState = { data: [], loading: false }; @@ -51,7 +54,7 @@ function queuesReducer( name: q.queue, currentStats: q, history: [], - pauseRequestPending: false, + requestPending: false, })), }; @@ -62,21 +65,29 @@ function queuesReducer( name: action.queue, currentStats: action.payload.current, history: action.payload.history, - pauseRequestPending: false, + requestPending: false, }); return { ...state, data: newData }; + case DELETE_QUEUE_BEGIN: case PAUSE_QUEUE_BEGIN: case RESUME_QUEUE_BEGIN: { const newData = state.data.map((queueInfo) => { if (queueInfo.name !== action.queue) { return queueInfo; } - return { ...queueInfo, pauseRequestPending: true }; + return { ...queueInfo, requestPending: true }; }); return { ...state, data: newData }; } + case DELETE_QUEUE_SUCCESS: { + const newData = state.data.filter( + (queueInfo) => queueInfo.name !== action.queue + ); + return { ...state, data: newData }; + } + case PAUSE_QUEUE_SUCCESS: { const newData = state.data.map((queueInfo) => { if (queueInfo.name !== action.queue) { @@ -84,7 +95,7 @@ function queuesReducer( } return { ...queueInfo, - pauseRequestPending: false, + requestPending: false, currentStats: { ...queueInfo.currentStats, paused: true }, }; }); @@ -98,13 +109,14 @@ function queuesReducer( } return { ...queueInfo, - pauseRequestPending: false, + requestPending: false, currentStats: { ...queueInfo.currentStats, paused: false }, }; }); return { ...state, data: newData }; } + case DELETE_QUEUE_ERROR: case PAUSE_QUEUE_ERROR: case RESUME_QUEUE_ERROR: { const newData = state.data.map((queueInfo) => { @@ -113,7 +125,7 @@ function queuesReducer( } return { ...queueInfo, - pauseRequestPending: false, + requestPending: false, }; }); return { ...state, data: newData }; @@ -130,7 +142,7 @@ function queuesReducer( name: action.queue, currentStats: action.payload.stats, history: [], - pauseRequestPending: false, + requestPending: false, }); return { ...state, data: newData }; } diff --git a/ui/src/views/DashboardView.tsx b/ui/src/views/DashboardView.tsx index a848aa0..66eced1 100644 --- a/ui/src/views/DashboardView.tsx +++ b/ui/src/views/DashboardView.tsx @@ -10,6 +10,7 @@ import { listQueuesAsync, pauseQueueAsync, resumeQueueAsync, + deleteQueueAsync, } from "../actions/queuesActions"; import { AppState } from "../store"; import QueueSizeChart from "../components/QueueSizeChart"; @@ -56,7 +57,7 @@ function mapStateToProps(state: AppState) { loading: state.queues.loading, queues: state.queues.data.map((q) => ({ ...q.currentStats, - pauseRequestPending: q.pauseRequestPending, + requestPending: q.requestPending, })), pollInterval: state.settings.pollInterval, }; @@ -66,6 +67,7 @@ const mapDispatchToProps = { listQueuesAsync, pauseQueueAsync, resumeQueueAsync, + deleteQueueAsync, }; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -171,6 +173,7 @@ function DashboardView(props: Props) { queues={queues} onPauseClick={props.pauseQueueAsync} onResumeClick={props.resumeQueueAsync} + onDeleteClick={props.deleteQueueAsync} />