feat: progress for the remote sketch creation

Closes #1668

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-11-14 15:57:06 +01:00 committed by Akos Kitta
parent d24a3911f8
commit 1a7784a540
3 changed files with 167 additions and 34 deletions

View File

@ -1,9 +1,22 @@
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { DialogError } from '@theia/core/lib/browser/dialogs';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { CompositeTreeNode } from '@theia/core/lib/browser/tree'; import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { Widget } from '@theia/core/lib/browser/widgets/widget';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
Progress,
ProgressUpdate,
} from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog';
import { v4 } from 'uuid';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
import type { AuthenticationSession } from '../../node/auth/types'; import type { AuthenticationSession } from '../../node/auth/types';
import { AuthenticationClientService } from '../auth/authentication-client-service'; import { AuthenticationClientService } from '../auth/authentication-client-service';
@ -90,7 +103,7 @@ export class NewCloudSketch extends Contribution {
private async createNewSketch( private async createNewSketch(
initialValue?: string | undefined initialValue?: string | undefined
): Promise<URI | undefined> { ): Promise<unknown> {
const widget = await this.widgetContribution.widget; const widget = await this.widgetContribution.widget;
const treeModel = this.treeModelFrom(widget); const treeModel = this.treeModelFrom(widget);
if (!treeModel) { if (!treeModel) {
@ -102,14 +115,24 @@ export class NewCloudSketch extends Contribution {
if (!rootNode) { if (!rootNode) {
return undefined; return undefined;
} }
return this.openWizard(rootNode, treeModel, initialValue);
const newSketchName = await this.newSketchName(rootNode, initialValue);
if (!newSketchName) {
return undefined;
} }
private withProgress(
value: string,
treeModel: CloudSketchbookTreeModel
): (progress: Progress) => Promise<unknown> {
return async (progress: Progress) => {
let result: Create.Sketch | undefined | 'conflict'; let result: Create.Sketch | undefined | 'conflict';
try { try {
result = await this.createApi.createSketch(newSketchName); progress.report({
message: nls.localize(
'arduino/cloudSketch/creating',
"Creating remote sketch '{0}'...",
value
),
});
result = await this.createApi.createSketch(value);
} catch (err) { } catch (err) {
if (isConflict(err)) { if (isConflict(err)) {
result = 'conflict'; result = 'conflict';
@ -118,18 +141,24 @@ export class NewCloudSketch extends Contribution {
} }
} finally { } finally {
if (result) { if (result) {
progress.report({
message: nls.localize(
'arduino/cloudSketch/synchronizing',
"Synchronizing sketchbook, pulling '{0}'...",
value
),
});
await treeModel.refresh(); await treeModel.refresh();
} }
} }
if (result === 'conflict') { if (result === 'conflict') {
return this.createNewSketch(newSketchName); return this.createNewSketch(value);
} }
if (result) { if (result) {
return this.open(treeModel, result); return this.open(treeModel, result);
} }
return undefined; return undefined;
};
} }
private async open( private async open(
@ -183,14 +212,15 @@ export class NewCloudSketch extends Contribution {
return undefined; return undefined;
} }
private async newSketchName( private async openWizard(
rootNode: CompositeTreeNode, rootNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
initialValue?: string | undefined initialValue?: string | undefined
): Promise<string | undefined> { ): Promise<unknown> {
const existingNames = rootNode.children const existingNames = rootNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is) .filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name); .map(({ fileStat }) => fileStat.name);
return new WorkspaceInputDialog( return new NewCloudSketchDialog(
{ {
title: nls.localize( title: nls.localize(
'arduino/newCloudSketch/newSketchTitle', 'arduino/newCloudSketch/newSketchTitle',
@ -216,7 +246,8 @@ export class NewCloudSketch extends Contribution {
); );
}, },
}, },
this.labelProvider this.labelProvider,
(value) => this.withProgress(value, treeModel)
).open(); ).open();
} }
} }
@ -245,3 +276,97 @@ function isErrorWithStatusOf(
} }
return false; return false;
} }
@injectable()
class NewCloudSketchDialog extends WorkspaceInputDialog {
constructor(
@inject(WorkspaceInputDialogProps)
protected override readonly props: WorkspaceInputDialogProps,
@inject(LabelProvider)
protected override readonly labelProvider: LabelProvider,
private readonly withProgress: (
value: string
) => (progress: Progress) => Promise<unknown>
) {
super(props, labelProvider);
}
protected override async accept(): Promise<void> {
if (!this.resolve) {
return;
}
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
const spinner = document.createElement('div');
spinner.classList.add('spinner');
const disposables = new DisposableCollection();
try {
this.toggleButtons(true);
disposables.push(Disposable.create(() => this.toggleButtons(false)));
const closeParent = this.closeCrossNode.parentNode;
closeParent?.removeChild(this.closeCrossNode);
disposables.push(
Disposable.create(() => {
closeParent?.appendChild(this.closeCrossNode);
})
);
this.errorMessageNode.classList.add('progress');
disposables.push(
Disposable.create(() =>
this.errorMessageNode.classList.remove('progress')
)
);
const errorParent = this.errorMessageNode.parentNode;
errorParent?.insertBefore(spinner, this.errorMessageNode);
disposables.push(
Disposable.create(() => errorParent?.removeChild(spinner))
);
const cancellationSource = new CancellationTokenSource();
const progress: Progress = {
id: v4(),
cancel: () => cancellationSource.cancel(),
report: (update: ProgressUpdate) => {
this.setProgressMessage(update);
},
result: Promise.resolve(value),
};
await this.withProgress(value)(progress);
} finally {
disposables.dispose();
}
this.resolve(value);
Widget.detach(this);
}
}
private toggleButtons(disabled: boolean): void {
if (this.acceptButton) {
this.acceptButton.disabled = disabled;
}
if (this.closeButton) {
this.closeButton.disabled = disabled;
}
}
private setProgressMessage(update: ProgressUpdate): void {
if (update.work && update.work.done === update.work.total) {
this.errorMessageNode.innerText = '';
} else {
if (update.message) {
this.errorMessageNode.innerText = update.message;
}
}
}
}

View File

@ -55,6 +55,7 @@
align-items: center; align-items: center;
} }
.p-Widget.dialogOverlay .dialogControl .spinner,
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner { .p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner {
background: var(--theia-icon-loading) center center no-repeat; background: var(--theia-icon-loading) center center no-repeat;
animation: theia-spin 1.25s linear infinite; animation: theia-spin 1.25s linear infinite;
@ -85,3 +86,8 @@
max-height: 400px; max-height: 400px;
} }
} }
.p-Widget.dialogOverlay .error.progress {
color: var(--theia-button-background);
align-self: center;
}

View File

@ -120,7 +120,9 @@
"visitArduinoCloud": "Visit Arduino Cloud to create Cloud Sketches." "visitArduinoCloud": "Visit Arduino Cloud to create Cloud Sketches."
}, },
"cloudSketch": { "cloudSketch": {
"new": "New Remote Sketch" "creating": "Creating remote sketch '{0}'...",
"new": "New Remote Sketch",
"synchronizing": "Synchronizing sketchbook, pulling '{0}'..."
}, },
"common": { "common": {
"all": "All", "all": "All",