frontend/app/Lightbox/NewLightbox.tsx (257 lines of code) (raw):
import React, {useContext, useEffect, useState} from "react";
import {RouteComponentProps} from "react-router";
import {makeStyles, Snackbar, Typography} from "@material-ui/core";
import {
ArchiveEntry,
LightboxBulk,
LightboxBulkResponse,
LightboxDetailsResponse, LightboxEntry, ObjectListResponse, RestoreStatusResponse,
UserDetails,
UserDetailsResponse
} from "../types";
import UserSelector from "../common/UserSelector";
import BulkSelectionsScroll from "./BulkSelectionsScroll";
import axios from "axios";
import {formatError} from "../common/ErrorViewComponent";
import NewSearchComponent from "../common/NewSearchComponent";
import MuiAlert from "@material-ui/lab/Alert";
import EntryDetails from "../Entry/EntryDetails";
import LightboxDetailsInsert from "./LightboxDetailsInsert";
import {UserContext} from "../Context/UserContext";
import BrowseFilter from "../browse/BrowseFilter";
import {Simulate} from "react-dom/test-utils";
import select = Simulate.select;
const useStyles = makeStyles({
browserWindow: {
display: "grid",
gridTemplateColumns: "repeat(20, 5%)",
gridTemplateRows: "[top] 40px [title-area] 200px [filter-area] 120px [info-area] auto [bottom]",
height: "95vh"
},
userNameBox: {
gridColumnStart: 1,
gridColumnEnd: 4,
gridRowStart: "top",
gridRowEnd: "title-area"
},
userNameText: {
fontSize: "1.6rem",
fontWeight: 800,
whiteSpace: "nowrap"
},
userSelectorBox: {
gridColumnStart: -5,
gridColumnEnd: -1,
gridRowStart: "top",
gridRowEnd: "title-area"
},
bulkSelectorBox: {
gridColumnStart: 1,
gridColumnEnd: -5,
gridRowStart: "title-area",
gridRowEnd: "filter-area",
overflowY: "hidden",
overflowX: "auto"
},
itemsArea: {
gridColumnStart: 1,
gridColumnEnd: -4,
gridRowStart: "info-area",
gridRowEnd: "bottom",
padding: "1em"
},
detailsArea: {
gridColumnStart: -5,
gridColumnEnd: -1,
gridRowStart: "title-area",
gridRowEnd: "bottom",
padding: "1em",
borderLeft: "1px"
},
filterArea: {
gridColumnStart: 1,
gridColumnEnd: -5,
gridRowStart: "filter-area",
gridRowEnd: "info-area",
padding: "1em"
}
});
const NewLightbox:React.FC<RouteComponentProps> = (props) => {
const classes = useStyles();
//bulk selector related state
const [selectedUser, setSelectedUser] = useState<string>("my");
const [selectedBulk, setSelectedBulk] = useState<string|undefined>(undefined);
const [expiryDays, setExpiryDays] = useState<number>(10);
//general state
const [lastError, setLastError] = useState<string|undefined>(undefined);
const [showingAlert, setShowingAlert] = useState(false);
const [loading, setLoading] = useState(false);
//lightbox-related information for every entry associated with
const [lightboxDetails, setLightboxDetails] = useState<Record<string, LightboxEntry>>({});
//used to refresh the lightbox state if a user removes and object
const [newlyLightboxed, setNewlyLightboxed] = useState<string[]>([]);
//entry view related state
const [selectedEntry, setSelectedEntry] = useState<ArchiveEntry|undefined>(undefined);
const [pageSize, setPageSize] = useState(100);
const [itemLimit, setItemLimit] = useState(300);
const [basicSearchUrl, setBasicSearchUrl] = useState<string|undefined>(undefined);
const [showingArchiveSpinner, setShowingArchiveSpinner] = useState(false);
const [filterString, setFilterString] = useState<string>("");
const [typeString, setTypeString] = useState<string>("Any");
const userContext = useContext(UserContext);
const userDisplayName = ()=>{
if(selectedUser!=="my") return selectedUser + "'s ";
if(userContext.profile) return "My ";
return undefined;
}
const performLoad = (controller:AbortController) => {
const detailsRequest = axios.get<LightboxDetailsResponse>("/api/lightbox/" + selectedUser+"/details", {signal: controller.signal});
const configRequest = axios.get<ObjectListResponse<string>>("/api/config", {signal: controller.signal});
return Promise.all([detailsRequest, configRequest]);
}
const refreshData = async (controller:AbortController) => {
setLoading(true);
try {
if(selectedUser=="") {
console.log("Not trying to load lightbox for empty selectedUser string");
return;
}
console.log(`debug: loading lightbox details for '${selectedUser}'`);
const results = await performLoad(controller);
const detailsResult = results[0].data as LightboxDetailsResponse;
const configResult = results[1].data as ObjectListResponse<string>;
setLightboxDetails(detailsResult.entries);
console.log("debug: lightbox details loaded");
setExpiryDays(configResult.entries.length>0 ? parseInt(configResult.entries[0]) : 10);
} catch(err) {
if(err.name && (err.name==="CanceledError" || err.name==="Aborted")) {
console.log("Canceled data load because user changed")
} else {
console.error("Could not load in lightbox data: ", err);
setLastError(formatError(err, false));
setShowingAlert(true);
}
}
}
/**
* ensure that we de-select any item that was selected when we change the user, because it isn't valid for the
* new user's lightbox
*/
useEffect(()=>{
setSelectedEntry(undefined);
setSelectedBulk(undefined);
}, [selectedUser]);
/**
* updates our record of the archive status for the currently selected item
*/
const checkArchiveStatus = async (archiveEntryId:string) => {
try {
const response = await axios.get<RestoreStatusResponse>(`/api/archive/status/${archiveEntryId}?user=${selectedUser}`)
setShowingArchiveSpinner(false);
if(lightboxDetails.hasOwnProperty(response.data.fileId)) {
const updatedEntry = Object.assign({},
lightboxDetails[response.data.fileId],
{
restoreStatus: response.data.restoreStatus,
availableUntil: response.data.expiry
});
setLightboxDetails((prevState)=>Object.assign({}, prevState, {[response.data.fileId]: updatedEntry}));
} else {
console.error("could not find record with id ", response.data.fileId, " in the lightbox data");
setLastError("internal error: could not find record with id " + response.data.fileId);
setShowingAlert(true);
}
} catch(err) {
if(err.name && (err.name==="AbortError" || err.name==="CanceledError") ) {
console.log("Cancelled previous load");
} else {
console.error(err);
setLastError(formatError(err, false));
setShowingAlert(true);
setShowingArchiveSpinner(false);
}
}
}
const redoRestore = async (entryId:string) => {
const url = `/api/lightbox/${selectedUser}/redoRestore/${entryId}`;
setShowingArchiveSpinner(true);
try {
await axios.put(url);
console.info("redo restore requested, updating archive status in 1s...");
window.setTimeout(()=>checkArchiveStatus(entryId), 1000);
} catch(err) {
console.error(`could not request redo restore on ${entryId}: `, err);
setLastError(formatError(err, false));
setShowingAlert(true);
setShowingArchiveSpinner(false);
}
}
//update the search view if the selected user or bulk param changed
useEffect(()=>{
const userParam:Record<string,string> = {user: selectedUser};
const withBulkIdParam:Record<string,string> = selectedBulk ? Object.assign({bulkId: selectedBulk}, userParam) : userParam;
const paramString = Object.keys(withBulkIdParam)
.map(key=>`${key}=${withBulkIdParam[key]}`)
.join("&");
setBasicSearchUrl(`/api/search/myLightBox?${paramString}`);
}, [selectedUser, selectedBulk]);
//reload the search if the currently selected bulk changes
useEffect(()=>{
const controller = new AbortController();
refreshData(controller);
return ()=>{
controller.abort();
}
}, [selectedBulk, selectedUser]);
const handleComponentError = (desc:string) => {
setLastError(desc);
setShowingAlert(true);
}
const closeAlert = ()=> {
setShowingAlert(false);
}
return <div className={classes.browserWindow}>
<Snackbar open={showingAlert} onClose={closeAlert} autoHideDuration={8000}>
<MuiAlert severity="error" onClose={closeAlert}>{lastError}</MuiAlert>
</Snackbar>
<div className={classes.userNameBox}>
<Typography className={classes.userNameText}>{userDisplayName() ? `${userDisplayName()} Lightbox` : "Lightbox"}</Typography>
</div>
{ userContext.profile?.isAdmin ? <div className={classes.userSelectorBox}>
<UserSelector onChange={(newUser)=>setSelectedUser(newUser)} selectedUser={selectedUser}/>
</div> : null }
<div className={classes.bulkSelectorBox}>
<BulkSelectionsScroll onSelected={(newId:string|undefined)=>setSelectedBulk(newId)}
currentSelection={selectedBulk}
forUser={selectedUser}
isAdmin={userContext.profile?.isAdmin ?? false}
expiryDays={expiryDays}
onError={handleComponentError}
/>
</div>
<div className={classes.filterArea}>
<BrowseFilter filterString={filterString}
filterStringChanged={(newString)=>setFilterString(newString)}
typeString={typeString}
typeStringChanged={(newString)=>setTypeString(newString)}/>
</div>
<div className={classes.itemsArea}>
<NewSearchComponent pageSize={pageSize}
itemLimit={itemLimit}
basicQueryUrl={basicSearchUrl}
newlyLightboxed={newlyLightboxed}
selectedEntry={selectedEntry}
onEntryClicked={(newEntry)=>setSelectedEntry(newEntry)}
onErrorOccurred={handleComponentError}
filterString={filterString}
typeString={typeString}
/>
</div>
<div className={classes.detailsArea}>
{selectedEntry ? <EntryDetails entry={selectedEntry}
autoPlay={true}
showJobs={true}
loadJobs={false}
lightboxedCb={(entryId:string)=>setNewlyLightboxed((prevState) => prevState.concat(entryId))}
preLightboxInsert={
<LightboxDetailsInsert
checkArchiveStatusClicked={()=>checkArchiveStatus(selectedEntry.id)}
redoRestoreClicked={redoRestore}
archiveEntryId={selectedEntry?.id}
archiveEntryPath={selectedEntry?.path}
lightboxEntry={lightboxDetails[selectedEntry.id]}
user={selectedUser}
showingArchiveSpinner={showingArchiveSpinner}
/>
}
/> : undefined }
</div>
</div>
}
export default NewLightbox;