frontend/app/GeneralListComponent.jsx (253 lines of code) (raw):

import React from "react"; import axios from "axios"; import { Link } from "react-router-dom"; import moment from "moment"; import { Helmet } from "react-helmet"; import EnhancedTable from "./MaterialUITable"; import DeleteIcon from "@material-ui/icons/Delete"; import EditIcon from "@material-ui/icons/Edit"; import { IconButton } from "@material-ui/core"; class GeneralListComponent extends React.Component { static ITEM_LIMIT = 50; constructor(props) { super(props); this.state = { data: [], hovered: false, filterTerms: { match: "W_ENDSWITH", }, currentPage: 0, maximumItemsLoaded: false, plutoConfig: {}, uid: "", isAdmin: false, }; this.pageSize = 20; this.gotDataCallback = this.gotDataCallback.bind(this); this.newElementCallback = this.newElementCallback.bind(this); this.filterDidUpdate = this.filterDidUpdate.bind(this); /* this must be supplied by a subclass */ this.endpoint = "/unknown"; this.filterEndpoint = null; this.style = { backgroundColor: "#eee", border: "1px solid black", borderCollapse: "collapse", }; this.iconStyle = { color: "#aaa", paddingLeft: "5px", paddingRight: "5px", }; this.canCreateNew = true; /* this must be supplied by a subclass */ this.columns = []; } componentDidMount() { this.loadDependencies().then(() => { this.dependenciesDidLoad(); this.reload(); }); } /** * override this in a subclass to update state once dependencies have loaded */ dependenciesDidLoad() {} loadDependencies() { return new Promise((accept, reject) => axios .get("/api/isLoggedIn") .then((response) => { if (response.data.status === "ok") this.setState( { isAdmin: response.data.isAdmin, uid: response.data.uid }, () => accept() ); }) .catch((error) => { if ( this.props.error.response && this.props.error.response.status === 403 ) this.setState({ isAdmin: false }, () => accept()); else { console.error(error); this.setState({ isAdmin: false, error: error }, () => reject()); } }) ); } /* this method supplies a column definition as a convenience for subclasses */ static standardColumn(name, key) { return { header: name, key: key, headerProps: { className: "dashboardheader" }, render: (value) => <span style={{ fontStyle: "italic" }}>n/a</span> ? ( value ) : ( value && value.length > 0 ), }; } static boolColumn(name, key) { return { header: name, key: key, headerProps: { className: "dashboardheader" }, render: (value) => <span>{String(value)}</span>, }; } /* this method supplies a column definition for datetimes */ static dateTimeColumn(name, key) { return { header: name, key: key, headerProps: { className: "dashboardheader" }, render: (value) => ( <span className="datetime"> {moment(value).format("ddd Do MMM, HH:mm")} </span> ), }; } breakdownPathComponents() { return this.props.location.pathname.split("/"); } /* this method supplies the edit and delete icons. Can't be static as <Link> relies on the object context to access * history. */ actionIcons() { const deploymentRoot = deploymentRootPath ?? "/"; const componentName = this.breakdownPathComponents()[1]; return { header: "", key: "id", render: (id) => ( <span className="icons" style={{ minWidth: "100px", display: this.state.isAdmin ? "inherit" : "none", }} > <Link to={"/" + componentName + "/" + id}> <IconButton> <EditIcon /> </IconButton> </Link> <Link to={"/" + componentName + "/" + id + "/delete"}> <IconButton> <DeleteIcon /> </IconButton> </Link> </span> ), }; } /* loads the next page of data */ getNextPage() { const startAt = this.state.currentPage * this.pageSize; const length = this.pageSize; const axiosFuture = this.filterEndpoint ? axios.put( this.filterEndpoint + "?startAt=" + startAt + "&length=" + length, this.state.filterTerms ) : axios.get(this.endpoint + "?startAt=" + startAt + "&length=" + length); axiosFuture .then((response) => { this.setState( { currentPage: this.state.currentPage + 1, }, () => { this.gotDataCallback(response, () => { if (response.data.result.length > 0) if ( this.pageSize * this.state.currentPage >= GeneralListComponent.ITEM_LIMIT ) this.setState({ maximumItemsLoaded: true }); else this.getNextPage(); }); } ); }) .catch((error) => { console.error(error); }); } /* reloads the data for the component based on the endpoint configured in the constructor */ reload() { this.setState( { currentPage: 0, data: [], }, () => this.getNextPage() ); } /* called when we receive data; can be over-ridden by a subclass to do something more clever */ gotDataCallback(response, cb) { this.setState( { data: this.state.data.concat(response.data.result), }, cb ); } /* called when the New button is clicked; can be over-ridden by a subclass to do something more clever */ newElementCallback(event) {} /* called to insert a filtering component; should be over-ridden by a subclass if filtering is required */ getFilterComponent() { return <span />; } /* this can be referenced from a filter component in a subclass and should be called to update the active filtering. this will cause a reload of data from the server */ filterDidUpdate(newterms) { this.setState({ filterTerms: newterms }, () => this.reload()); } itemLimitWarning() { if (this.state.maximumItemsLoaded) return ( <p className="warning-text"> <i className="fa-info fa" style={{ marginRight: "0.5em", color: "orange" }} /> Maximum of {GeneralListComponent.ITEM_LIMIT} items have been loaded. Use filters to narrow this down. </p> ); else return <p style={{ margin: 0 }} />; } render() { return ( <> <Helmet> <title>Core Admin</title> </Helmet> <div> <span className="list-title"> <h2 className="list-title">{this.props.title}</h2> </span> {this.getFilterComponent()} {this.itemLimitWarning()} <span className="banner-control"> <button id="newElementButton" onClick={this.newElementCallback}> New </button> </span> <EnhancedTable columnData={this.columns} tableData={this.state.data} /> </div> </> ); } } export default GeneralListComponent;