frontend/app/projectsearch/ProjectContentSummary.jsx (307 lines of code) (raw):

import React from "react"; import PropTypes from "prop-types"; import { Doughnut, Pie } from "react-chartjs-2"; import BytesFormatter from "../common/BytesFormatter.jsx"; import { authenticatedFetch } from "../auth"; import { Loop } from "@material-ui/icons"; import { withStyles } from "@material-ui/core/styles"; const styles = { summaryContainer: { marginLeft: "auto", marginRight: "auto", display: "block", overflow: "hidden", width: "90vw", marginTop: "2em", maxHeight: "80vh", minHeight: "230px", borderStyle: "dashed", padding: "0.4em", paddingBottom: "1em", }, }; class ProjectContentSummary extends React.Component { static propTypes = { projectId: PropTypes.string, vaultId: PropTypes.string.isRequired, plutoBaseUrl: PropTypes.string, }; constructor(props) { super(props); const emptySummary = { count: 0, size: 0 }; const pieCasing = { datasets: [ { data: [], }, ], labels: [], }; this.state = { lastError: null, loading: false, summaryData: { total: emptySummary, }, typePieData: pieCasing, fileTypePieData: pieCasing, mediaTypePieData: pieCasing, showType: true, showExtensions: false, showMediaType: true, }; this.initiateDownload = this.initiateDownload.bind(this); } static getDerivedStateFromError(error) { return { loading: false, lastError: error ? error.toString : "Internal error, see console", }; } componentDidCatch(error, errorInfo) { console.error(error); console.error("A subcomponent threw an error: ", errorInfo); } setStatePromise(newState) { return new Promise((resolve, reject) => this.setState(newState, () => resolve()) ); } checkVaultId(resolve, reject, iteration) { if (this.props.vaultId) { resolve(); } else { const count = iteration ? iteration + 1 : 2; if (count > 10) { console.error("Timed out waiting for vault ID to be set"); reject("Timeout"); } window.setTimeout(() => this.waitForVaultId(resolve, count), 100); } } waitForVaultId() { return new Promise((resolve, reject) => { this.checkVaultId(resolve, reject, 0); }); } async getSummaryInfo() { await this.waitForVaultId(); //when the view is passed a project ID in the URL on a fresh load then this can (on occasion) get triggered _before_ the vaultid is set. //we chain ourselves to a Promise that only resolves once the vaultid is set, using a promise-timeout that does not block so the backend can //set up the vaultId property correctly. if (!this.props.projectId) { return this.setStatePromise({ loading: false, lastError: "No project requested!", }); } const url = "/api/vault/" + this.props.vaultId + "/projectSummary/" + this.props.projectId; const result = await authenticatedFetch(url); const bodyText = await result.text(); if (result.ok) { const content = JSON.parse(bodyText); return this.setStatePromise({ loading: false, lastError: null, summaryData: content, }).then(() => this.setupChartData()); } else { console.error(bodyText); return this.setStatePromise({ loading: false, lastError: bodyText }); } } async setupChartData() { const updatedTypePieData = { datasets: [ { data: Object.values(this.state.summaryData.gnmType).map( (entry) => entry.count ), }, ], labels: Object.keys(this.state.summaryData.gnmType), }; const updatedFileTypePieData = { datasets: [ { data: Object.values(this.state.summaryData.fileType).map( (entry) => entry.count ), }, ], labels: Object.keys(this.state.summaryData.fileType), }; const updatedMediaTypePieData = { datasets: [ { data: Object.values(this.state.summaryData.mediaType).map( (entry) => entry.count ), }, ], labels: Object.keys(this.state.summaryData.mediaType), }; return this.setStatePromise({ typePieData: updatedTypePieData, fileTypePieData: updatedFileTypePieData, mediaTypePieData: updatedMediaTypePieData, }); } componentDidMount() { this.setState({ loading: true }, () => this.getSummaryInfo()); } componentDidUpdate(prevProps, prevState, snapshot) { if (prevProps.projectId !== this.props.projectId) this.setState({ loading: true }, () => this.getSummaryInfo()); if (prevProps.vaultId !== this.props.vaultId) this.setState({ loading: true }, () => this.getSummaryInfo()); } async requestDownloadLink() { const url = "/api/bulk/new/" + this.props.vaultId + "/" + this.props.projectId; const result = await authenticatedFetch(url); const bodyText = await result.text(); if (result.ok) { const content = JSON.parse(bodyText); if (content.status === "ok" && content.itemClass === "link") { return this.setStatePromise({ lastError: null }).then( () => (window.location.href = content.entry) ); } else { console.log("Got malformed response ", content); return this.setStatePromise({ lastError: "malformed server response, check javascript logs for details", }); } } else { return this.setStatePromise({ lastError: bodyText }); } } initiateDownload() { this.requestDownloadLink().catch((err) => { console.error(err); this.setState({ lastError: "Clientside error, check javascript logs for details", }); }); } render() { if (this.state.loading) { return ( <div id="project-content-summary" className={this.props.classes.summaryContainer} > <p className="centered information"> Project {this.props.projectId} <br /> Loading summary information... </p> <a href={this.props.plutoBaseUrl + "/project/" + this.props.projectId} target="_blank" style={{ fontSize: "1.4em", paddingTop: "0.8em", display: this.props.plutoBaseUrl ? "inherit" : "none", }} > View in Pluto &gt; </a> <div className="centered" style={{ marginTop: "1em" }}> <Loop className="spinner" size="5x" /> </div> </div> ); } return ( <div id="project-content-summary" className={this.props.classes.summaryContainer} > {this.props.projectId && this.props.projectId !== "" ? ( <p className="centered information" style={{ marginBottom: "0.6em" }}> Project {this.props.projectId} </p> ) : ( <p className="centered information">Select a project above</p> )} {this.state.summaryData.total.count === 0 && !this.state.loading && this.props.projectId ? ( <p className="information centered" style={{ fontSize: "1.2em" }}> No media found for this project in the selected vault. </p> ) : ( <span /> )} <div className="chart-holder"> <table> <tbody> <tr> <td>Files in this project</td> <td>{this.state.summaryData.total.count}</td> </tr> <tr> <td>Total size of this project</td> <td> <BytesFormatter value={this.state.summaryData.total.size} /> </td> </tr> </tbody> </table> <a href={this.props.plutoBaseUrl + "/project/" + this.props.projectId} target="_blank" style={{ fontSize: "1.4em", paddingTop: "0.8em", display: this.props.plutoBaseUrl ? "inherit" : "none", }} > View in Pluto &gt; </a> <a onClick={this.initiateDownload} style={{ fontSize: "1.4em", display: this.state.summaryData.total.count > 0 ? "block" : "none", }} className="clickable" > Open in Download Manager &gt; </a> <p className="error">{this.state.lastError}</p> </div> <div className="chart-holder" style={{ display: this.state.showType ? "inline-block" : "none" }} > <Doughnut data={this.state.typePieData} /> </div> <div className="chart-holder" style={{ display: this.state.showExtensions ? "inline-block" : "none", }} > <Doughnut data={this.state.fileTypePieData} /> </div> <div className="chart-holder" style={{ display: this.state.showMediaType ? "inline-block" : "none", }} > <Doughnut data={this.state.mediaTypePieData} /> </div> </div> ); } } export default withStyles(styles)(ProjectContentSummary);