diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 7b0557bb29fbe7c5e069602821f5ea45ff5abb73..d498b387c5a048c4b0d20c81d179ab8b77dc1517 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -176,6 +176,10 @@ export class PluginContext { return this.tasks.run(task); } + requestTaskAbort(task: Task<any> | number, reason?: string) { + this.tasks.requestAbort(task, reason); + } + dispose() { if (this.disposed) return; this.commands.dispose(); diff --git a/src/mol-plugin/skin/base/components/tasks.scss b/src/mol-plugin/skin/base/components/tasks.scss index 77da59ce53be55595ae7e692e7438bb20e6c8e8c..d5a9e1d92e84fce37f45a8f7b971e93b0d091238 100644 --- a/src/mol-plugin/skin/base/components/tasks.scss +++ b/src/mol-plugin/skin/base/components/tasks.scss @@ -120,7 +120,7 @@ > button { display: inline-block; margin-top: -3px; - font-size: 140%; + // font-size: 140%; } } } diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index ab6203b4b8d49979bbbfbf9d1d63e80d2d86ee6b..b8e0847fadb18dea5803a8866dc7160dd78fc9b9 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -141,9 +141,7 @@ export class ViewportWrapper extends PluginUIComponent { <StateSnapshotViewportControls /> </div> <ViewportControls /> - <div style={{ position: 'absolute', left: '10px', bottom: '10px' }}> - <BackgroundTaskProgress /> - </div> + <BackgroundTaskProgress /> <div className='msp-highlight-toast-wrapper'> <LociLabels /> <Toasts /> diff --git a/src/mol-plugin/ui/state/common.tsx b/src/mol-plugin/ui/state/common.tsx index ed816b3e049f7f244bcf3d916801be138dc2bcf9..7d778a60f6ba85d7f01321e023121c9504282f82 100644 --- a/src/mol-plugin/ui/state/common.tsx +++ b/src/mol-plugin/ui/state/common.tsx @@ -144,6 +144,8 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS this.setState({ busy: true }); try { await this.applyAction(); + } catch { + // eat errors because they should be handled elsewhere } finally { this.busy.next(false); } diff --git a/src/mol-plugin/ui/task.tsx b/src/mol-plugin/ui/task.tsx index aec6e264adec93da9d3dbdd02122750f5f9edfa8..e05171d707aba0749ce29ef52a1ff040092e93af 100644 --- a/src/mol-plugin/ui/task.tsx +++ b/src/mol-plugin/ui/task.tsx @@ -10,6 +10,7 @@ import { OrderedMap } from 'immutable'; import { TaskManager } from '../../mol-plugin/util/task-manager'; import { filter } from 'rxjs/operators'; import { Progress } from '../../mol-task'; +import { IconButton } from './controls/common'; export class BackgroundTaskProgress extends PluginUIComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> { componentDidMount() { @@ -24,13 +25,18 @@ export class BackgroundTaskProgress extends PluginUIComponent<{ }, { tracked: Or state = { tracked: OrderedMap<number, TaskManager.ProgressEvent>() }; render() { - return <div> + return <div className='msp-background-tasks'> {this.state.tracked.valueSeq().map(e => <ProgressEntry key={e!.id} event={e!} />)} </div>; } } class ProgressEntry extends PluginUIComponent<{ event: TaskManager.ProgressEvent }> { + + abort = () => { + this.plugin.requestTaskAbort(this.props.event.id, 'User Request'); + } + render() { const root = this.props.event.progress.root; const subtaskCount = countSubtasks(this.props.event.progress.root) - 1; @@ -39,9 +45,15 @@ class ProgressEntry extends PluginUIComponent<{ event: TaskManager.ProgressEvent : <>[{root.progress.current}/{root.progress.max}]</>; const subtasks = subtaskCount > 0 ? <>[{subtaskCount} subtask(s)]</> - : void 0 - return <div> - {root.progress.message} {pr} {subtasks} + : void 0; + + return <div className='msp-task-state'> + <div> + {root.progress.canAbort && <IconButton onClick={this.abort} icon='abort' title='Abort' />} + <div> + {root.progress.message} {pr} {subtasks} + </div> + </div> </div>; } } diff --git a/src/mol-task/execution/observable.ts b/src/mol-task/execution/observable.ts index dbb19df9cc627c8478c80fb02b2afdbc2e4eeae6..b0140ec739e041cfaac097a75b2c37df722acd4e 100644 --- a/src/mol-task/execution/observable.ts +++ b/src/mol-task/execution/observable.ts @@ -27,7 +27,7 @@ export function ExecuteInContext<T>(ctx: RuntimeContext, task: Task<T>) { } export function ExecuteObservableChild<T>(ctx: RuntimeContext, task: Task<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>) { - return (ctx as ObservableRuntimeContext).runChild(task, progress); + return (ctx as ObservableRuntimeContext).runChild(task as ExposedTask<T>, progress); } export namespace ExecuteObservable { @@ -99,7 +99,7 @@ async function execute<T>(task: ExposedTask<T>, ctx: ObservableRuntimeContext) { UserTiming.markEnd(task) UserTiming.measure(task) if (ctx.info.abortToken.abortRequested) { - abort(ctx.info, ctx.node); + abort(ctx.info); } return ret; } catch (e) { @@ -108,20 +108,21 @@ async function execute<T>(task: ExposedTask<T>, ctx: ObservableRuntimeContext) { if (ctx.node.children.length > 0) { await new Promise(res => { ctx.onChildrenFinished = res; }); } - if (task.onAbort) task.onAbort(); + if (task.onAbort) { + task.onAbort(); + } } if (ExecuteObservable.PRINT_ERRORS_TO_STD_ERR) console.error(e); throw e; } } -function abort(info: ProgressInfo, node: Progress.Node) { +function abort(info: ProgressInfo) { if (!info.abortToken.treeAborted) { info.abortToken.treeAborted = true; abortTree(info.root); notifyObserver(info, now()); } - throw Task.Aborted(info.abortToken.reason); } @@ -157,7 +158,7 @@ class ObservableRuntimeContext implements RuntimeContext { private checkAborted() { if (this.info.abortToken.abortRequested) { - abort(this.info, this.node); + abort(this.info); } } @@ -205,7 +206,7 @@ class ObservableRuntimeContext implements RuntimeContext { return Scheduler.immediatePromise(); } - async runChild<T>(task: Task<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>): Promise<T> { + async runChild<T>(task: ExposedTask<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>): Promise<T> { this.updateProgress(progress); // Create a new child context and add it to the progress tree. diff --git a/src/mol-task/task.ts b/src/mol-task/task.ts index f27c85df7dac2f2e666d8604431c37db6dd99660..be42ea46c4e7ba8ea6ea6c5ea58ba69d17c14630 100644 --- a/src/mol-task/task.ts +++ b/src/mol-task/task.ts @@ -54,9 +54,9 @@ namespace Task { } } - export interface Aborted { isAborted: true, reason: string } + export interface Aborted { isAborted: true, reason: string, toString(): string } export function isAbort(e: any): e is Aborted { return !!e && !!e.isAborted; } - export function Aborted(reason: string): Aborted { return { isAborted: true, reason }; } + export function Aborted(reason: string): Aborted { return { isAborted: true, reason, toString() { return `Aborted${reason ? ': ' + reason : ''}`; } }; } export function create<T>(name: string, f: (ctx: RuntimeContext) => Promise<T>, onAbort?: () => void): Task<T> { return new Impl(name, f, onAbort); diff --git a/src/mol-util/data-source.ts b/src/mol-util/data-source.ts index 42de776085ffa537beac9feeb325f2f74f5b2940..c66d6c68c8b760d639c1ef40788f093d8bfb7ef7 100644 --- a/src/mol-util/data-source.ts +++ b/src/mol-util/data-source.ts @@ -91,13 +91,19 @@ function readData(ctx: RuntimeContext, action: string, data: XMLHttpRequest | Fi reject(error ? error : 'Failed.'); }; - data.onabort = () => reject(Task.Aborted('')); - + let hasError = false; data.onprogress = (e: ProgressEvent) => { - if (e.lengthComputable) { - ctx.update({ message: action, isIndeterminate: false, current: e.loaded, max: e.total }); - } else { - ctx.update({ message: `${action} ${(e.loaded / 1024 / 1024).toFixed(2)} MB`, isIndeterminate: true }); + if (!ctx.shouldUpdate || hasError) return; + + try { + if (e.lengthComputable) { + ctx.update({ message: action, isIndeterminate: false, current: e.loaded, max: e.total }); + } else { + ctx.update({ message: `${action} ${(e.loaded / 1024 / 1024).toFixed(2)} MB`, isIndeterminate: true }); + } + } catch (e) { + hasError = true; + reject(e); } } data.onload = (e: any) => resolve(e); @@ -178,36 +184,36 @@ async function processAjax(ctx: RuntimeContext, asUint8Array: boolean, decompres function ajaxGetInternal(title: string | undefined, url: string, type: 'json' | 'xml' | 'string' | 'binary', decompressGzip: boolean, body?: string): Task<string | Uint8Array> { let xhttp: XMLHttpRequest | undefined = void 0; return Task.create(title ? title : 'Download', async ctx => { - try { - const asUint8Array = type === 'binary'; - if (!asUint8Array && decompressGzip) { - throw 'Decompress is only available when downloading binary data.'; - } + const asUint8Array = type === 'binary'; + if (!asUint8Array && decompressGzip) { + throw 'Decompress is only available when downloading binary data.'; + } - xhttp = RequestPool.get(); + xhttp = RequestPool.get(); - xhttp.open(body ? 'post' : 'get', url, true); - xhttp.responseType = asUint8Array ? 'arraybuffer' : 'text'; - xhttp.send(body); + xhttp.open(body ? 'post' : 'get', url, true); + xhttp.responseType = asUint8Array ? 'arraybuffer' : 'text'; + xhttp.send(body); - ctx.update({ message: 'Waiting for server...', canAbort: true }); - const e = await readData(ctx, 'Downloading...', xhttp, asUint8Array); - const result = await processAjax(ctx, asUint8Array, decompressGzip, e) + await ctx.update({ message: 'Waiting for server...', canAbort: true }); + const e = await readData(ctx, 'Downloading...', xhttp, asUint8Array); + xhttp = void 0; + const result = await processAjax(ctx, asUint8Array, decompressGzip, e) - if (type === 'json') { - ctx.update({ message: 'Parsing JSON...', canAbort: false }); - return JSON.parse(result); - } else if (type === 'xml') { - ctx.update({ message: 'Parsing XML...', canAbort: false }); - return parseXml(result); - } + if (type === 'json') { + await ctx.update({ message: 'Parsing JSON...', canAbort: false }); + return JSON.parse(result); + } else if (type === 'xml') { + await ctx.update({ message: 'Parsing XML...', canAbort: false }); + return parseXml(result); + } - return result; - } finally { + return result; + }, () => { + if (xhttp) { + xhttp.abort(); xhttp = void 0; } - }, () => { - if (xhttp) xhttp.abort(); }); }