// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-FileCopyrightText: 2020-2022 grommunio GmbH import React, { PureComponent } from "react"; import PropTypes from "prop-types"; import { withStyles } from "@mui/styles"; import { withTranslation } from "react-i18next"; import { Paper, Table, TableHead, TableRow, TableCell, TableBody, TableSortLabel, Tooltip, Grid, InputAdornment, TextField, FormControlLabel, Checkbox, MenuItem, IconButton, } from "@mui/material"; import { connect } from "react-redux"; import { fetchSyncData } from "../actions/sync"; import { CheckCircleOutlined, HelpOutline, HighlightOffOutlined, Search } from "@mui/icons-material"; import { getStringFromCommand, getTimePast } from "../utils"; import SyncStatistics from "../components/SyncStatistics"; import { grey, red } from "@mui/material/colors"; import TableViewContainer from "../components/TableViewContainer"; const styles = (theme) => ({ actions: { display: 'flex', flex: 1, margin: theme.spacing(0, 2, 0, 0), justifyContent: 'flex-end', alignItems: 'flex-end', }, buttonGrid: { padding: theme.spacing(0, 0, 2, 2), }, select: { maxWidth: 224, marginRight: 8, }, defaultRow: { fontWeight: 400, }, terminated: { color: grey['500'], fontWeight: 400, }, cell: { color: 'inherit', fontWeight: 'inherit', }, justUpdated: { fontWeight: 'bold', }, darkred: { color: red['900'], fontWeight: 400, }, red: { color: red['500'], fontWeight: 400, }, }); class Sync extends PureComponent { state = { snackbar: null, sortedDevices: null, order: 'desc', orderBy: 'update', type: 'int', match: '', showPush: true, onlyActive: false, filterEnded: 20, filterUpdated: 120, }; columns = [ { label: "PID", value: "pid", type: 'int', padding: "checkbox" }, { label: "IP", value: "ip", padding: "checkbox" }, { label: "User", value: "user" }, { label: "Command", value: "command", type: 'int' }, { label: "Time", value: 'update', type: 'int' }, { label: "Device ID", value: "devid" }, { label: "Info", value: "addinfo" }, ]; fetchInterval = null; componentDidMount() { const { orderBy, type, filterEnded, filterUpdated } = this.state; this.props.fetch({ filterEnded, filterUpdated }) .then(this.handleSort(orderBy, type, false)) .catch(snackbar => this.setState({ snackbar })); this.fetchInterval = setInterval(() => { this.handleRefresh(); }, 2000); } componentWillUnmount() { clearInterval(this.fetchInterval); } handleNavigation = (path) => (event) => { const { history } = this.props; event.preventDefault(); history.push(`/${path}`); }; handleRefresh = () => { const { orderBy, type, filterEnded, filterUpdated } = this.state; this.props.fetch({ filterEnded, filterUpdated }) .then(this.handleSort(orderBy, type, false)) .catch(snackbar => this.setState({ snackbar })); } handleSort = (attribute, type, switchOrder) => () => { const sortedDevices = [...this.props.sync]; const { order: stateOrder, orderBy } = this.state; const order = orderBy === attribute && stateOrder === "asc" ? "desc" : "asc"; if((switchOrder && order === 'asc') || (!switchOrder && stateOrder === 'asc')) { sortedDevices.sort((a, b) => type !== 'int' ? a[attribute].localeCompare(b[attribute]) : a[attribute] - b[attribute] ); } else { sortedDevices.sort((a, b) => type !== 'int' ? b[attribute].localeCompare(a[attribute]) : b[attribute] - a[attribute] ); } this.setState({ sortedDevices, order: switchOrder ? order : stateOrder, orderBy: attribute, type }); } getRowClass(row, diff) { const { classes } = this.props; if(row.justUpdated) return classes.justUpdated; if(row.ended !== 0) return classes.terminated; if(row.push && diff > 32) return classes.darkred; if(!row.push && diff > 2) return classes.red; return classes.defaultRow; } getMatch(row) { const { match, showPush, onlyActive } = this.state; const lcMatch = match.toLowerCase(); const { user, devtype, devagent, ip } = row; const stringMatch = user.toLowerCase().includes(lcMatch) || devtype.toLowerCase().includes(lcMatch) || devagent.toLowerCase().includes(lcMatch) || ip.toLowerCase().includes(lcMatch); return stringMatch && (!onlyActive || row.ended === 0) && (showPush || !row.push); } handleInput = field => ({ target: t }) => this.setState({ [field]: t.value }); handleCheckbox = field => ({ target: t }) => this.setState({ [field]: t.checked }); render() { const { classes, t, sync } = this.props; const { snackbar, sortedDevices, order, orderBy, match, showPush, onlyActive, filterEnded, filterUpdated } = this.state; return ( <TableViewContainer handleScroll={this.handleScroll} headline={<> {t("Mobile devices")} <IconButton size="small" href="https://docs.grommunio.com/admin/administration.html#mobile-devices" target="_blank" > <HelpOutline fontSize="small"/> </IconButton> </> } subtitle={t('sync_sub')} snackbar={snackbar} onSnackbarClose={() => this.setState({ snackbar: '' })} > <Grid container alignItems="flex-end" className={classes.buttonGrid}> <FormControlLabel control={ <Checkbox checked={showPush} onChange={this.handleCheckbox('showPush')} color="primary" /> } label={t('Show push connections')} /> <FormControlLabel control={ <Checkbox checked={onlyActive} onChange={this.handleCheckbox('onlyActive')} color="primary" /> } label={t('Only show active connections')} /> <TextField value={filterUpdated} onChange={this.handleInput('filterUpdated')} label={t("Last updated (seconds)")} className={classes.select} select color="primary" fullWidth > <MenuItem value={60}>10</MenuItem> <MenuItem value={60}>30</MenuItem> <MenuItem value={60}>60</MenuItem> <MenuItem value={120}>120</MenuItem> </TextField> <TextField value={filterEnded} onChange={this.handleInput('filterEnded')} label={t("Last ended (seconds)")} className={classes.select} select color="primary" fullWidth > <MenuItem value={20}>3</MenuItem> <MenuItem value={20}>5</MenuItem> <MenuItem value={20}>10</MenuItem> <MenuItem value={20}>20</MenuItem> </TextField> <div className={classes.actions}> <TextField value={match} onChange={this.handleInput('match')} placeholder={t("Filter")} variant="outlined" className={classes.textfield} InputProps={{ startAdornment: ( <InputAdornment position="start"> <Search color="secondary" /> </InputAdornment> ), }} color="primary" /> </div> </Grid> <SyncStatistics data={sync}/> <Paper elevation={1}> <Table size="small"> <TableHead> <TableRow> {this.columns.map((column) => ( <TableCell key={column.value} padding={column.padding || 'normal'} > <TableSortLabel active={orderBy === column.value} align="left" direction={order} onClick={this.handleSort(column.value, column.type, true)} > {t(column.label)} </TableSortLabel> </TableCell> ))} <TableCell padding="checkbox"> {t('Push')} </TableCell> </TableRow> </TableHead> <TableBody> {(sortedDevices || sync).map((obj, idx) => { const timePast = getTimePast(obj.diff); const matches = this.getMatch(obj); return matches ? ( <Tooltip key={idx} placement="top" title={obj.devtype + ' / ' + obj.devagent}> <TableRow hover className={this.getRowClass(obj, obj.diff)}> <TableCell className={classes.cell} padding="checkbox">{obj.pid || ''}</TableCell> <TableCell className={classes.cell} padding="checkbox">{obj.ip || ''}</TableCell> <TableCell className={classes.cell}>{obj.user || ''}</TableCell> <TableCell className={classes.cell}>{getStringFromCommand(obj.command)}</TableCell> <TableCell className={classes.cell}>{timePast}</TableCell> <TableCell className={classes.cell}>{obj.devid || ''}</TableCell> <TableCell className={classes.cell}>{obj.addinfo || ''}</TableCell> <TableCell className={classes.cell} padding="checkbox"> {obj.push ? <CheckCircleOutlined /> : <HighlightOffOutlined />} </TableCell> </TableRow> </Tooltip> ) : null; })} </TableBody> </Table> </Paper> </TableViewContainer> ); } } Sync.propTypes = { classes: PropTypes.object.isRequired, t: PropTypes.func.isRequired, history: PropTypes.object.isRequired, sync: PropTypes.array.isRequired, fetch: PropTypes.func.isRequired, }; const mapStateToProps = (state) => { return { sync: state.sync.Sync || [] }; }; const mapDispatchToProps = (dispatch) => { return { fetch: async (params) => { await dispatch(fetchSyncData(params)).catch((error) => Promise.reject(error) ); }, }; }; export default connect( mapStateToProps, mapDispatchToProps )(withTranslation()(withStyles(styles)(Sync)));