// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-FileCopyrightText: 2020-2022 grommunio GmbH import React, { Component } from "react"; import PropTypes from "prop-types"; import { Paper, Table, TableHead, TableRow, TableCell, TableBody, Typography, Button, Grid, TableSortLabel, CircularProgress, TextField, InputAdornment, Chip, } from "@mui/material"; import Search from "@mui/icons-material/Search"; import { SYSTEM_ADMIN_WRITE } from "../constants"; import { CapabilityContext } from "../CapabilityContext"; import TableViewContainer from "../components/TableViewContainer"; import { fetchTaskqData, fetchTaskqStatus, startTaskqServer, stopTaskqServer } from "../actions/taskq"; import { setDateTimeString } from "../utils"; import withStyledReduxTable from "../components/withTable"; import defaultTableProptypes from "../proptypes/defaultTableProptypes"; const styles = (theme) => ({ circularProgress: { margin: theme.spacing(1, 0, 1, 0), }, actions: { display: 'flex', flex: 1, margin: theme.spacing(0, 4, 0, 0), justifyContent: 'flex-end', alignItems: 'flex-end', }, buttonGrid: { margin: theme.spacing(0, 2, 2, 2), }, chipGrid: { margin: theme.spacing(2, 2, 0, 2), }, count: { marginLeft: 16, }, chip: { margin: theme.spacing(0, 0.5), }, }); class TasQ extends Component { state = { snackbar: '', }; columns = [ { label: "Command", value: "command" }, { label: "State ", value: "state" }, //The whitespace is necessary because of a country's state { label: "Message", value: "message" }, { label: "Created", value: "created" }, { label: "Updated", value: "updated" }, ]; componentDidMount() { this.props.status().catch((msg) => this.setState({ snackbar: msg })); } handleStart = () => { this.props.start() .then(() => this.setState({ snackbar: 'Success!' })) .catch((msg) => this.setState({ snackbar: msg })); } handleStop = () => { this.props.stop().catch((msg) => this.setState({ snackbar: msg })); } getTaskState(state) { switch(state) { case 0: return "Queued"; case 1: return "Loaded"; case 2: return "Running"; case 3: return "Completed"; case 4: return "Error"; case 5: return "Cancelled"; default: return "Unknown"; } } handleSnackbarClose = () => { this.setState({ snackbar: '', }); this.props.clearSnackbar(); } render() { const { classes, t, taskq, tableState, handleMatch, handleRequestSort, handleEdit } = this.props; const { order, orderBy, match, snackbar } = tableState; const writable = this.context.includes(SYSTEM_ADMIN_WRITE); return ( <TableViewContainer headline={t("Task queue")} href="https://docs.grommunio.com/admin/administration.html#taskq" // subtitle={t("taskq_sub")} snackbar={snackbar || this.state.snackbar} onSnackbarClose={this.handleSnackbarClose} > <Grid container alignItems="flex-end" className={classes.chipGrid}> <Chip className={classes.chip} label={t(taskq.running ? "Running" : "Not running")} color={taskq.running ? "success" : "secondary"} /> <Chip className={classes.chip} label={"Queued: " + taskq.queued} color={"primary"} /> <Chip className={classes.chip} label={"Workers: " + taskq.workers} color={"primary"} /> </Grid> <Grid container alignItems="flex-end" className={classes.buttonGrid}> <Button variant="contained" color="primary" onClick={this.handleStart} disabled={!writable || taskq.running} > {t("Start server")} </Button> <div className={classes.actions}> <TextField value={match} onChange={handleMatch} placeholder={t("Search tasks")} variant={"outlined"} InputProps={{ startAdornment: ( <InputAdornment position="start"> <Search color="secondary" /> </InputAdornment> ), }} color="primary" /> </div> </Grid> <Typography className={classes.count} color="textPrimary"> {t("showingTaskq", { count: taskq.Tasks.length })} </Typography> <Paper elevation={1}> <Table size="small"> <TableHead> <TableRow> {this.columns.map((column) => ( <TableCell key={column.value}> <TableSortLabel active={orderBy === column.value} align="left" direction={orderBy === column.value ? order : "asc"} onClick={handleRequestSort(column.value)} > {t(column.label)} </TableSortLabel> </TableCell> ))} </TableRow> </TableHead> <TableBody> {taskq.Tasks.map((obj, idx) => <TableRow key={idx} hover onClick={handleEdit('/taskq/' + obj.ID)}> <TableCell>{obj.command}</TableCell> <TableCell>{t(this.getTaskState(obj.state))}</TableCell> <TableCell>{obj.message}</TableCell> <TableCell>{obj.created ? setDateTimeString(obj.created) : ''}</TableCell> <TableCell>{obj.updated ? setDateTimeString(obj.updated) : ''}</TableCell> </TableRow> )} </TableBody> </Table> {taskq.Tasks.length < taskq.count && ( <Grid container justifyContent="center"> <CircularProgress color="primary" className={classes.circularProgress} /> </Grid> )} </Paper> </TableViewContainer> ); } } TasQ.contextType = CapabilityContext; TasQ.propTypes = { taskq: PropTypes.object.isRequired, status: PropTypes.func.isRequired, start: PropTypes.func.isRequired, stop: PropTypes.func.isRequired, ...defaultTableProptypes, }; const mapStateToProps = (state) => { return { taskq: state.taskq }; }; const mapDispatchToProps = (dispatch) => { return { fetchTableData: async (params) => await dispatch(fetchTaskqData({ limit: 200, ...params })) .catch((error) => Promise.reject(error)), status: async (params) => await dispatch(fetchTaskqStatus(params)) .catch((error) => Promise.reject(error)), start: async params => await dispatch(startTaskqServer(params)) .catch((error) => Promise.reject(error)), stop: async params => await dispatch(stopTaskqServer(params)) .catch((error) => Promise.reject(error)), }; }; export default withStyledReduxTable( mapStateToProps, mapDispatchToProps, styles)(TasQ, { orderBy: 'created', order: 'desc' });