github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/asynqmon/ui/src/components/SchedulerEntriesTable.tsx (about) 1 import React, { useState } from "react"; 2 import clsx from "clsx"; 3 import { makeStyles } from "@material-ui/core/styles"; 4 import IconButton from "@material-ui/core/IconButton"; 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 Modal from "@material-ui/core/Modal"; 12 import Typography from "@material-ui/core/Typography"; 13 import Tooltip from "@material-ui/core/Tooltip"; 14 import HistoryIcon from "@material-ui/icons/History"; 15 import Alert from "@material-ui/lab/Alert"; 16 import AlertTitle from "@material-ui/lab/AlertTitle"; 17 import { SortDirection, SortableTableColumn } from "../types/table"; 18 import TableSortLabel from "@material-ui/core/TableSortLabel"; 19 import SyntaxHighlighter from "./SyntaxHighlighter"; 20 import { SchedulerEntry } from "../api"; 21 import { timeAgo, durationBefore, prettifyPayload } from "../utils"; 22 import SchedulerEnqueueEventsTable from "./SchedulerEnqueueEventsTable"; 23 24 const useStyles = makeStyles((theme) => ({ 25 table: { 26 minWidth: 650, 27 }, 28 noBorder: { 29 border: "none", 30 }, 31 fixedCell: { 32 position: "sticky", 33 zIndex: 1, 34 left: 0, 35 background: theme.palette.background.paper, 36 }, 37 modal: { 38 display: "flex", 39 alignItems: "center", 40 justifyContent: "center", 41 }, 42 modalContent: { 43 background: theme.palette.background.paper, 44 padding: theme.spacing(2), 45 width: "540px", 46 outline: "none", 47 borderRadius: theme.shape.borderRadius, 48 }, 49 eventsTable: { 50 maxHeight: "80vh", 51 }, 52 })); 53 54 enum SortBy { 55 EntryId, 56 Spec, 57 Type, 58 Payload, 59 Options, 60 NextEnqueue, 61 PrevEnqueue, 62 63 None, 64 } 65 66 const colConfigs: SortableTableColumn<SortBy>[] = [ 67 { 68 label: "Entry ID", 69 key: "entry_id", 70 sortBy: SortBy.EntryId, 71 align: "left", 72 }, 73 { 74 label: "Spec", 75 key: "spec", 76 sortBy: SortBy.Spec, 77 align: "left", 78 }, 79 { 80 label: "Type", 81 key: "type", 82 sortBy: SortBy.Type, 83 align: "left", 84 }, 85 { 86 label: "Payload", 87 key: "task_payload", 88 sortBy: SortBy.Payload, 89 align: "left", 90 }, 91 { 92 label: "Options", 93 key: "options", 94 sortBy: SortBy.Options, 95 align: "left", 96 }, 97 { 98 label: "Next Enqueue", 99 key: "next_enqueue", 100 sortBy: SortBy.NextEnqueue, 101 align: "left", 102 }, 103 { 104 label: "Prev Enqueue", 105 key: "prev_enqueue", 106 sortBy: SortBy.PrevEnqueue, 107 align: "left", 108 }, 109 { 110 label: "", 111 key: "show_history", 112 sortBy: SortBy.None, 113 align: "left", 114 }, 115 ]; 116 117 // sortEntries takes a array of entries and return a sorted array. 118 // It returns a new array and leave the original array untouched. 119 function sortEntries( 120 entries: SchedulerEntry[], 121 cmpFn: (first: SchedulerEntry, second: SchedulerEntry) => number 122 ): SchedulerEntry[] { 123 let copy = [...entries]; 124 copy.sort(cmpFn); 125 return copy; 126 } 127 128 interface Props { 129 entries: SchedulerEntry[]; 130 } 131 132 export default function SchedulerEntriesTable(props: Props) { 133 const classes = useStyles(); 134 const [sortBy, setSortBy] = useState<SortBy>(SortBy.EntryId); 135 const [sortDir, setSortDir] = useState<SortDirection>(SortDirection.Asc); 136 const [activeEntryId, setActiveEntryId] = useState<string>(""); 137 138 const createSortClickHandler = (sortKey: SortBy) => (e: React.MouseEvent) => { 139 if (sortKey === sortBy) { 140 // Toggle sort direction. 141 const nextSortDir = 142 sortDir === SortDirection.Asc ? SortDirection.Desc : SortDirection.Asc; 143 setSortDir(nextSortDir); 144 } else { 145 // Change the sort key. 146 setSortBy(sortKey); 147 } 148 }; 149 150 const cmpFunc = (e1: SchedulerEntry, e2: SchedulerEntry): number => { 151 let isE1Smaller: boolean; 152 switch (sortBy) { 153 case SortBy.EntryId: 154 if (e1.id === e2.id) return 0; 155 isE1Smaller = e1.id < e2.id; 156 break; 157 case SortBy.Spec: 158 if (e1.spec === e2.spec) return 0; 159 isE1Smaller = e1.spec < e2.spec; 160 break; 161 case SortBy.Type: 162 if (e1.task_type === e2.task_type) return 0; 163 isE1Smaller = e1.task_type < e2.task_type; 164 break; 165 case SortBy.Payload: 166 if (e1.task_payload === e2.task_payload) return 0; 167 isE1Smaller = e1.task_payload < e2.task_payload; 168 break; 169 case SortBy.Options: 170 if (e1.options === e2.options) return 0; 171 isE1Smaller = e1.options < e2.options; 172 break; 173 case SortBy.NextEnqueue: 174 if (e1.next_enqueue_at === e2.next_enqueue_at) return 0; 175 isE1Smaller = e1.next_enqueue_at < e2.next_enqueue_at; 176 break; 177 case SortBy.PrevEnqueue: 178 const e1PrevEnqueueAt = e1.prev_enqueue_at || ""; 179 const e2PrevEnqueueAt = e2.prev_enqueue_at || ""; 180 if (e1PrevEnqueueAt === e2PrevEnqueueAt) return 0; 181 isE1Smaller = e1PrevEnqueueAt < e2PrevEnqueueAt; 182 break; 183 default: 184 // eslint-disable-next-line no-throw-literal 185 throw `Unexpected order by value: ${sortBy}`; 186 } 187 if (sortDir === SortDirection.Asc) { 188 return isE1Smaller ? -1 : 1; 189 } else { 190 return isE1Smaller ? 1 : -1; 191 } 192 }; 193 194 if (props.entries.length === 0) { 195 return ( 196 <Alert severity="info"> 197 <AlertTitle>Info</AlertTitle> 198 No entries found at this time. 199 </Alert> 200 ); 201 } 202 203 return ( 204 <> 205 <TableContainer> 206 <Table className={classes.table} aria-label="scheduler entries table"> 207 <TableHead> 208 <TableRow> 209 {colConfigs.map((cfg, i) => ( 210 <TableCell 211 key={cfg.key} 212 align={cfg.align} 213 className={clsx(i === 0 && classes.fixedCell)} 214 > 215 <TableSortLabel 216 active={cfg.sortBy === sortBy} 217 direction={sortDir} 218 onClick={createSortClickHandler(cfg.sortBy)} 219 > 220 {cfg.label} 221 </TableSortLabel> 222 </TableCell> 223 ))} 224 </TableRow> 225 </TableHead> 226 <TableBody> 227 {sortEntries(props.entries, cmpFunc).map((entry, idx) => ( 228 <Row 229 key={entry.id} 230 entry={entry} 231 isLastRow={idx === props.entries.length - 1} 232 onShowHistoryClick={() => setActiveEntryId(entry.id)} 233 /> 234 ))} 235 </TableBody> 236 </Table> 237 <Modal 238 open={activeEntryId !== ""} 239 onClose={() => setActiveEntryId("")} 240 className={classes.modal} 241 > 242 <div className={classes.modalContent}> 243 <Typography variant="h6" gutterBottom color="textPrimary"> 244 Recent History 245 </Typography> 246 <SchedulerEnqueueEventsTable entryId={activeEntryId} /> 247 </div> 248 </Modal> 249 </TableContainer> 250 </> 251 ); 252 } 253 254 interface RowProps { 255 entry: SchedulerEntry; 256 isLastRow: boolean; 257 onShowHistoryClick: () => void; 258 } 259 260 const useRowStyles = makeStyles((theme) => ({ 261 rowRoot: { 262 "& > *": { 263 borderBottom: "unset", 264 }, 265 }, 266 noBorder: { 267 border: "none", 268 }, 269 })); 270 271 function Row(props: RowProps) { 272 const { entry, isLastRow } = props; 273 const classes = useRowStyles(); 274 return ( 275 <TableRow className={classes.rowRoot}> 276 <TableCell 277 component="th" 278 scope="row" 279 className={clsx(isLastRow && classes.noBorder)} 280 > 281 {entry.id} 282 </TableCell> 283 <TableCell className={clsx(isLastRow && classes.noBorder)}> 284 {entry.spec} 285 </TableCell> 286 <TableCell className={clsx(isLastRow && classes.noBorder)}> 287 {entry.task_type} 288 </TableCell> 289 <TableCell className={clsx(isLastRow && classes.noBorder)}> 290 <SyntaxHighlighter language="json"> 291 {prettifyPayload(entry.task_payload)} 292 </SyntaxHighlighter> 293 </TableCell> 294 <TableCell className={clsx(isLastRow && classes.noBorder)}> 295 <SyntaxHighlighter language="go"> 296 {entry.options.length > 0 ? entry.options.join(", ") : "No options"} 297 </SyntaxHighlighter> 298 </TableCell> 299 <TableCell className={clsx(isLastRow && classes.noBorder)}> 300 {durationBefore(entry.next_enqueue_at)} 301 </TableCell> 302 <TableCell className={clsx(isLastRow && classes.noBorder)}> 303 {entry.prev_enqueue_at ? timeAgo(entry.prev_enqueue_at) : "N/A"} 304 </TableCell> 305 <TableCell> 306 <Tooltip title="See History"> 307 <IconButton 308 aria-label="expand row" 309 size="small" 310 onClick={props.onShowHistoryClick} 311 > 312 <HistoryIcon /> 313 </IconButton> 314 </Tooltip> 315 </TableCell> 316 </TableRow> 317 ); 318 }