Skip to content
Snippets Groups Projects
Commit 04d39e0e authored by David Sehnal's avatar David Sehnal
Browse files

support Task cancellation in plugin UI

parent c5e64b39
No related branches found
No related tags found
No related merge requests found
......@@ -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();
......
......@@ -120,7 +120,7 @@
> button {
display: inline-block;
margin-top: -3px;
font-size: 140%;
// font-size: 140%;
}
}
}
......
......@@ -141,9 +141,7 @@ export class ViewportWrapper extends PluginUIComponent {
<StateSnapshotViewportControls />
</div>
<ViewportControls />
<div style={{ position: 'absolute', left: '10px', bottom: '10px' }}>
<BackgroundTaskProgress />
</div>
<div className='msp-highlight-toast-wrapper'>
<LociLabels />
<Toasts />
......
......@@ -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);
}
......
......@@ -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>
: 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>;
}
}
......
......@@ -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.
......
......@@ -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);
......
......@@ -91,14 +91,20 @@ 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 (!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,7 +184,6 @@ 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.';
......@@ -190,24 +195,25 @@ function ajaxGetInternal(title: string | undefined, url: string, type: 'json' |
xhttp.responseType = asUint8Array ? 'arraybuffer' : 'text';
xhttp.send(body);
ctx.update({ message: 'Waiting for server...', canAbort: true });
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 });
await ctx.update({ message: 'Parsing JSON...', canAbort: false });
return JSON.parse(result);
} else if (type === 'xml') {
ctx.update({ message: 'Parsing XML...', canAbort: false });
await ctx.update({ message: 'Parsing XML...', canAbort: false });
return parseXml(result);
}
return result;
} finally {
}, () => {
if (xhttp) {
xhttp.abort();
xhttp = void 0;
}
}, () => {
if (xhttp) xhttp.abort();
});
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment