frontend & backend update

This commit is contained in:
2023-01-16 21:47:23 +01:00
parent 1d6ff17994
commit 4cec531a25
51 changed files with 1010 additions and 571 deletions

View File

@@ -3,7 +3,7 @@
}
.data-table td, .data-table th {
padding: 2px;
padding: 5px;
vertical-align: middle;
}
@@ -11,10 +11,17 @@
background-color: #bbb;
}
.sortable {
cursor: pointer;
.data-table th > svg {
vertical-align: middle;
width: 18px;
height: 18px;
margin-right: 5px;
}
.center {
text-align: center;
}
.data-table-clickable {
cursor: pointer;
}

View File

@@ -2,48 +2,51 @@ import {Table, TableBody, TableCell, TableHead, TableRow} from "@material-ui/cor
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward";
import React, {useCallback, useContext, useEffect, useState} from "react";
import usePagination from "../hooks/pagination";
import {parse} from "date-fns";
import "./data-table.css";
import {LocaleContext} from "../locale";
import clsx from "clsx";
import {Box} from "@mui/material";
import {formatDate} from "../util";
import {Box, IconButton} from "@mui/material";
import {formatDateTime} from "../util";
import UserLink from "security-lab/src/elements/user/userlink";
import CachedIcon from "@material-ui/icons/Cached";
export function DataTable(props) {
const { className, placeholder,
columns, data, pagination,
fetchData, onClick, onFilter,
defaultSortColumn, defaultSortOrder,
columns, ...other } = props;
title, ...other } = props;
const {currentLocale, requestModules, translate: L} = useContext(LocaleContext);
const {translate: L} = useContext(LocaleContext);
const [doFetchData, setFetchData] = useState(false);
const [data, setData] = useState(null);
const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase));
const [sortColumn, setSortColumn] = useState(defaultSortColumn || null);
const pagination = usePagination();
const sortable = props.hasOwnProperty("sortable") ? !!props.sortable : true;
const onRowClick = onClick || (() => {});
const onFetchData = useCallback((force = false) => {
if (doFetchData || force) {
setFetchData(false);
const orderBy = columns[sortColumn]?.field || null;
const sortOrder = sortAscending ? "asc" : "desc";
fetchData(pagination.getPage(), pagination.getPageSize(), orderBy, sortOrder).then(([data, dataPagination]) => {
if (data) {
setData(data);
pagination.update(dataPagination);
}
});
fetchData(pagination.getPage(), pagination.getPageSize(), orderBy, sortOrder);
}
}, [doFetchData, columns, sortColumn, sortAscending, pagination]);
// pagination changed?
useEffect(() => {
let forceFetch = (pagination.getPageSize() < pagination.getTotal());
let forceFetch = false;
if (pagination.getPageSize() < pagination.getTotal()) {
// page size is smaller than the total count
forceFetch = true;
} else if (data?.length && pagination.getPageSize() >= data.length && data.length < pagination.getTotal()) {
// page size is greater than the current visible count but there were hidden rows before
forceFetch = true;
}
onFetchData(forceFetch);
}, [pagination.data.pageSize, pagination.data.current]);
@@ -69,13 +72,14 @@ export function DataTable(props) {
}
if (sortable && column.sortable) {
headerRow.push(<TableCell key={"col-" + index} className={"sortable"}
headerRow.push(<TableCell key={"col-" + index} className={"data-table-clickable"}
title={L("general.sort_by") + ": " + column.label}
onClick={() => onChangeSort(index, column) }>
onClick={() => onChangeSort(index, column)}
align={column.align}>
{sortColumn === index ? (sortAscending ? <ArrowUpwardIcon /> : <ArrowDownwardIcon />): <></>}{column.renderHead(index)}
</TableCell>);
} else {
headerRow.push(<TableCell key={"col-" + index}>
headerRow.push(<TableCell key={"col-" + index} align={column.align}>
{column.renderHead(index)}
</TableCell>);
}
@@ -83,14 +87,20 @@ export function DataTable(props) {
const numColumns = columns.length;
let rows = [];
if (data) {
for (const [key, entry] of Object.entries(data)) {
if (data && data?.length) {
for (const [rowIndex, entry] of data.entries()) {
let row = [];
for (const [index, column] of columns.entries()) {
row.push(<TableCell key={"col-" + index}>{column.renderData(L, entry)}</TableCell>);
row.push(<TableCell key={"col-" + index} align={column.align}>
{column.renderData(L, entry, index)}
</TableCell>);
}
rows.push(<TableRow key={"row-" + key}>{ row }</TableRow>);
rows.push(<TableRow className={clsx({["data-table-clickable"]: typeof onClick === 'function'})}
onClick={() => onRowClick(rowIndex, entry)}
key={"row-" + rowIndex}>
{ row }
</TableRow>);
}
} else if (placeholder) {
rows.push(<TableRow key={"row-placeholder"}>
@@ -100,151 +110,13 @@ export function DataTable(props) {
</TableRow>);
}
/*
let columnElements = [];
if (columns) {
for (const [key, column] of Object.entries(columns)) {
const centered = column.alignment === "center";
const sortable = doSort && (!column.hasOwnProperty("sortable") || !!column.sortable);
const label = column.label;
if (!sortable) {
columnElements.push(
<TableCell key={"column-" + key} className={clsx(centered && classes.columnCenter)}>
{ label }
</TableCell>
);
} else {
columnElements.push(
<TableCell key={"column-" + key} label={L("Sort By") + ": " + label} className={clsx(classes.clickable, centered && classes.columnCenter)}
onClick={() => (key === sortColumn ? setSortAscending(!sortAscending) : setSortColumn(key)) }>
{ key === sortColumn ?
<Grid container alignItems={"center"} spacing={1} direction={"row"} className={classes.gridSorted}>
<Grid item>{ sortAscending ? <ArrowUpwardIcon fontSize={"small"} /> : <ArrowDownwardIcon fontSize={"small"} /> }</Grid>
<Grid item>{ label }</Grid>
<Grid item/>
</Grid> :
<span><i/>{label}</span>
}
</TableCell>
);
}
}
}
const getValue = useCallback((entry, key) => {
if (typeof columns[key]?.value === 'function') {
return columns[key].value(entry);
} else {
return entry[columns[key]?.value] ?? null;
}
}, [columns]);
let numColumns = columns ? Object.keys(columns).length : 0;
const compare = (a,b,callback) => {
let definedA = a !== null && typeof a !== 'undefined';
let definedB = b !== null && typeof b !== 'undefined';
if (!definedA && !definedB) {
return 0;
} else if (!definedA) {
return 1;
} else if (!definedB) {
return -1;
} else {
return callback(a,b);
}
}
let rows = [];
const hasClickHandler = typeof onClick === 'function';
if (data !== null && columns) {
let hidden = 0;
let sortedEntries = data.slice();
if (sortColumn && columns[sortColumn]) {
let sortFunction;
if (typeof columns[sortColumn]?.compare === 'function') {
sortFunction = columns[sortColumn].compare;
} else if (columns[sortColumn]?.type === Date) {
sortFunction = (a, b) => compare(a, b, (a,b) => a.getTime() - b.getTime());
} else if (columns[sortColumn]?.type === Number) {
sortFunction = (a, b) => compare(a, b, (a,b) => a - b);
} else {
sortFunction = ((a, b) =>
compare(a, b, (a,b) => a.toString().toLowerCase().localeCompare(b.toString().toLowerCase()))
)
}
sortedEntries.sort((a, b) => {
let entryA = getValue(a, sortColumn);
let entryB = getValue(b, sortColumn);
return sortFunction(entryA, entryB);
});
if (!sortAscending) {
sortedEntries = sortedEntries.reverse();
}
}
Array.from(Array(sortedEntries.length).keys()).forEach(rowIndex => {
if (typeof props.filter === 'function' && !props.filter(sortedEntries[rowIndex])) {
hidden++;
return;
}
let rowData = [];
for (const [key, column] of Object.entries(columns)) {
let value = getValue(sortedEntries[rowIndex], key);
if (typeof column.render === 'function') {
value = column.render(sortedEntries[rowIndex], value);
}
rowData.push(<TableCell key={"column-" + key} className={clsx(column.alignment === "center" && classes.columnCenter)}>
{ value }
</TableCell>);
}
rows.push(
<TableRow key={"entry-" + rowIndex}
className={clsx(hasClickHandler && classes.clickable)}
onClick={() => hasClickHandler && onClick(sortedEntries[rowIndex])}>
{ rowData }
</TableRow>
);
});
if (hidden > 0) {
rows.push(<TableRow key={"row-hidden"}>
<TableCell colSpan={numColumns} className={classes.hidden}>
{ "(" + (hidden > 1
? sprintf(L("%d rows hidden due to filter"), hidden)
: L("1 rows hidden due to filter")) + ")"
}
</TableCell>
</TableRow>);
} else if (rows.length === 0 && placeholder) {
rows.push(<TableRow key={"row-placeholder"}>
<TableCell colSpan={numColumns} className={classes.hidden}>
{ placeholder }
</TableCell>
</TableRow>);
}
} else if (columns && data === null) {
rows.push(<TableRow key={"loading"}>
<TableCell colSpan={numColumns} className={classes.columnCenter}>
<Grid container alignItems={"center"} spacing={1} justifyContent={"center"}>
<Grid item>{L("Loading")}…</Grid>
<Grid item><CircularProgress size={15}/></Grid>
</Grid>
</TableCell>
</TableRow>)
}
*/
return <Box position={"relative"}>
<h3>
<IconButton onClick={() => onFetchData(true)}>
<CachedIcon/>
</IconButton>
{title}
</h3>
<Table className={clsx("data-table", className)} size="small" {...other}>
<TableHead>
<TableRow>
@@ -260,17 +132,14 @@ export function DataTable(props) {
}
export class DataColumn {
constructor(label, field = null, sortable = true) {
constructor(label, field = null, params = {}) {
this.label = label;
this.field = field;
this.sortable = sortable;
this.sortable = !params.hasOwnProperty("sortable") || !!params.sortable;
this.align = params.align || "left";
}
compare(a, b) {
throw new Error("Not implemented: compare");
}
renderData(L, entry) {
renderData(L, entry, index) {
return entry[this.field]
}
@@ -280,49 +149,88 @@ export class DataColumn {
}
export class StringColumn extends DataColumn {
constructor(label, field = null, sortable = true, caseSensitive = false) {
super(label, field, sortable);
this.caseSensitve = caseSensitive;
}
compare(a, b) {
if (this.caseSensitve) {
return a.toString().localeCompare(b.toString());
} else {
return a.toString().toLowerCase().localeCompare(b.toString().toLowerCase());
}
constructor(label, field = null, params = {}) {
super(label, field, params);
}
}
export class NumericColumn extends DataColumn {
constructor(label, field = null, sortable = true) {
super(label, field, sortable);
constructor(label, field = null, params = {}) {
super(label, field, params);
this.decimalDigits = params.decimalDigits || null;
this.integerDigits = params.integerDigits || null;
this.prefix = params.prefix || "";
this.suffix = params.suffix || "";
this.decimalChar = params.decimalChar || ".";
}
compare(a, b) {
return a - b;
renderData(L, entry, index) {
let number = super.renderData(L, entry).toString();
if (this.decimalDigits !== null) {
number = number.toFixed(this.decimalDigits);
}
if (this.integerDigits !== null) {
let currentLength = number.split(".")[0].length;
if (currentLength < this.integerDigits) {
number = number.padStart(this.integerDigits - currentLength, "0");
}
}
if (this.decimalChar !== ".") {
number = number.replace(".", this.decimalChar);
}
return this.prefix + number + this.suffix;
}
}
export class DateTimeColumn extends DataColumn {
constructor(label, field = null, sortable = true, format = "YYYY-MM-dd HH:mm:ss") {
super(label, field, sortable);
this.format = format;
constructor(label, field = null, params = {}) {
super(label, field, params);
this.precise = !!params.precise;
}
compare(a, b) {
if (typeof a === 'string') {
a = parse(a, this.format, new Date()).getTime();
}
renderData(L, entry, index) {
let date = super.renderData(L, entry);
return formatDateTime(L, date, this.precise);
}
}
if (typeof b === 'string') {
b = parse(b, this.format, new Date()).getTime();
}
return a - b;
export class UserLinkColumn extends DataColumn {
constructor(label, field = null, params = {}) {
super(label, field, params);
}
renderData(L, entry) {
return formatDate(L, super.renderData(L, entry));
renderData(L, entry, index) {
return <UserLink user={super.renderData(L, entry)}/>
}
}
export class ControlsColumn extends DataColumn {
constructor(buttons = [], params = {}) {
super("general.controls", null, { align: "center", ...params, sortable: false });
this.buttons = buttons;
}
renderData(L, entry, index) {
let buttonElements = [];
for (const [index, button] of this.buttons.entries()) {
let element = button.element;
let props = {
key: "button-" + index,
onClick: (() => button.onClick(entry)),
className: "data-table-clickable"
}
if (typeof button.showIf !== 'function' || button.showIf(entry)) {
buttonElements.push(React.createElement(element, props))
}
}
return <>
{buttonElements}
</>
}
}