github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/asynqmon/ui/src/components/QueuesOverviewTable.tsx (about) 1 import React, { useState } from "react"; 2 import clsx from "clsx"; 3 import { Link } from "react-router-dom"; 4 import { makeStyles } from "@material-ui/core/styles"; 5 import Table from "@material-ui/core/Table"; 6 import TableBody from "@material-ui/core/TableBody"; 7 import TableCell from "@material-ui/core/TableCell"; 8 import TableContainer from "@material-ui/core/TableContainer"; 9 import TableHead from "@material-ui/core/TableHead"; 10 import TableRow from "@material-ui/core/TableRow"; 11 import TableSortLabel from "@material-ui/core/TableSortLabel"; 12 import IconButton from "@material-ui/core/IconButton"; 13 import Tooltip from "@material-ui/core/Tooltip"; 14 import PauseCircleFilledIcon from "@material-ui/icons/PauseCircleFilled"; 15 import PlayCircleFilledIcon from "@material-ui/icons/PlayCircleFilled"; 16 import DeleteIcon from "@material-ui/icons/Delete"; 17 import MoreHorizIcon from "@material-ui/icons/MoreHoriz"; 18 import DeleteQueueConfirmationDialog from "./DeleteQueueConfirmationDialog"; 19 import { Queue } from "../api"; 20 import { queueDetailsPath } from "../paths"; 21 import { SortDirection, SortableTableColumn } from "../types/table"; 22 import prettyBytes from "pretty-bytes"; 23 import { percentage } from "../utils"; 24 25 const useStyles = makeStyles((theme) => ({ 26 table: { 27 minWidth: 650, 28 }, 29 fixedCell: { 30 position: "sticky", 31 zIndex: 1, 32 left: 0, 33 background: theme.palette.background.paper, 34 }, 35 })); 36 37 interface QueueWithMetadata extends Queue { 38 requestPending: boolean; // indicates pause/resume/delete request is pending for the queue. 39 } 40 41 interface Props { 42 queues: QueueWithMetadata[]; 43 onPauseClick: (qname: string) => Promise<void>; 44 onResumeClick: (qname: string) => Promise<void>; 45 onDeleteClick: (qname: string) => Promise<void>; 46 } 47 48 enum SortBy { 49 Queue, 50 State, 51 Size, 52 MemoryUsage, 53 Latency, 54 Processed, 55 Failed, 56 ErrorRate, 57 58 None, // no sort support 59 } 60 61 const colConfigs: SortableTableColumn<SortBy>[] = [ 62 { label: "Queue", key: "queue", sortBy: SortBy.Queue, align: "left" }, 63 { label: "State", key: "state", sortBy: SortBy.State, align: "left" }, 64 { 65 label: "Size", 66 key: "size", 67 sortBy: SortBy.Size, 68 align: "right", 69 }, 70 { 71 label: "Memory usage", 72 key: "memory_usage", 73 sortBy: SortBy.MemoryUsage, 74 align: "right", 75 }, 76 { 77 label: "Latency", 78 key: "latency", 79 sortBy: SortBy.Latency, 80 align: "right", 81 }, 82 { 83 label: "Processed", 84 key: "processed", 85 sortBy: SortBy.Processed, 86 align: "right", 87 }, 88 { label: "Failed", key: "failed", sortBy: SortBy.Failed, align: "right" }, 89 { 90 label: "Error rate", 91 key: "error_rate", 92 sortBy: SortBy.ErrorRate, 93 align: "right", 94 }, 95 { label: "Actions", key: "actions", sortBy: SortBy.None, align: "center" }, 96 ]; 97 98 // sortQueues takes a array of queues and return a sorted array. 99 // It returns a new array and leave the original array untouched. 100 function sortQueues( 101 queues: QueueWithMetadata[], 102 cmpFn: (first: QueueWithMetadata, second: QueueWithMetadata) => number 103 ): QueueWithMetadata[] { 104 let copy = [...queues]; 105 copy.sort(cmpFn); 106 return copy; 107 } 108 109 export default function QueuesOverviewTable(props: Props) { 110 const classes = useStyles(); 111 const [sortBy, setSortBy] = useState<SortBy>(SortBy.Queue); 112 const [sortDir, setSortDir] = useState<SortDirection>(SortDirection.Asc); 113 const [queueToDelete, setQueueToDelete] = useState<QueueWithMetadata | null>( 114 null 115 ); 116 const createSortClickHandler = (sortKey: SortBy) => (e: React.MouseEvent) => { 117 if (sortKey === sortBy) { 118 // Toggle sort direction. 119 const nextSortDir = 120 sortDir === SortDirection.Asc ? SortDirection.Desc : SortDirection.Asc; 121 setSortDir(nextSortDir); 122 } else { 123 // Change the sort key. 124 setSortBy(sortKey); 125 } 126 }; 127 128 const cmpFunc = (q1: QueueWithMetadata, q2: QueueWithMetadata): number => { 129 let isQ1Smaller: boolean; 130 switch (sortBy) { 131 case SortBy.Queue: 132 if (q1.queue === q2.queue) return 0; 133 isQ1Smaller = q1.queue < q2.queue; 134 break; 135 case SortBy.State: 136 if (q1.paused === q2.paused) return 0; 137 isQ1Smaller = !q1.paused; 138 break; 139 case SortBy.Size: 140 if (q1.size === q2.size) return 0; 141 isQ1Smaller = q1.size < q2.size; 142 break; 143 case SortBy.MemoryUsage: 144 if (q1.memory_usage_bytes === q2.memory_usage_bytes) return 0; 145 isQ1Smaller = q1.memory_usage_bytes < q2.memory_usage_bytes; 146 break; 147 case SortBy.Latency: 148 if (q1.latency_msec === q2.latency_msec) return 0; 149 isQ1Smaller = q1.latency_msec < q2.latency_msec; 150 break; 151 case SortBy.Processed: 152 if (q1.processed === q2.processed) return 0; 153 isQ1Smaller = q1.processed < q2.processed; 154 break; 155 case SortBy.Failed: 156 if (q1.failed === q2.failed) return 0; 157 isQ1Smaller = q1.failed < q2.failed; 158 break; 159 case SortBy.ErrorRate: 160 const q1ErrorRate = q1.failed / q1.processed; 161 const q2ErrorRate = q2.failed / q2.processed; 162 if (q1ErrorRate === q2ErrorRate) return 0; 163 isQ1Smaller = q1ErrorRate < q2ErrorRate; 164 break; 165 default: 166 // eslint-disable-next-line no-throw-literal 167 throw `Unexpected order by value: ${sortBy}`; 168 } 169 if (sortDir === SortDirection.Asc) { 170 return isQ1Smaller ? -1 : 1; 171 } else { 172 return isQ1Smaller ? 1 : -1; 173 } 174 }; 175 176 const handleDialogClose = () => { 177 setQueueToDelete(null); 178 }; 179 180 return ( 181 <React.Fragment> 182 <TableContainer> 183 <Table className={classes.table} aria-label="queues overview table"> 184 <TableHead> 185 <TableRow> 186 {colConfigs 187 .filter((cfg) => { 188 // Filter out actions column in readonly mode. 189 return !window.READ_ONLY || cfg.key !== "actions"; 190 }) 191 .map((cfg, i) => ( 192 <TableCell 193 key={cfg.key} 194 align={cfg.align} 195 className={clsx(i === 0 && classes.fixedCell)} 196 > 197 {cfg.sortBy !== SortBy.None ? ( 198 <TableSortLabel 199 active={sortBy === cfg.sortBy} 200 direction={sortDir} 201 onClick={createSortClickHandler(cfg.sortBy)} 202 > 203 {cfg.label} 204 </TableSortLabel> 205 ) : ( 206 <div>{cfg.label}</div> 207 )} 208 </TableCell> 209 ))} 210 </TableRow> 211 </TableHead> 212 <TableBody> 213 {sortQueues(props.queues, cmpFunc).map((q) => ( 214 <Row 215 key={q.queue} 216 queue={q} 217 onPauseClick={() => props.onPauseClick(q.queue)} 218 onResumeClick={() => props.onResumeClick(q.queue)} 219 onDeleteClick={() => setQueueToDelete(q)} 220 /> 221 ))} 222 </TableBody> 223 </Table> 224 </TableContainer> 225 <DeleteQueueConfirmationDialog 226 onClose={handleDialogClose} 227 queue={queueToDelete} 228 /> 229 </React.Fragment> 230 ); 231 } 232 233 const useRowStyles = makeStyles((theme) => ({ 234 row: { 235 "&:last-child td": { 236 borderBottomWidth: 0, 237 }, 238 "&:last-child th": { 239 borderBottomWidth: 0, 240 }, 241 }, 242 linkText: { 243 textDecoration: "none", 244 color: theme.palette.text.primary, 245 "&:hover": { 246 textDecoration: "underline", 247 }, 248 }, 249 textGreen: { 250 color: theme.palette.success.dark, 251 }, 252 textRed: { 253 color: theme.palette.error.dark, 254 }, 255 boldCell: { 256 fontWeight: 600, 257 }, 258 fixedCell: { 259 position: "sticky", 260 zIndex: 1, 261 left: 0, 262 background: theme.palette.background.paper, 263 }, 264 actionIconsContainer: { 265 display: "flex", 266 justifyContent: "center", 267 minWidth: "100px", 268 }, 269 })); 270 271 interface RowProps { 272 queue: QueueWithMetadata; 273 onPauseClick: () => void; 274 onResumeClick: () => void; 275 onDeleteClick: () => void; 276 } 277 278 function Row(props: RowProps) { 279 const classes = useRowStyles(); 280 const { queue: q } = props; 281 const [showIcons, setShowIcons] = useState<boolean>(false); 282 return ( 283 <TableRow key={q.queue} className={classes.row}> 284 <TableCell 285 component="th" 286 scope="row" 287 className={clsx(classes.boldCell, classes.fixedCell)} 288 > 289 <Link to={queueDetailsPath(q.queue)} className={classes.linkText}> 290 {q.queue} 291 </Link> 292 </TableCell> 293 <TableCell> 294 {q.paused ? ( 295 <span className={classes.textRed}>paused</span> 296 ) : ( 297 <span className={classes.textGreen}>run</span> 298 )} 299 </TableCell> 300 <TableCell align="right">{q.size}</TableCell> 301 <TableCell align="right">{prettyBytes(q.memory_usage_bytes)}</TableCell> 302 <TableCell align="right">{q.display_latency}</TableCell> 303 <TableCell align="right">{q.processed}</TableCell> 304 <TableCell align="right">{q.failed}</TableCell> 305 <TableCell align="right">{percentage(q.failed, q.processed)}</TableCell> 306 {!window.READ_ONLY && ( 307 <TableCell 308 align="center" 309 onMouseEnter={() => setShowIcons(true)} 310 onMouseLeave={() => setShowIcons(false)} 311 > 312 <div className={classes.actionIconsContainer}> 313 {showIcons ? ( 314 <React.Fragment> 315 {q.paused ? ( 316 <Tooltip title="Resume"> 317 <IconButton 318 color="secondary" 319 onClick={props.onResumeClick} 320 disabled={q.requestPending} 321 size="small" 322 > 323 <PlayCircleFilledIcon fontSize="small" /> 324 </IconButton> 325 </Tooltip> 326 ) : ( 327 <Tooltip title="Pause"> 328 <IconButton 329 color="primary" 330 onClick={props.onPauseClick} 331 disabled={q.requestPending} 332 size="small" 333 > 334 <PauseCircleFilledIcon fontSize="small" /> 335 </IconButton> 336 </Tooltip> 337 )} 338 <Tooltip title="Delete"> 339 <IconButton onClick={props.onDeleteClick} size="small"> 340 <DeleteIcon fontSize="small" /> 341 </IconButton> 342 </Tooltip> 343 </React.Fragment> 344 ) : ( 345 <IconButton size="small"> 346 <MoreHorizIcon fontSize="small" /> 347 </IconButton> 348 )} 349 </div> 350 </TableCell> 351 )} 352 </TableRow> 353 ); 354 }