@uppy/dashboard
Version:
Universal UI plugin for Uppy.
165 lines (164 loc) • 8.17 kB
JavaScript
import { jsx as _jsx } from "preact/jsx-runtime";
import { emaFilter } from '@uppy/utils';
import { Component } from 'preact';
import statusBarStates from './StatusBarStates.js';
import StatusBarUI, {} from './StatusBarUI.js';
const speedFilterHalfLife = 2000;
const ETAFilterHalfLife = 2000;
function getUploadingState(error, isAllComplete, recoveredState, files) {
if (error) {
return statusBarStates.STATE_ERROR;
}
if (isAllComplete) {
return statusBarStates.STATE_COMPLETE;
}
if (recoveredState) {
return statusBarStates.STATE_WAITING;
}
let state = statusBarStates.STATE_WAITING;
const fileIDs = Object.keys(files);
for (let i = 0; i < fileIDs.length; i++) {
const { progress } = files[fileIDs[i]];
// If ANY files are being uploaded right now, show the uploading state.
if (progress.uploadStarted && !progress.uploadComplete) {
return statusBarStates.STATE_UPLOADING;
}
// If files are being preprocessed AND postprocessed at this time, we show the
// preprocess state. If any files are being uploaded we show uploading.
if (progress.preprocess) {
state = statusBarStates.STATE_PREPROCESSING;
}
// If NO files are being preprocessed or uploaded right now, but some files are
// being postprocessed, show the postprocess state.
if (progress.postprocess && state !== statusBarStates.STATE_PREPROCESSING) {
state = statusBarStates.STATE_POSTPROCESSING;
}
}
return state;
}
export default class StatusBar extends Component {
#lastUpdateTime;
#previousUploadedBytes;
#previousSpeed;
#previousETA;
componentDidMount() {
// Initialize ETA calculation variables
this.#lastUpdateTime = performance.now();
this.#previousUploadedBytes = this.props.uppy
.getFiles()
.reduce((pv, file) => pv + (file.progress.bytesUploaded || 0), 0);
// Listen for upload start to reset ETA calculation
this.props.uppy.on('upload', this.#onUploadStart);
}
componentWillUnmount() {
this.props.uppy.off('upload', this.#onUploadStart);
}
#onUploadStart = () => {
const { recoveredState } = this.props.uppy.getState();
this.#previousSpeed = null;
this.#previousETA = null;
if (recoveredState) {
this.#previousUploadedBytes = Object.values(recoveredState.files).reduce((pv, { progress }) => pv + (progress.bytesUploaded || 0), 0);
// We don't set `#lastUpdateTime` at this point because the upload won't
// actually resume until the user asks for it.
return;
}
this.#lastUpdateTime = performance.now();
this.#previousUploadedBytes = 0;
};
#computeSmoothETA(totalBytes) {
if (totalBytes.total == null || totalBytes.total === 0) {
return null;
}
const remaining = totalBytes.total - totalBytes.uploaded;
if (remaining <= 0) {
return null;
}
// When state is restored, lastUpdateTime is still nullish at this point.
this.#lastUpdateTime ??= performance.now();
const dt = performance.now() - this.#lastUpdateTime;
if (dt === 0) {
return Math.round((this.#previousETA ?? 0) / 100) / 10;
}
// Initialize previousUploadedBytes if it's null
if (this.#previousUploadedBytes == null) {
this.#previousUploadedBytes = totalBytes.uploaded;
return null; // Can't calculate speed on first call
}
const uploadedBytesSinceLastTick = totalBytes.uploaded - this.#previousUploadedBytes;
this.#previousUploadedBytes = totalBytes.uploaded;
// uploadedBytesSinceLastTick can be negative in some cases (packet loss?)
// in which case, we wait for next tick to update ETA.
if (uploadedBytesSinceLastTick <= 0) {
return Math.round((this.#previousETA ?? 0) / 100) / 10;
}
const currentSpeed = uploadedBytesSinceLastTick / dt;
// Guard against invalid speed values
if (!Number.isFinite(currentSpeed) || currentSpeed <= 0) {
return null;
}
const filteredSpeed = this.#previousSpeed == null
? currentSpeed
: emaFilter(currentSpeed, this.#previousSpeed, speedFilterHalfLife, dt);
// Guard against invalid filtered speed
if (!Number.isFinite(filteredSpeed) || filteredSpeed <= 0) {
return null;
}
this.#previousSpeed = filteredSpeed;
const instantETA = remaining / filteredSpeed;
// Guard against invalid instantETA
if (!Number.isFinite(instantETA) || instantETA < 0) {
return null;
}
const updatedPreviousETA = Math.max((this.#previousETA ?? 0) - dt, 0);
const filteredETA = this.#previousETA == null
? instantETA
: emaFilter(instantETA, updatedPreviousETA, ETAFilterHalfLife, dt);
// Guard against invalid filteredETA
if (!Number.isFinite(filteredETA) || filteredETA < 0) {
return null;
}
this.#previousETA = filteredETA;
this.#lastUpdateTime = performance.now();
return Math.round(filteredETA / 100) / 10;
}
startUpload = () => {
const { recoveredState } = this.props.uppy.getState();
if (recoveredState) {
this.props.uppy.emit('restore-confirmed');
}
else {
this.props.uppy.upload().catch((() => {
// Error logged in Core
}));
}
};
render() {
const { capabilities, files, allowNewUpload, totalProgress, error, recoveredState, } = this.props.uppy.getState();
const { newFiles, startedFiles, completeFiles, isUploadStarted, isAllComplete, isAllPaused, isUploadInProgress, isSomeGhost, } = this.props.uppy.getObjectOfFilesPerState();
const newFilesOrRecovered = recoveredState ? Object.values(files) : newFiles;
const resumableUploads = !!capabilities.resumableUploads;
const supportsUploadProgress = capabilities.uploadProgress !== false;
let totalSize = null;
let totalUploadedSize = 0;
// Only if all files have a known size, does it make sense to display a total size
if (startedFiles.every((f) => f.progress.bytesTotal != null && f.progress.bytesTotal !== 0)) {
totalSize = 0;
startedFiles.forEach((file) => {
totalSize += file.progress.bytesTotal || 0;
totalUploadedSize += file.progress.bytesUploaded || 0;
});
}
else {
// however uploaded size we will always have
startedFiles.forEach((file) => {
totalUploadedSize += file.progress.bytesUploaded || 0;
});
}
const totalETA = this.#computeSmoothETA({
uploaded: totalUploadedSize,
total: totalSize,
});
return (_jsx(StatusBarUI, { error: error, uploadState: getUploadingState(error, isAllComplete, recoveredState, files || {}), allowNewUpload: allowNewUpload, totalProgress: totalProgress, totalSize: totalSize, totalUploadedSize: totalUploadedSize, isAllComplete: isAllComplete, isAllPaused: isAllPaused, isUploadStarted: isUploadStarted, isUploadInProgress: isUploadInProgress, isSomeGhost: isSomeGhost, recoveredState: recoveredState, complete: completeFiles.length, newFiles: newFilesOrRecovered.length, numUploads: startedFiles.length, totalETA: totalETA, files: files, i18n: this.props.i18n, uppy: this.props.uppy, startUpload: this.startUpload, doneButtonHandler: this.props.doneButtonHandler, resumableUploads: resumableUploads, supportsUploadProgress: supportsUploadProgress, hideProgressDetails: this.props.hideProgressDetails, hideUploadButton: this.props.hideUploadButton, hideRetryButton: this.props.hideRetryButton, hidePauseResumeButton: this.props.hidePauseResumeButton, hideCancelButton: this.props.hideCancelButton, hideAfterFinish: this.props.hideAfterFinish }));
}
}