frontend/app/SearchComponent.tsx (176 lines of code) (raw):

import React from "react"; import SearchBarFile from "./searchnbrowse/SearchBarFile"; import ndjsonStream from "can-ndjson-stream"; import ResultsPanel from "./searchnbrowse/ResultsPanel"; import PopupPreview from "./PopupPreview.jsx"; import {RouteComponentProps, withRouter} from "react-router-dom"; import { authenticatedFetch } from "./auth"; import SearchComponentContext from "./searchnbrowse/SearchComponentContext"; interface SearchComponentState { searching?: boolean; fileEntries?: FileEntry[]; requestedPreview?: any; currentReader?: ReadableStreamReader<FileEntry>; currentAbort?: any; vaultId?: string; } class SearchComponent extends React.Component<RouteComponentProps, SearchComponentState> { static resultsLimit = 100; constructor(props:RouteComponentProps) { super(props); this.state = { searching: false, fileEntries: [], requestedPreview: undefined, currentReader: undefined, currentAbort: undefined, vaultId: undefined }; this.asyncDownload = this.asyncDownload.bind(this); this.previewRequested = this.previewRequested.bind(this); this.previewClosed = this.previewClosed.bind(this); this.projectClicked = this.projectClicked.bind(this); this.vaultIdUpdated = this.vaultIdUpdated.bind(this); } setStatePromise(newState:SearchComponentState) { return new Promise<void>((resolve, reject) => { try { this.setState(newState, () => resolve()); } catch (err) { reject(err); } }); } async asyncDownload(url:string) { const abortController = new AbortController(); const response = await authenticatedFetch(url, { signal: abortController.signal, }); if (response.status !== 200) { console.error(`Could not load data: server error ${response.status}`); const rawData = await response.text(); console.error(`Server said ${rawData}`); return; } const stream = await ndjsonStream<Uint8Array,FileEntry>(response.body); const reader = stream.getReader(); await this.setStatePromise({ currentReader: reader, currentAbort: abortController, }); const parentComponent = this; const readNextChunk = (reader:ReadableStreamReader<FileEntry>) => { reader.read().then(({ done, value }) => { if (value) { parentComponent.setState( (oldState) => { return { fileEntries: oldState.fileEntries ? oldState.fileEntries.concat([value]) : [value], searching: !done, }; }, () => { if ( parentComponent.state.fileEntries && parentComponent.state.fileEntries.length >= SearchComponent.resultsLimit ) { console.log("Reached limit, stopping"); reader.cancel(); } } ); } else { console.warn("Got no data"); } if (done) { parentComponent.setState({ searching: false }); } else { readNextChunk(reader); } }); } readNextChunk(reader); } abortReadInProgress() { const myRef = this; return new Promise<void>((resolve, reject) => { if (!myRef.state.currentReader) { resolve(); return; } //if(myRef.state.currentAbort) myRef.state.currentAbort.abort(); myRef.state.currentReader.cancel().then((_:void) => { function waitForSearch() { if (myRef.state.searching) { console.log("Waiting for search to cancel..."); window.setTimeout(waitForSearch, 500); } else { console.log("Search cancelled"); resolve(); } } waitForSearch(); }); }); } newSearch(url:string) { this.abortReadInProgress().then((_) => this.setState({ searching: true, fileEntries: [] }, () => this.asyncDownload(url).catch((err) => { console.error(err); this.setState({ searching: false }); }) ) ); } previewRequested(oid:string) { console.log("preview requested: ", oid); this.setState({ requestedPreview: oid }); } previewClosed() { this.setState({ requestedPreview: null }); } projectClicked(projectId:string) { this.props.history.push("/byproject?project=" + projectId); } vaultIdUpdated(newValue:string) { this.setState({vaultId: newValue}); } render() { return ( <div className="windowpanel"> <SearchComponentContext.Provider value={{ vaultId: this.state.vaultId, vaultIdUpdated: this.vaultIdUpdated}}> <SearchBarFile searchUrlChanged={(newUrl) => { if (!newUrl.includes("/undefined/")) { this.newSearch(newUrl); } }} /> <span style={{ float: "right", marginRight: "2em", display: this.state.searching ? "inline-block" : "none", }} > Loaded {this.state.fileEntries?.length}... </span> <ResultsPanel entries={this.state.fileEntries ?? []} previewRequestedCb={this.previewRequested} projectClicked={this.projectClicked} /> {this.state.requestedPreview ? ( <PopupPreview oid={this.state.requestedPreview} dialogClose={this.previewClosed} /> ) : ( "" )} </SearchComponentContext.Provider> </div> ); } } export default withRouter(SearchComponent);