import {Table, TableBody, TableCell, TableHead, TableRow} from "@material-ui/core";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward";
import React, {useCallback, useContext, useEffect, useState} from "react";
import "./data-table.css";
import {LocaleContext} from "../locale";
import clsx from "clsx";
import {Box, IconButton, Select, TextField} from "@mui/material";
import {formatDate, formatDateTime} from "../util";
import CachedIcon from "@material-ui/icons/Cached";
import {isNumber} from "chart.js/helpers";
export function DataTable(props) {
const { className, placeholder,
columns, data, pagination,
fetchData, onClick, onFilter,
defaultSortColumn, defaultSortOrder,
forceReload,
title, ...other } = props;
const {translate: L} = useContext(LocaleContext);
const [doFetchData, setFetchData] = useState(false);
const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase()));
const [sortColumn, setSortColumn] = useState(isNumber(defaultSortColumn) || null);
const sortable = !!fetchData && (props.hasOwnProperty("sortable") ? !!props.sortable : true);
const onRowClick = onClick || (() => {});
const onFetchData = useCallback((force = false) => {
if (fetchData) {
if (doFetchData || force) {
setFetchData(false);
const orderBy = columns[sortColumn]?.field || null;
const sortOrder = sortAscending ? "asc" : "desc";
fetchData(pagination.getPage(), pagination.getPageSize(), orderBy, sortOrder);
}
}
}, [fetchData, doFetchData, columns, sortColumn, sortAscending, pagination]);
// pagination changed?
useEffect(() => {
if (pagination) {
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]);
// sorting changed or we forced an update
useEffect(() => {
onFetchData(true);
}, [sortAscending, sortColumn, forceReload]);
let headerRow = [];
const onChangeSort = useCallback((index, column) => {
if (sortable && column.sortable) {
if (sortColumn === index) {
setSortAscending(!sortAscending);
} else {
setSortColumn(index);
}
}
}, [onFetchData, sortColumn, sortAscending]);
for (const [index, column] of columns.entries()) {
if (!(column instanceof DataColumn)) {
throw new Error("DataTable can only have DataColumn-objects as column definition, got: " + typeof column);
} else if (column.hidden) {
continue;
}
if (sortable && column.sortable) {
headerRow.push( onChangeSort(index, column)}
align={column.align}>
{sortColumn === index ?
(sortAscending ? : ) :
<>>
}
{column.renderHead(index)}
);
} else {
headerRow.push(
{column.renderHead(index)}
);
}
}
const numColumns = columns.length;
let numRows = 0;
let rows = [];
if (data && data?.length) {
numRows = data.length;
for (const [rowIndex, entry] of data.entries()) {
let row = [];
for (const [index, column] of columns.entries()) {
row.push(
{column.renderData(L, entry, index)}
);
}
rows.push( ["tr","td"].includes(e.target.tagName.toLowerCase()) && onRowClick(rowIndex, entry)}
key={"row-" + rowIndex}>
{ row }
);
}
} else if (placeholder) {
rows.push(
{ placeholder }
);
}
return
{title ?
{fetchData ?
onFetchData(true)} title={L("general.reload")}>
: <>>
}
{title}
: <>>
}
{pagination && pagination.renderPagination(L, numRows)}
}
export class DataColumn {
constructor(label, field = null, params = {}) {
this.label = label;
this.field = field;
this.sortable = !params.hasOwnProperty("sortable") || !!params.sortable;
this.align = params.align || "left";
this.hidden = !!params.hidden;
this.params = params;
}
renderData(L, entry, index) {
return typeof this.field === 'function' ? this.field(entry) : entry[this.field];
}
renderHead() {
return this.label;
}
}
export class StringColumn extends DataColumn {
constructor(label, field = null, params = {}) {
super(label, field, params);
}
renderData(L, entry, index) {
let data = super.renderData(L, entry, index);
if (this.params.maxLength && data?.length && data.length > this.params.maxLength) {
data = data.substring(0, this.params.maxLength) + "...";
}
if (this.params.style) {
let style = (typeof this.params.style === 'function'
? this.params.style(entry) : this.params.style);
data = {data}
}
return data;
}
}
export class ArrayColumn extends DataColumn {
constructor(label, field = null, params = {}) {
super(label, field, params);
this.seperator = params.seperator || ", ";
}
renderData(L, entry, index) {
let data = super.renderData(L, entry, index);
if (!Array.isArray(data)) {
data = Object.values(data);
}
data = data.join(this.seperator);
if (this.params.style) {
let style = (typeof this.params.style === 'function'
? this.params.style(entry) : this.params.style);
data = {data}
}
return data;
}
}
export class SecretsColumn extends DataColumn {
constructor(label, field = null, params = {}) {
super(label, field, params);
this.asteriskCount = params.asteriskCount || 8;
this.character = params.character || "*";
this.canCopy = params.hasOwnProperty("canCopy") ? params.canCopy : true;
}
renderData(L, entry, index) {
let originalData = super.renderData(L, entry, index);
if (!originalData) {
return "(None)";
}
let properties = this.params.properties || {};
properties.className = clsx(properties.className, "font-monospace");
if (this.canCopy) {
properties.title = L("Click to copy");
properties.className = clsx(properties.className, "data-table-clickable");
properties.onClick = () => {
navigator.clipboard.writeText(originalData);
};
}
return {this.character.repeat(this.asteriskCount)}
}
}
export class NumericColumn extends DataColumn {
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 || ".";
}
renderData(L, entry, index) {
let number = super.renderData(L, entry, index).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, params = {}) {
super(label, field, params);
this.precise = !!params.precise;
}
renderData(L, entry, index) {
let date = super.renderData(L, entry, index);
return date ? formatDateTime(L, date, this.precise) : "";
}
}
export class DateColumn extends DataColumn {
constructor(label, field = null, params = {}) {
super(label, field, params);
}
renderData(L, entry, index) {
let date = super.renderData(L, entry, index);
return date ? formatDate(L, date) : "";
}
}
export class BoolColumn extends DataColumn {
constructor(label, field = null, params = {}) {
super(label, field, params);
}
renderData(L, entry, index) {
let data = super.renderData(L, entry, index);
return L(data ? "general.yes" : "general.no");
}
}
export class InputColumn extends DataColumn {
constructor(label, field, type, onChange, params = {}) {
super(label, field, { ...params, sortable: false });
this.type = type;
this.onChange = onChange;
this.props = params.props || {};
}
renderData(L, entry, index) {
let value = super.renderData(L, entry, index);
let inputProps = typeof this.props === 'function' ? this.props(entry, index) : this.props;
if (this.type === 'text') {
return this.onChange(entry, index, e.target.value)} />
} else if (this.type === "select") {
let options = Object.entries(this.params.options || {}).map(([value, label]) =>
);
return
}
return <>[Invalid type: {this.type}]>
}
}
export class ControlsColumn extends DataColumn {
constructor(label, buttons = [], params = {}) {
super(label, 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 = typeof button.element === 'function'
? button.element(entry, index)
: button.element;
let buttonProps = {};
if (typeof button.props === 'function') {
buttonProps = button.props(entry, index);
} else {
buttonProps = button.props;
}
let props = {
...buttonProps,
key: "button-" + index,
}
// TODO: icon button!
if (button.hasOwnProperty("disabled")) {
props.disabled = typeof button.disabled === 'function'
? button.disabled(entry, index)
: button.disabled;
}
if (!props.disabled) {
props.onClick = (e) => { e.stopPropagation(); button.onClick(entry, index); }
}
if ((!button.hasOwnProperty("hidden")) ||
(typeof button.hidden === 'function' && !button.hidden(entry, index)) ||
(!button.hidden)) {
buttonElements.push(React.createElement(element, props))
}
}
return
{buttonElements}
}
}