github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/asynqmon/ui/src/components/ServersTable.tsx (about) 1 import React, { useState } from "react"; 2 import { Link } from "react-router-dom"; 3 import clsx from "clsx"; 4 import { makeStyles } from "@material-ui/core/styles"; 5 import Grid from "@material-ui/core/Grid"; 6 import Box from "@material-ui/core/Box"; 7 import Collapse from "@material-ui/core/Collapse"; 8 import IconButton from "@material-ui/core/IconButton"; 9 import Table from "@material-ui/core/Table"; 10 import TableBody from "@material-ui/core/TableBody"; 11 import TableCell from "@material-ui/core/TableCell"; 12 import TableContainer from "@material-ui/core/TableContainer"; 13 import TableHead from "@material-ui/core/TableHead"; 14 import TableRow from "@material-ui/core/TableRow"; 15 import TableSortLabel from "@material-ui/core/TableSortLabel"; 16 import Tooltip from "@material-ui/core/Tooltip"; 17 import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"; 18 import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; 19 import Alert from "@material-ui/lab/Alert"; 20 import AlertTitle from "@material-ui/lab/AlertTitle"; 21 import SyntaxHighlighter from "./SyntaxHighlighter"; 22 import { ServerInfo } from "../api"; 23 import { SortDirection, SortableTableColumn } from "../types/table"; 24 import { timeAgo, uuidPrefix, prettifyPayload } from "../utils"; 25 import { queueDetailsPath } from "../paths"; 26 import Typography from "@material-ui/core/Typography"; 27 28 const useStyles = makeStyles((theme) => ({ 29 table: { 30 minWidth: 650, 31 }, 32 fixedCell: { 33 position: "sticky", 34 zIndex: 1, 35 left: 0, 36 background: theme.palette.background.paper, 37 }, 38 })); 39 40 enum SortBy { 41 HostPID, 42 Status, 43 ActiveWorkers, 44 Queues, 45 Started, 46 } 47 const colConfigs: SortableTableColumn<SortBy>[] = [ 48 { 49 label: "Host:PID", 50 key: "host", 51 sortBy: SortBy.HostPID, 52 align: "left", 53 }, 54 { 55 label: "Started", 56 key: "started", 57 sortBy: SortBy.Started, 58 align: "left", 59 }, 60 { 61 label: "Status", 62 key: "status", 63 sortBy: SortBy.Status, 64 align: "left", 65 }, 66 { 67 label: "Queues", 68 key: "queues", 69 sortBy: SortBy.Queues, 70 align: "left", 71 }, 72 { 73 label: "Active Workers", 74 key: "workers", 75 sortBy: SortBy.ActiveWorkers, 76 align: "left", 77 }, 78 ]; 79 80 // sortServers takes a array of server-infos and return a sorted array. 81 // It returns a new array and leave the original array untouched. 82 function sortServerInfos( 83 entries: ServerInfo[], 84 cmpFn: (first: ServerInfo, second: ServerInfo) => number 85 ): ServerInfo[] { 86 let copy = [...entries]; 87 copy.sort(cmpFn); 88 return copy; 89 } 90 91 interface Props { 92 servers: ServerInfo[]; 93 } 94 95 export default function ServersTable(props: Props) { 96 const classes = useStyles(); 97 const [sortBy, setSortBy] = useState<SortBy>(SortBy.HostPID); 98 const [sortDir, setSortDir] = useState<SortDirection>(SortDirection.Asc); 99 100 const createSortClickHandler = (sortKey: SortBy) => (e: React.MouseEvent) => { 101 if (sortKey === sortBy) { 102 // Toggle sort direction. 103 const nextSortDir = 104 sortDir === SortDirection.Asc ? SortDirection.Desc : SortDirection.Asc; 105 setSortDir(nextSortDir); 106 } else { 107 // Change the sort key. 108 setSortBy(sortKey); 109 } 110 }; 111 112 const cmpFunc = (s1: ServerInfo, s2: ServerInfo): number => { 113 let isS1Smaller = false; 114 switch (sortBy) { 115 case SortBy.HostPID: 116 if (s1.host === s2.host && s1.pid === s2.pid) return 0; 117 if (s1.host === s2.host) { 118 isS1Smaller = s1.pid < s2.pid; 119 } else { 120 isS1Smaller = s1.host < s2.host; 121 } 122 break; 123 case SortBy.Started: 124 const s1StartTime = Date.parse(s1.start_time); 125 const s2StartTime = Date.parse(s2.start_time); 126 if (s1StartTime === s2StartTime) return 0; 127 isS1Smaller = s1StartTime < s2StartTime; 128 break; 129 case SortBy.Status: 130 if (s1.status === s2.status) return 0; 131 isS1Smaller = s1.status < s2.status; 132 break; 133 case SortBy.Queues: 134 const s1Queues = Object.keys(s1.queue_priorities).join(","); 135 const s2Queues = Object.keys(s2.queue_priorities).join(","); 136 if (s1Queues === s2Queues) return 0; 137 isS1Smaller = s1Queues < s2Queues; 138 break; 139 case SortBy.ActiveWorkers: 140 if (s1.active_workers.length === s2.active_workers.length) { 141 return 0; 142 } 143 isS1Smaller = s1.active_workers.length < s2.active_workers.length; 144 break; 145 default: 146 // eslint-disable-next-line no-throw-literal 147 throw `Unexpected order by value: ${sortBy}`; 148 } 149 if (sortDir === SortDirection.Asc) { 150 return isS1Smaller ? -1 : 1; 151 } else { 152 return isS1Smaller ? 1 : -1; 153 } 154 }; 155 156 if (props.servers.length === 0) { 157 return ( 158 <Alert severity="info"> 159 <AlertTitle>Info</AlertTitle> 160 No servers found at this time. 161 </Alert> 162 ); 163 } 164 165 return ( 166 <TableContainer> 167 <Table className={classes.table} aria-label="server info table"> 168 <TableHead> 169 <TableRow> 170 {colConfigs.map((cfg, i) => ( 171 <TableCell 172 key={cfg.key} 173 align={cfg.align} 174 className={clsx(i === 0 && classes.fixedCell)} 175 > 176 <TableSortLabel 177 active={cfg.sortBy === sortBy} 178 direction={sortDir} 179 onClick={createSortClickHandler(cfg.sortBy)} 180 > 181 {cfg.label} 182 </TableSortLabel> 183 </TableCell> 184 ))} 185 <TableCell /> 186 </TableRow> 187 </TableHead> 188 <TableBody> 189 {sortServerInfos(props.servers, cmpFunc).map((srv) => ( 190 <Row key={srv.id} server={srv} /> 191 ))} 192 </TableBody> 193 </Table> 194 </TableContainer> 195 ); 196 } 197 interface RowProps { 198 server: ServerInfo; 199 } 200 201 const useRowStyles = makeStyles((theme) => ({ 202 rowRoot: { 203 "& > *": { 204 borderBottom: "unset", 205 }, 206 }, 207 noBorder: { 208 border: "none", 209 }, 210 link: { 211 color: theme.palette.text.primary, 212 }, 213 })); 214 215 function Row(props: RowProps) { 216 const classes = useRowStyles(); 217 const { server } = props; 218 const [open, setOpen] = useState<boolean>(false); 219 const qnames = Object.keys(server.queue_priorities); 220 return ( 221 <React.Fragment> 222 <TableRow className={classes.rowRoot}> 223 <TableCell> 224 {server.host}:{server.pid} 225 </TableCell> 226 <TableCell>{timeAgo(server.start_time)}</TableCell> 227 <TableCell>{server.status}</TableCell> 228 <TableCell> 229 {qnames.map((qname, idx) => ( 230 <span key={qname}> 231 <Link to={queueDetailsPath(qname)} className={classes.link}> 232 {qname} 233 </Link> 234 {idx === qnames.length - 1 ? "" : ", "} 235 </span> 236 ))} 237 </TableCell> 238 <TableCell> 239 {server.active_workers.length}/{server.concurrency} 240 </TableCell> 241 <TableCell> 242 <Tooltip title={open ? "Hide Details" : "Show Details"}> 243 <IconButton 244 aria-label="expand row" 245 size="small" 246 onClick={() => setOpen(!open)} 247 > 248 {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />} 249 </IconButton> 250 </Tooltip> 251 </TableCell> 252 </TableRow> 253 <TableRow className={classes.rowRoot}> 254 <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}> 255 <Collapse in={open} timeout="auto" unmountOnExit> 256 <Grid container spacing={2}> 257 <Grid item xs={9}> 258 <Typography 259 variant="subtitle1" 260 gutterBottom 261 color="textSecondary" 262 > 263 Active Workers 264 </Typography> 265 <Table size="small" aria-label="active workers"> 266 <TableHead> 267 <TableRow> 268 <TableCell>Task ID</TableCell> 269 <TableCell>Task Payload</TableCell> 270 <TableCell>Queue</TableCell> 271 <TableCell>Started</TableCell> 272 </TableRow> 273 </TableHead> 274 <TableBody> 275 {server.active_workers.map((worker) => ( 276 <TableRow key={worker.task_id}> 277 <TableCell component="th" scope="row"> 278 {uuidPrefix(worker.task_id)} 279 </TableCell> 280 <TableCell> 281 <SyntaxHighlighter 282 language="json" 283 customStyle={{ margin: 0 }} 284 > 285 {prettifyPayload(worker.task_payload)} 286 </SyntaxHighlighter> 287 </TableCell> 288 <TableCell>{worker.queue}</TableCell> 289 <TableCell>{timeAgo(worker.start_time)}</TableCell> 290 </TableRow> 291 ))} 292 </TableBody> 293 </Table> 294 </Grid> 295 <Grid item xs={3}> 296 <Typography 297 variant="subtitle1" 298 gutterBottom 299 color="textSecondary" 300 > 301 Queue Priority 302 </Typography> 303 <Table size="small" aria-label="active workers"> 304 <TableHead> 305 <TableRow> 306 <TableCell>Queue</TableCell> 307 <TableCell align="right">Priority</TableCell> 308 </TableRow> 309 </TableHead> 310 <TableBody> 311 {qnames.map((qname) => ( 312 <TableRow key={qname}> 313 <TableCell> 314 <Link 315 to={queueDetailsPath(qname)} 316 className={classes.link} 317 > 318 {qname} 319 </Link> 320 </TableCell> 321 <TableCell align="right"> 322 {server.queue_priorities[qname]} 323 </TableCell> 324 </TableRow> 325 ))} 326 </TableBody> 327 </Table> 328 <Box margin={2}> 329 <Typography variant="subtitle2" component="span"> 330 Strict Priority:{" "} 331 </Typography> 332 <Typography variant="button" component="span"> 333 {server.strict_priority_enabled ? "ON" : "OFF"} 334 </Typography> 335 </Box> 336 </Grid> 337 </Grid> 338 </Collapse> 339 </TableCell> 340 </TableRow> 341 </React.Fragment> 342 ); 343 }