frontend/app/AssetSearch/AssetSearchResults.tsx (217 lines of code) (raw):
import React from "react";
import axios from "axios";
import { validator } from "./UuidValidator";
import {
Link,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TableSortLabel,
Typography,
} from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import { Link as RouterLink } from "react-router-dom";
import DeliverableSummaryCell from "../ProjectDeliverables/DeliverableSummaryCell";
import DateTimeFormatter from "../Form/DateTimeFormatter";
interface AssetSearchResultsProps {
resultsLimit: number;
filter: AssetSearchFilter;
}
interface AssetSearchResultsState {
loading: boolean;
lastError: string | undefined;
results: Deliverable[];
startAt: number;
pageSize: number;
sortBy: string;
sortOrder?: "desc" | "asc";
cancelTokenSource?: any;
}
const useStyles = makeStyles({
table: {
width: "100%",
},
informative: {
marginLeft: "auto",
marginRight: "auto",
colour: "darkorange",
},
});
interface SearchTableProps {
results: Deliverable[];
sortBy: string;
sortOrder?: "desc" | "asc";
onSortChanged: (
newSortBy: string,
newSortOrder: "desc" | "asc" | undefined
) => void;
}
const AssetSearchTable: React.FC<SearchTableProps> = (props) => {
const classes = useStyles();
const updateSort = (forField: string) => {
if (props.sortBy == forField) {
const newSortOrder = props.sortOrder == "desc" ? "asc" : "desc";
props.onSortChanged(forField, newSortOrder);
} else {
props.onSortChanged(forField, props.sortOrder);
}
};
return (
<>
<TableContainer>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell
sortDirection={
props.sortBy === "filename" ? props.sortOrder : false
}
>
<TableSortLabel
active={props.sortBy === "filename"}
direction={
props.sortBy === "filename" ? props.sortOrder : undefined
}
onClick={(evt) => updateSort("filename")}
>
Filename
</TableSortLabel>
</TableCell>
<TableCell>Type</TableCell>
<TableCell
sortDirection={
props.sortBy === "modified_dt" ? props.sortOrder : false
}
>
<TableSortLabel
active={props.sortBy === "modified_dt"}
direction={
props.sortBy === "modified_dt" ? props.sortOrder : undefined
}
onClick={(evt) => updateSort("modified_dt")}
>
Last Modified
</TableSortLabel>
</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.results.map((entry, idx) => (
<TableRow key={idx}>
<TableCell>
<Link
component={RouterLink}
to={`/bundle/${entry.deliverable}`}
>
<DeliverableSummaryCell deliverable={entry} />
</Link>
</TableCell>
<TableCell>{entry.type_string}</TableCell>
<TableCell>
<DateTimeFormatter value={entry.modified_dt} />
</TableCell>
<TableCell>{entry.status_string}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{props.results.length === 0 ? (
<Typography
align="center"
variant="caption"
className={classes.informative}
>
No assets matched this search
</Typography>
) : undefined}
</TableContainer>
</>
);
};
class AssetSearchResults extends React.Component<
AssetSearchResultsProps,
AssetSearchResultsState
> {
constructor(props: AssetSearchResultsProps) {
super(props);
this.state = {
loading: false,
lastError: undefined,
results: [],
startAt: 0,
pageSize: 25,
sortBy: "modified_dt",
sortOrder: "desc",
cancelTokenSource: axios.CancelToken.source(),
};
}
static getDerivedStateFromError(error: any) {
console.error(
"An uncaught error occurred in AssetSearchResults, this is a code bug"
);
return {
loading: false,
lastError: error.toString(),
};
}
setStatePromise(newState: any): Promise<void> {
return new Promise((resolve, reject) =>
this.setState(newState, () => resolve())
);
}
reset(): Promise<void> {
return this.setStatePromise({
results: [],
lastError: undefined,
startAt: 0,
cancelTokenSource: axios.CancelToken.source(),
});
}
validatedSearchRequest() {
if (!this.props.filter) return undefined;
if (
this.props.filter.atom_id &&
validator.test(this.props.filter.atom_id)
) {
return this.props.filter;
} else {
let updatedFilters = Object.assign({}, this.props.filter);
updatedFilters["atom_id"] = undefined;
return updatedFilters;
}
}
djangoOrderParam() {
let orderSym: string;
switch (this.state.sortOrder) {
case "asc":
orderSym = "";
break;
case "desc":
orderSym = "-";
break;
default:
orderSym = "";
break;
}
return `${orderSym}${this.state.sortBy}`;
}
async loadNextPage(): Promise<void> {
try {
const initialSearchDoc = this.validatedSearchRequest();
if (!initialSearchDoc) {
console.error("There was no search document set, this is a code bug");
return this.setStatePromise({
loading: false,
lastError: "internal error, see browser log",
});
}
const searchDoc = Object.assign({}, initialSearchDoc, {
order_by: this.djangoOrderParam(),
});
const response = await axios.post<DeliverableSearchResponse>(
`/api/asset/search?startAt=${this.state.startAt}&limit=${this.state.pageSize}`,
searchDoc,
{
cancelToken: this.state.cancelTokenSource.token,
}
);
if (response.data.results.length == 0) {
console.log("Reached end of list");
return this.setStatePromise({ loading: false });
}
await this.setStatePromise((prevState: AssetSearchResultsState) => ({
results: prevState.results.concat(...response.data.results),
startAt: prevState.startAt + this.state.pageSize,
}));
return this.state.startAt >= this.props.resultsLimit
? new Promise((resolve, reject) => resolve())
: this.loadNextPage();
} catch (err) {
if (err != "Cancel") {
return this.setStatePromise({
loading: false,
lastError: err.toString(),
});
}
}
}
componentDidMount() {
console.log("AssetSearchResults loaded");
this.loadNextPage();
}
componentDidUpdate(
prevProps: Readonly<AssetSearchResultsProps>,
prevState: Readonly<AssetSearchResultsState>,
snapshot?: any
) {
if (prevProps.filter !== this.props.filter) {
this.state.cancelTokenSource.cancel();
console.log("Filter updated, reloading results");
this.reset().then(() => this.loadNextPage());
}
}
render() {
return (
<>
<span style={{ height: "1em", overflow: "hidden", color: "red" }}>
{this.state.lastError ? this.state.lastError : ""}
</span>
<Paper elevation={3}>
<AssetSearchTable
results={this.state.results}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
onSortChanged={(newSortBy, newSortOrder) => {
this.setState(
{ sortBy: newSortBy, sortOrder: newSortOrder },
() => this.reset().then(() => this.loadNextPage())
);
}}
/>
</Paper>
</>
);
}
}
export default AssetSearchResults;