mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-18 16:56:33 +00:00
ATL-786: Progress indication for install.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
parent
8071298598
commit
9aff90b0af
@ -44,7 +44,7 @@ import { WorkspaceService } from './theia/workspace/workspace-service';
|
||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { OutputService } from '../common/protocol/output-service';
|
||||
import { ResponseService } from '../common/protocol/response-service';
|
||||
import { ArduinoPreferences } from './arduino-preferences';
|
||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
||||
import { SaveAsSketch } from './contributions/save-as-sketch';
|
||||
@ -151,8 +151,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
@inject(ExecutableService)
|
||||
protected executableService: ExecutableService;
|
||||
|
||||
@inject(OutputService)
|
||||
protected readonly outputService: OutputService;
|
||||
@inject(ResponseService)
|
||||
protected readonly responseService: ResponseService;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
@ -120,8 +120,8 @@ import { OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl, Ou
|
||||
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
|
||||
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
|
||||
import { OutputServiceImpl } from './output-service-impl';
|
||||
import { OutputServicePath, OutputService } from '../common/protocol/output-service';
|
||||
import { ResponseServiceImpl } from './response-service-impl';
|
||||
import { ResponseServicePath, ResponseService } from '../common/protocol/response-service';
|
||||
import { NotificationCenter } from './notification-center';
|
||||
import { NotificationServicePath, NotificationServiceServer } from '../common/protocol';
|
||||
import { About } from './contributions/about';
|
||||
@ -159,6 +159,10 @@ import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco
|
||||
import { DebugEditorModel } from './theia/debug/debug-editor-model';
|
||||
import { DebugEditorModelFactory } from '@theia/debug/lib/browser/editor/debug-editor-model';
|
||||
import { StorageWrapper } from './storage-wrapper';
|
||||
import { NotificationManager } from './theia/messages/notifications-manager';
|
||||
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
|
||||
import { NotificationsRenderer } from './theia/messages/notifications-renderer';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
@ -383,11 +387,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, ArchiveSketch);
|
||||
Contribution.configure(bind, AddZipLibrary);
|
||||
|
||||
bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => {
|
||||
WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService);
|
||||
return outputService;
|
||||
bind(ResponseServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, responseService) => {
|
||||
WebSocketConnectionProvider.createProxy(container, ResponseServicePath, responseService);
|
||||
return responseService;
|
||||
});
|
||||
bind(OutputService).toService(OutputServiceImpl);
|
||||
bind(ResponseService).toService(ResponseServiceImpl);
|
||||
|
||||
bind(NotificationCenter).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(NotificationCenter);
|
||||
@ -439,4 +443,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
|
||||
bind(StorageWrapper).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(StorageWrapper);
|
||||
|
||||
bind(NotificationManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaNotificationManager).toService(NotificationManager);
|
||||
bind(NotificationsRenderer).toSelf().inSingletonScope();
|
||||
rebind(TheiaNotificationsRenderer).toService(NotificationsRenderer);
|
||||
});
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { BoardsService, BoardsPackage } from '../../common/protocol/boards-service';
|
||||
import { BoardsService, BoardsPackage, Board } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
|
||||
import { InstallationProgressDialog } from '../widgets/progress-dialog';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { Installable } from '../../common/protocol';
|
||||
import { ResponseServiceImpl } from '../response-service-impl';
|
||||
|
||||
/**
|
||||
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
|
||||
@ -23,9 +24,15 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(ResponseServiceImpl)
|
||||
protected readonly responseService: ResponseServiceImpl;
|
||||
|
||||
@inject(BoardsListWidgetFrontendContribution)
|
||||
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||
|
||||
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
|
||||
protected notifications: Board[] = [];
|
||||
|
||||
onStart(): void {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this));
|
||||
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
|
||||
@ -33,22 +40,29 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
|
||||
protected ensureCoreExists(config: BoardsConfig.Config): void {
|
||||
const { selectedBoard } = config;
|
||||
if (selectedBoard) {
|
||||
if (selectedBoard && !this.notifications.find(board => Board.sameAs(board, selectedBoard))) {
|
||||
this.notifications.push(selectedBoard);
|
||||
this.boardsService.search({}).then(packages => {
|
||||
const candidates = packages
|
||||
.filter(pkg => BoardsPackage.contains(selectedBoard, pkg))
|
||||
.filter(({ installable, installedVersion }) => installable && !installedVersion);
|
||||
for (const candidate of candidates) {
|
||||
const candidate = candidates[0];
|
||||
if (candidate) {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Install Manually', 'Yes').then(async answer => {
|
||||
const index = this.notifications.findIndex(board => Board.sameAs(board, selectedBoard));
|
||||
if (index !== -1) {
|
||||
this.notifications.splice(index, 1);
|
||||
}
|
||||
if (answer === 'Yes') {
|
||||
const dialog = new InstallationProgressDialog(candidate.name, candidate.availableVersions[0]);
|
||||
dialog.open();
|
||||
try {
|
||||
await this.boardsService.install({ item: candidate });
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
await Installable.installWithProgress({
|
||||
installable: this.boardsService,
|
||||
item: candidate,
|
||||
messageService: this.messageService,
|
||||
responseService: this.responseService,
|
||||
version: candidate.availableVersions[0]
|
||||
});
|
||||
return
|
||||
}
|
||||
if (answer) {
|
||||
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
|
||||
|
@ -33,9 +33,14 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
]);
|
||||
}
|
||||
|
||||
async install({ item, version }: { item: BoardsPackage; version: string; }): Promise<void> {
|
||||
await super.install({ item, version });
|
||||
this.messageService.info(`Successfully installed platform ${item.name}:${version}.`, { timeout: 3000 });
|
||||
protected async install({ item, progressId, version }: { item: BoardsPackage, progressId: string, version: string; }): Promise<void> {
|
||||
await super.install({ item, progressId, version });
|
||||
this.messageService.info(`Successfully installed platform ${item.name}:${version}`, { timeout: 3000 });
|
||||
}
|
||||
|
||||
protected async uninstall({ item, progressId }: { item: BoardsPackage, progressId: string }): Promise<void> {
|
||||
await super.uninstall({ item, progressId });
|
||||
this.messageService.info(`Successfully uninstalled platform ${item.name}:${item.installedVersion}`, { timeout: 3000 });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { InstallationProgressDialog } from '../widgets/progress-dialog';
|
||||
import { LibraryService } from '../../common/protocol';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ResponseServiceImpl } from '../response-service-impl';
|
||||
import { Installable, LibraryService } from '../../common/protocol';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class AddZipLibrary extends SketchContribution {
|
||||
@ -14,6 +14,9 @@ export class AddZipLibrary extends SketchContribution {
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(ResponseServiceImpl)
|
||||
protected readonly responseService: ResponseServiceImpl;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
|
||||
@ -69,11 +72,14 @@ export class AddZipLibrary extends SketchContribution {
|
||||
}
|
||||
|
||||
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
|
||||
const dialog = new InstallationProgressDialog('Installing library', 'zip');
|
||||
try {
|
||||
this.outputChannelManager.getChannel('Arduino').clear();
|
||||
dialog.open();
|
||||
await this.libraryService.installZip({ zipUri, overwrite });
|
||||
await Installable.doWithProgress({
|
||||
messageService: this.messageService,
|
||||
progressText: `Processing ${new URI(zipUri).path.base}`,
|
||||
responseService: this.responseService,
|
||||
run: () => this.libraryService.installZip({ zipUri, overwrite })
|
||||
});
|
||||
this.messageService.info(`Successfully installed library from ${new URI(zipUri).path.base} archive`, { timeout: 3000 });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
const match = error.message.match(/library (.*?) already installed/);
|
||||
@ -88,8 +94,6 @@ export class AddZipLibrary extends SketchContribution {
|
||||
}
|
||||
this.messageService.error(error.toString());
|
||||
throw error;
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,6 +82,7 @@
|
||||
"colors": {
|
||||
"list.highlightForeground": "#005c5f",
|
||||
"list.activeSelectionBackground": "#005c5f",
|
||||
"progressBar.background": "#005c5f",
|
||||
"editor.background": "#ffffff",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editor.foreground": "#434f54",
|
||||
|
@ -37,7 +37,7 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
|
||||
]);
|
||||
}
|
||||
|
||||
protected async install({ item, version }: { item: LibraryPackage, version: Installable.Version }): Promise<void> {
|
||||
protected async install({ item, progressId, version }: { item: LibraryPackage, progressId: string, version: Installable.Version }): Promise<void> {
|
||||
const dependencies = await this.service.listDependencies({ item, version, filterSelf: true });
|
||||
let installDependencies: boolean | undefined = undefined;
|
||||
if (dependencies.length) {
|
||||
@ -84,11 +84,16 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
|
||||
}
|
||||
|
||||
if (typeof installDependencies === 'boolean') {
|
||||
await this.service.install({ item, version, installDependencies });
|
||||
this.messageService.info(`Successfully installed library ${item.name}:${version}.`, { timeout: 3000 });
|
||||
await this.service.install({ item, version, progressId, installDependencies });
|
||||
this.messageService.info(`Successfully installed library ${item.name}:${version}`, { timeout: 3000 });
|
||||
}
|
||||
}
|
||||
|
||||
protected async uninstall({ item, progressId }: { item: LibraryPackage, progressId: string }): Promise<void> {
|
||||
await super.uninstall({ item, progressId });
|
||||
this.messageService.info(`Successfully uninstalled library ${item.name}:${item.installedVersion}`, { timeout: 3000 });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
|
||||
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { OutputService, OutputMessage } from '../common/protocol/output-service';
|
||||
|
||||
@injectable()
|
||||
export class OutputServiceImpl implements OutputService {
|
||||
|
||||
@inject(OutputContribution)
|
||||
protected outputContribution: OutputContribution;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected outputChannelManager: OutputChannelManager;
|
||||
|
||||
append(message: OutputMessage): void {
|
||||
const { chunk } = message;
|
||||
const channel = this.outputChannelManager.getChannel('Arduino');
|
||||
channel.show({ preserveFocus: true });
|
||||
channel.append(chunk);
|
||||
}
|
||||
|
||||
}
|
34
arduino-ide-extension/src/browser/response-service-impl.ts
Normal file
34
arduino-ide-extension/src/browser/response-service-impl.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
|
||||
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { ResponseService, OutputMessage, ProgressMessage } from '../common/protocol/response-service';
|
||||
|
||||
@injectable()
|
||||
export class ResponseServiceImpl implements ResponseService {
|
||||
|
||||
@inject(OutputContribution)
|
||||
protected outputContribution: OutputContribution;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected outputChannelManager: OutputChannelManager;
|
||||
|
||||
protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
|
||||
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
|
||||
|
||||
appendToOutput(message: OutputMessage): void {
|
||||
const { chunk } = message;
|
||||
const channel = this.outputChannelManager.getChannel('Arduino');
|
||||
channel.show({ preserveFocus: true });
|
||||
channel.append(chunk);
|
||||
}
|
||||
|
||||
clearArduinoChannel(): void {
|
||||
this.outputChannelManager.getChannel('Arduino').clear();
|
||||
}
|
||||
|
||||
reportProgress(progress: ProgressMessage): void {
|
||||
this.progressDidChangeEmitter.fire(progress);
|
||||
}
|
||||
|
||||
}
|
@ -67,3 +67,20 @@ button.theia-button.secondary {
|
||||
button.theia-button.main {
|
||||
color: var(--theia-button-foreground);
|
||||
}
|
||||
|
||||
/* To make the progress-bar slightly thicker, and use the color from the status bar */
|
||||
.theia-progress-bar-container {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.theia-progress-bar {
|
||||
height: 4px;
|
||||
width: 3%;
|
||||
animation: progress-animation 1.3s 0s infinite cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
.theia-notification-item-progressbar {
|
||||
height: 4px;
|
||||
width: 66%;
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import { NotificationComponent } from './notification-component';
|
||||
import { NotificationCenterComponent as TheiaNotificationCenterComponent } from '@theia/messages/lib/browser/notification-center-component'
|
||||
|
||||
const PerfectScrollbar = require('react-perfect-scrollbar');
|
||||
|
||||
export class NotificationCenterComponent extends TheiaNotificationCenterComponent {
|
||||
|
||||
render(): React.ReactNode {
|
||||
const empty = this.state.notifications.length === 0;
|
||||
const title = empty ? 'NO NEW NOTIFICATIONS' : 'NOTIFICATIONS';
|
||||
return (
|
||||
<div className={`theia-notifications-container theia-notification-center ${this.state.visibilityState === 'center' ? 'open' : 'closed'}`}>
|
||||
<div className='theia-notification-center-header'>
|
||||
<div className='theia-notification-center-header-title'>{title}</div>
|
||||
<div className='theia-notification-center-header-actions'>
|
||||
<ul className='theia-notification-actions'>
|
||||
<li className='collapse' title='Hide Notification Center' onClick={this.onHide} />
|
||||
<li className='clear-all' title='Clear All' onClick={this.onClearAll} />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<PerfectScrollbar className='theia-notification-list-scroll-container'>
|
||||
<div className='theia-notification-list'>
|
||||
{this.state.notifications.map(notification =>
|
||||
<NotificationComponent key={notification.messageId} notification={notification} manager={this.props.manager} />
|
||||
)}
|
||||
</div>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import * as React from 'react';
|
||||
import { NotificationComponent as TheiaNotificationComponent } from '@theia/messages/lib/browser/notification-component'
|
||||
|
||||
export class NotificationComponent extends TheiaNotificationComponent {
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { messageId, message, type, collapsed, expandable, source, actions } = this.props.notification;
|
||||
return (<div key={messageId} className='theia-notification-list-item'>
|
||||
<div className={`theia-notification-list-item-content ${collapsed ? 'collapsed' : ''}`}>
|
||||
<div className='theia-notification-list-item-content-main'>
|
||||
<div className={`theia-notification-icon theia-notification-icon-${type}`} />
|
||||
<div className='theia-notification-message'>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} onClick={this.onMessageClick} />
|
||||
</div>
|
||||
<ul className='theia-notification-actions'>
|
||||
{expandable && (
|
||||
<li className={collapsed ? 'expand' : 'collapse'} title={collapsed ? 'Expand' : 'Collapse'}
|
||||
data-message-id={messageId} onClick={this.onToggleExpansion} />
|
||||
)}
|
||||
{!this.isProgress && (<li className='clear' title='Clear' data-message-id={messageId} onClick={this.onClear} />)}
|
||||
</ul>
|
||||
</div>
|
||||
{(source || !!actions.length) && (
|
||||
<div className='theia-notification-list-item-content-bottom'>
|
||||
<div className='theia-notification-source'>
|
||||
{source && (<span>{source}</span>)}
|
||||
</div>
|
||||
<div className='theia-notification-buttons'>
|
||||
{actions && actions.map((action, index) => (
|
||||
<button key={messageId + `-action-${index}`} className='theia-button'
|
||||
data-message-id={messageId} data-action={action}
|
||||
onClick={this.onAction}>
|
||||
{action}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{this.renderProgressBar()}
|
||||
</div>);
|
||||
}
|
||||
|
||||
private renderProgressBar(): React.ReactNode {
|
||||
if (!this.isProgress) {
|
||||
return undefined;
|
||||
}
|
||||
if (!Number.isNaN(this.props.notification.progress)) {
|
||||
return <div className='theia-notification-item-progress'>
|
||||
<div className='theia-notification-item-progressbar' style={{ width: `${this.props.notification.progress}%` }} />
|
||||
</div>;
|
||||
}
|
||||
return <div className='theia-progress-bar-container'>
|
||||
<div className='theia-progress-bar' />
|
||||
</div>
|
||||
}
|
||||
|
||||
private get isProgress(): boolean {
|
||||
return typeof this.props.notification.progress === 'number';
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { NotificationComponent } from './notification-component';
|
||||
import { NotificationToastsComponent as TheiaNotificationToastsComponent } from '@theia/messages/lib/browser/notification-toasts-component'
|
||||
|
||||
export class NotificationToastsComponent extends TheiaNotificationToastsComponent {
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<div className={`theia-notifications-container theia-notification-toasts ${this.state.visibilityState === 'toasts' ? 'open' : 'closed'}`}>
|
||||
<div className='theia-notification-list'>
|
||||
{this.state.toasts.map(notification => <NotificationComponent key={notification.messageId} notification={notification} manager={this.props.manager} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { CancellationToken } from '@theia/core/lib/common/cancellation';
|
||||
import { ProgressMessage, ProgressUpdate } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||
|
||||
@injectable()
|
||||
export class NotificationManager extends TheiaNotificationManager {
|
||||
|
||||
async reportProgress(messageId: string, update: ProgressUpdate, originalMessage: ProgressMessage, cancellationToken: CancellationToken): Promise<void> {
|
||||
const notification = this.find(messageId);
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
this.clear(messageId);
|
||||
} else {
|
||||
notification.message = originalMessage.text && update.message ? `${originalMessage.text}: ${update.message}` :
|
||||
originalMessage.text || update?.message || notification.message;
|
||||
|
||||
// Unlike in Theia, we allow resetting the progress monitor to NaN to enforce unknown progress.
|
||||
const candidate = this.toPlainProgress(update);
|
||||
notification.progress = typeof candidate === 'number' ? candidate : notification.progress;
|
||||
}
|
||||
this.fireUpdatedEvent();
|
||||
}
|
||||
|
||||
protected toPlainProgress(update: ProgressUpdate): number | undefined {
|
||||
if (!update.work) {
|
||||
return undefined;
|
||||
}
|
||||
if (Number.isNaN(update.work.done) || Number.isNaN(update.work.total)) {
|
||||
return Number.NaN; // This should trigger the unknown monitor.
|
||||
}
|
||||
return Math.min(update.work.done / update.work.total * 100, 100);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { injectable } from 'inversify';
|
||||
import { NotificationCenterComponent } from './notification-center-component';
|
||||
import { NotificationToastsComponent } from './notification-toasts-component';
|
||||
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
|
||||
|
||||
@injectable()
|
||||
export class NotificationsRenderer extends TheiaNotificationsRenderer {
|
||||
|
||||
protected render(): void {
|
||||
ReactDOM.render(<div>
|
||||
<NotificationToastsComponent manager={this.manager} corePreferences={this.corePreferences} />
|
||||
<NotificationCenterComponent manager={this.manager} />
|
||||
</div>, this.container);
|
||||
}
|
||||
|
||||
}
|
@ -3,16 +3,15 @@ import debounce = require('lodash.debounce');
|
||||
import { Event } from '@theia/core/lib/common/event';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { OutputCommands } from '@theia/output/lib/browser/output-commands';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { Searchable } from '../../../common/protocol/searchable';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { InstallationProgressDialog, UninstallationProgressDialog } from '../progress-dialog';
|
||||
import { SearchBar } from './search-bar';
|
||||
import { ListWidget } from './list-widget';
|
||||
import { ComponentList } from './component-list';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
import { ResponseServiceImpl } from '../../response-service-impl';
|
||||
|
||||
export class FilterableListContainer<T extends ArduinoComponent> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
|
||||
|
||||
@ -84,20 +83,14 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
|
||||
}
|
||||
|
||||
protected async install(item: T, version: Installable.Version): Promise<void> {
|
||||
const { install, searchable, itemLabel } = this.props;
|
||||
const dialog = new InstallationProgressDialog(itemLabel(item), version);
|
||||
try {
|
||||
dialog.open();
|
||||
await this.clearArduinoChannel();
|
||||
await install({ item, version });
|
||||
const items = await searchable.search({ query: this.state.filterText });
|
||||
this.setState({ items: this.sort(items) });
|
||||
} catch (error) {
|
||||
this.props.messageService.error(error instanceof Error ? error.message : String(error));
|
||||
throw error;
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
const { install, searchable } = this.props;
|
||||
await Installable.doWithProgress({
|
||||
...this.props,
|
||||
progressText: `Processing ${item.name}:${version}`,
|
||||
run: ({ progressId }) => install({ item, progressId, version })
|
||||
});
|
||||
const items = await searchable.search({ query: this.state.filterText });
|
||||
this.setState({ items: this.sort(items) });
|
||||
}
|
||||
|
||||
protected async uninstall(item: T): Promise<void> {
|
||||
@ -110,21 +103,14 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
const { uninstall, searchable, itemLabel } = this.props;
|
||||
const dialog = new UninstallationProgressDialog(itemLabel(item));
|
||||
try {
|
||||
await this.clearArduinoChannel();
|
||||
dialog.open();
|
||||
await uninstall({ item });
|
||||
const items = await searchable.search({ query: this.state.filterText });
|
||||
this.setState({ items: this.sort(items) });
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async clearArduinoChannel(): Promise<void> {
|
||||
return this.props.commandService.executeCommand(OutputCommands.CLEAR.id, { name: 'Arduino' });
|
||||
const { uninstall, searchable } = this.props;
|
||||
await Installable.doWithProgress({
|
||||
...this.props,
|
||||
progressText: `Processing ${item.name}${item.installedVersion ? `:${item.installedVersion}` : ''}`,
|
||||
run: ({ progressId }) => uninstall({ item, progressId })
|
||||
});
|
||||
const items = await searchable.search({ query: this.state.filterText });
|
||||
this.setState({ items: this.sort(items) });
|
||||
}
|
||||
|
||||
}
|
||||
@ -139,9 +125,10 @@ export namespace FilterableListContainer {
|
||||
readonly resolveContainer: (element: HTMLElement) => void;
|
||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||
readonly filterTextChangeEvent: Event<string | undefined>;
|
||||
readonly install: ({ item, version }: { item: T, version: Installable.Version }) => Promise<void>;
|
||||
readonly uninstall: ({ item }: { item: T }) => Promise<void>;
|
||||
readonly messageService: MessageService;
|
||||
readonly responseService: ResponseServiceImpl;
|
||||
readonly install: ({ item, progressId, version }: { item: T, progressId: string, version: Installable.Version }) => Promise<void>;
|
||||
readonly uninstall: ({ item, progressId }: { item: T, progressId: string }) => Promise<void>;
|
||||
readonly commandService: CommandService;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { injectable, postConstruct, inject } from 'inversify';
|
||||
import { Widget } from '@phosphor/widgets';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
@ -7,12 +8,11 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { Searchable } from '../../../common/protocol/searchable';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { Installable, Searchable, ArduinoComponent } from '../../../common/protocol';
|
||||
import { FilterableListContainer } from './filterable-list-container';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
import { NotificationCenter } from '../../notification-center';
|
||||
import { ResponseServiceImpl } from '../../response-service-impl';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget {
|
||||
@ -23,6 +23,9 @@ export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
@inject(ResponseServiceImpl)
|
||||
protected readonly responseService: ResponseServiceImpl;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@ -63,26 +66,31 @@ export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget
|
||||
return this.deferredContainer.promise;
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
protected onActivateRequest(message: Message): void {
|
||||
super.onActivateRequest(message);
|
||||
(this.focusNode || this.node).focus();
|
||||
}
|
||||
|
||||
protected onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
protected onUpdateRequest(message: Message): void {
|
||||
super.onUpdateRequest(message);
|
||||
this.render();
|
||||
}
|
||||
|
||||
protected onResize(message: Widget.ResizeMessage): void {
|
||||
super.onResize(message);
|
||||
this.updateScrollBar();
|
||||
}
|
||||
|
||||
protected onFocusResolved = (element: HTMLElement | undefined) => {
|
||||
this.focusNode = element;
|
||||
};
|
||||
|
||||
protected async install({ item, version }: { item: T, version: Installable.Version }): Promise<void> {
|
||||
return this.options.installable.install({ item, version });
|
||||
protected async install({ item, progressId, version }: { item: T, progressId: string, version: Installable.Version }): Promise<void> {
|
||||
return this.options.installable.install({ item, progressId, version });
|
||||
}
|
||||
|
||||
protected async uninstall({ item }: { item: T }): Promise<void> {
|
||||
return this.options.installable.uninstall({ item });
|
||||
protected async uninstall({ item, progressId }: { item: T, progressId: string }): Promise<void> {
|
||||
return this.options.installable.uninstall({ item, progressId });
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
@ -97,7 +105,8 @@ export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget
|
||||
itemRenderer={this.options.itemRenderer}
|
||||
filterTextChangeEvent={this.filterTextChangeEmitter.event}
|
||||
messageService={this.messageService}
|
||||
commandService={this.commandService} />;
|
||||
commandService={this.commandService}
|
||||
responseService={this.responseService} />;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { AbstractDialog } from '@theia/core/lib/browser';
|
||||
|
||||
export class InstallationProgressDialog extends AbstractDialog<undefined> {
|
||||
|
||||
readonly value = undefined;
|
||||
|
||||
constructor(componentName: string, version: string) {
|
||||
super({ title: 'Installation in progress' });
|
||||
this.contentNode.textContent = `Installing ${componentName} [${version}]. Please wait...`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UninstallationProgressDialog extends AbstractDialog<undefined> {
|
||||
|
||||
readonly value = undefined;
|
||||
|
||||
constructor(componentName: string) {
|
||||
super({ title: 'Uninstallation in progress' });
|
||||
this.contentNode.textContent = `Uninstalling ${componentName}. Please wait...`;
|
||||
}
|
||||
|
||||
}
|
@ -11,5 +11,5 @@ export * from './searchable';
|
||||
export * from './sketches-service';
|
||||
export * from './examples-service';
|
||||
export * from './executable-service';
|
||||
export * from './output-service';
|
||||
export * from './response-service';
|
||||
export * from './notification-service';
|
||||
|
@ -1,20 +1,26 @@
|
||||
import * as semver from 'semver';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
||||
import { naturalCompare } from './../utils';
|
||||
import { ArduinoComponent } from './arduino-component';
|
||||
import { MessageService } from '@theia/core';
|
||||
import { ResponseServiceImpl } from '../../browser/response-service-impl';
|
||||
|
||||
export interface Installable<T extends ArduinoComponent> {
|
||||
/**
|
||||
* If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`.
|
||||
*/
|
||||
install(options: { item: T, version?: Installable.Version }): Promise<void>;
|
||||
install(options: { item: T, progressId?: string, version?: Installable.Version }): Promise<void>;
|
||||
|
||||
/**
|
||||
* Uninstalls the given component. It is a NOOP if not installed.
|
||||
*/
|
||||
uninstall(options: { item: T }): Promise<void>;
|
||||
uninstall(options: { item: T, progressId?: string }): Promise<void>;
|
||||
}
|
||||
export namespace Installable {
|
||||
|
||||
export type Version = string;
|
||||
|
||||
export namespace Version {
|
||||
/**
|
||||
* Most recent version comes first, then the previous versions. (`1.8.1`, `1.6.3`, `1.6.2`, `1.6.1` and so on.)
|
||||
@ -26,4 +32,70 @@ export namespace Installable {
|
||||
return naturalCompare(left, right);
|
||||
};
|
||||
}
|
||||
|
||||
export async function installWithProgress<T extends ArduinoComponent>(options: {
|
||||
installable: Installable<T>,
|
||||
messageService: MessageService,
|
||||
responseService: ResponseServiceImpl,
|
||||
item: T,
|
||||
version: Installable.Version
|
||||
}): Promise<void> {
|
||||
|
||||
const { item, version } = options;
|
||||
return doWithProgress({
|
||||
...options,
|
||||
progressText: `Processing ${item.name}:${version}`,
|
||||
run: ({ progressId }) => options.installable.install({ item: options.item, version: options.version, progressId })
|
||||
});
|
||||
}
|
||||
|
||||
export async function uninstallWithProgress<T extends ArduinoComponent>(options: {
|
||||
installable: Installable<T>,
|
||||
messageService: MessageService,
|
||||
responseService: ResponseServiceImpl,
|
||||
item: T
|
||||
}): Promise<void> {
|
||||
|
||||
const { item } = options;
|
||||
return doWithProgress({
|
||||
...options,
|
||||
progressText: `Processing ${item.name}${item.installedVersion ? `:${item.installedVersion}` : ''}`,
|
||||
run: ({ progressId }) => options.installable.uninstall({ item: options.item, progressId })
|
||||
});
|
||||
}
|
||||
|
||||
export async function doWithProgress(options: {
|
||||
run: ({ progressId }: { progressId: string }) => Promise<void>,
|
||||
messageService: MessageService,
|
||||
responseService: ResponseServiceImpl,
|
||||
progressText: string
|
||||
}): Promise<void> {
|
||||
|
||||
return withProgress(options.progressText, options.messageService, async (progress, _) => {
|
||||
const progressId = progress.id;
|
||||
const toDispose = options.responseService.onProgressDidChange(progressMessage => {
|
||||
if (progressId === progressMessage.progressId) {
|
||||
const { message, work } = progressMessage;
|
||||
progress.report({ message, work });
|
||||
}
|
||||
});
|
||||
try {
|
||||
options.responseService.clearArduinoChannel();
|
||||
await options.run({ progressId });
|
||||
} finally {
|
||||
toDispose.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function withProgress(text: string, messageService: MessageService, cb: (progress: Progress, token: CancellationToken) => Promise<void>): Promise<void> {
|
||||
const cancellationSource = new CancellationTokenSource();
|
||||
const { token } = cancellationSource;
|
||||
const progress = await messageService.showProgress({ text, options: { cancelable: false } }, () => cancellationSource.cancel());
|
||||
try {
|
||||
await cb(progress, token);
|
||||
} finally {
|
||||
progress.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ export interface LibraryService extends Installable<LibraryPackage>, Searchable<
|
||||
/**
|
||||
* When `installDependencies` is not set, it is `true` by default. If you want to skip the installation of required dependencies, set it to `false`.
|
||||
*/
|
||||
install(options: { item: LibraryPackage, version?: Installable.Version, installDependencies?: boolean }): Promise<void>;
|
||||
installZip(options: { zipUri: string, overwrite?: boolean }): Promise<void>;
|
||||
install(options: { item: LibraryPackage, progressId?: string, version?: Installable.Version, installDependencies?: boolean }): Promise<void>;
|
||||
installZip(options: { zipUri: string, progressId?: string, overwrite?: boolean }): Promise<void>;
|
||||
/**
|
||||
* Set `filterSelf` to `true` if you want to avoid having `item` in the result set.
|
||||
* Note: as of today (22.02.2021), the CLI works like this: `./arduino-cli lib deps Adaino@0.1.0 ✕ Adaino 0.1.0 must be installed.`.
|
||||
|
@ -1,10 +0,0 @@
|
||||
export interface OutputMessage {
|
||||
readonly chunk: string;
|
||||
readonly severity?: 'error' | 'warning' | 'info'; // Currently not used!
|
||||
}
|
||||
|
||||
export const OutputServicePath = '/services/output-service';
|
||||
export const OutputService = Symbol('OutputService');
|
||||
export interface OutputService {
|
||||
append(message: OutputMessage): void;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
export interface OutputMessage {
|
||||
readonly chunk: string;
|
||||
readonly severity?: 'error' | 'warning' | 'info'; // Currently not used!
|
||||
}
|
||||
|
||||
export interface ProgressMessage {
|
||||
readonly progressId: string;
|
||||
readonly message: string;
|
||||
readonly work?: ProgressMessage.Work;
|
||||
}
|
||||
export namespace ProgressMessage {
|
||||
export interface Work {
|
||||
readonly done: number;
|
||||
readonly total: number;
|
||||
}
|
||||
}
|
||||
|
||||
export const ResponseServicePath = '/services/response-service';
|
||||
export const ResponseService = Symbol('ResponseService');
|
||||
export interface ResponseService {
|
||||
appendToOutput(message: OutputMessage): void;
|
||||
reportProgress(message: ProgressMessage): void;
|
||||
}
|
@ -29,7 +29,7 @@ import { ExamplesServiceImpl } from './examples-service-impl';
|
||||
import { ExamplesService, ExamplesServicePath } from '../common/protocol/examples-service';
|
||||
import { ExecutableService, ExecutableServicePath } from '../common/protocol/executable-service';
|
||||
import { ExecutableServiceImpl } from './executable-service-impl';
|
||||
import { OutputServicePath, OutputService } from '../common/protocol/output-service';
|
||||
import { ResponseServicePath, ResponseService } from '../common/protocol/response-service';
|
||||
import { NotificationServiceServerImpl } from './notification-service-server';
|
||||
import { NotificationServiceServer, NotificationServiceClient, NotificationServicePath } from '../common/protocol';
|
||||
import { BackendApplication } from './theia/core/backend-application';
|
||||
@ -127,7 +127,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
|
||||
// Output service per connection.
|
||||
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bindFrontendService }) => {
|
||||
bindFrontendService(OutputServicePath, OutputService);
|
||||
bindFrontendService(ResponseServicePath, ResponseService);
|
||||
}));
|
||||
|
||||
// Notify all connected frontend instances
|
||||
|
@ -4,17 +4,18 @@ import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import {
|
||||
BoardsService,
|
||||
Installable,
|
||||
BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, OutputService, NotificationServiceServer, AvailablePorts, BoardWithPackage
|
||||
BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, ResponseService, NotificationServiceServer, AvailablePorts, BoardWithPackage
|
||||
} from '../common/protocol';
|
||||
import {
|
||||
PlatformInstallRequest, PlatformInstallResponse, PlatformListRequest, PlatformListResponse, PlatformSearchRequest,
|
||||
PlatformSearchResponse, PlatformUninstallRequest, PlatformUninstallResponse
|
||||
PlatformInstallRequest, PlatformListRequest, PlatformListResponse, PlatformSearchRequest,
|
||||
PlatformSearchResponse, PlatformUninstallRequest
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/core_pb';
|
||||
import { Platform } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
import { BoardDiscovery } from './board-discovery';
|
||||
import { CoreClientAware } from './core-client-provider';
|
||||
import { BoardDetailsRequest, BoardDetailsResponse, BoardSearchRequest } from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
|
||||
import { ListProgrammersAvailableForUploadRequest, ListProgrammersAvailableForUploadResponse } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
||||
import { InstallWithProgress } from './grpc-installable';
|
||||
|
||||
@injectable()
|
||||
export class BoardsServiceImpl extends CoreClientAware implements BoardsService {
|
||||
@ -26,8 +27,8 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
@named('discovery')
|
||||
protected discoveryLogger: ILogger;
|
||||
|
||||
@inject(OutputService)
|
||||
protected readonly outputService: OutputService;
|
||||
@inject(ResponseService)
|
||||
protected readonly responseService: ResponseService;
|
||||
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly notificationService: NotificationServiceServer;
|
||||
@ -254,7 +255,7 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
return [...packages.values()];
|
||||
}
|
||||
|
||||
async install(options: { item: BoardsPackage, version?: Installable.Version }): Promise<void> {
|
||||
async install(options: { item: BoardsPackage, progressId?: string, version?: Installable.Version }): Promise<void> {
|
||||
const item = options.item;
|
||||
const version = !!options.version ? options.version : item.availableVersions[0];
|
||||
const coreClient = await this.coreClient();
|
||||
@ -270,17 +271,12 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
|
||||
console.info('>>> Starting boards package installation...', item);
|
||||
const resp = client.platformInstall(req);
|
||||
resp.on('data', (r: PlatformInstallResponse) => {
|
||||
const prog = r.getProgress();
|
||||
if (prog && prog.getFile()) {
|
||||
this.outputService.append({ chunk: `downloading ${prog.getFile()}\n` });
|
||||
}
|
||||
});
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService }));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', error => {
|
||||
this.outputService.append({ chunk: `Failed to install platform: ${item.id}.\n` });
|
||||
this.outputService.append({ chunk: error.toString() });
|
||||
this.responseService.appendToOutput({ chunk: `Failed to install platform: ${item.id}.\n` });
|
||||
this.responseService.appendToOutput({ chunk: error.toString() });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
@ -291,8 +287,8 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
console.info('<<< Boards package installation done.', item);
|
||||
}
|
||||
|
||||
async uninstall(options: { item: BoardsPackage }): Promise<void> {
|
||||
const item = options.item;
|
||||
async uninstall(options: { item: BoardsPackage, progressId?: string }): Promise<void> {
|
||||
const { item, progressId } = options;
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
@ -304,14 +300,8 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
req.setPlatformPackage(platform);
|
||||
|
||||
console.info('>>> Starting boards package uninstallation...', item);
|
||||
let logged = false;
|
||||
const resp = client.platformUninstall(req);
|
||||
resp.on('data', (_: PlatformUninstallResponse) => {
|
||||
if (!logged) {
|
||||
this.outputService.append({ chunk: `uninstalling ${item.id}\n` });
|
||||
logged = true;
|
||||
}
|
||||
})
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', reject);
|
||||
|
@ -57,9 +57,7 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
if (!instance) {
|
||||
throw new Error('Could not retrieve instance from the initialize response.');
|
||||
}
|
||||
|
||||
// No `await`. The index update event comes later. This way we do not block app startup with index update when invalid proxy is given.
|
||||
this.updateIndexes({ instance, client });
|
||||
await this.updateIndexes({ instance, client });
|
||||
|
||||
return { instance, client };
|
||||
}
|
||||
|
@ -2,22 +2,22 @@ import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { relative } from 'path';
|
||||
import * as jspb from 'google-protobuf';
|
||||
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
|
||||
import { ClientReadableStream } from '@grpc/grpc-js';
|
||||
import { CompilerWarnings, CoreService } from '../common/protocol/core-service';
|
||||
import { CompileRequest, CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
|
||||
import { CoreClientAware } from './core-client-provider';
|
||||
import { BurnBootloaderRequest, BurnBootloaderResponse, UploadRequest, UploadResponse, UploadUsingProgrammerRequest, UploadUsingProgrammerResponse } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
||||
import { OutputService } from '../common/protocol/output-service';
|
||||
import { ResponseService } from '../common/protocol/response-service';
|
||||
import { NotificationServiceServer } from '../common/protocol';
|
||||
import { ClientReadableStream } from '@grpc/grpc-js';
|
||||
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
|
||||
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
|
||||
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
|
||||
|
||||
@injectable()
|
||||
export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
|
||||
@inject(OutputService)
|
||||
protected readonly outputService: OutputService;
|
||||
@inject(ResponseService)
|
||||
protected readonly responseService: ResponseService;
|
||||
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly notificationService: NotificationServiceServer;
|
||||
@ -53,15 +53,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
result.on('data', (cr: CompileResponse) => {
|
||||
this.outputService.append({ chunk: Buffer.from(cr.getOutStream_asU8()).toString() });
|
||||
this.outputService.append({ chunk: Buffer.from(cr.getErrStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(cr.getOutStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(cr.getErrStream_asU8()).toString() });
|
||||
});
|
||||
result.on('error', error => reject(error));
|
||||
result.on('end', () => resolve());
|
||||
});
|
||||
this.outputService.append({ chunk: '\n--------------------------\nCompilation complete.\n' });
|
||||
this.responseService.appendToOutput({ chunk: '\n--------------------------\nCompilation complete.\n' });
|
||||
} catch (e) {
|
||||
this.outputService.append({ chunk: `Compilation error: ${e}\n`, severity: 'error' });
|
||||
this.responseService.appendToOutput({ chunk: `Compilation error: ${e}\n`, severity: 'error' });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@ -107,15 +107,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
result.on('data', (resp: UploadResponse) => {
|
||||
this.outputService.append({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
|
||||
this.outputService.append({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
|
||||
});
|
||||
result.on('error', error => reject(error));
|
||||
result.on('end', () => resolve());
|
||||
});
|
||||
this.outputService.append({ chunk: '\n--------------------------\n' + firstToLowerCase(task) + ' complete.\n' });
|
||||
this.responseService.appendToOutput({ chunk: '\n--------------------------\n' + firstToLowerCase(task) + ' complete.\n' });
|
||||
} catch (e) {
|
||||
this.outputService.append({ chunk: `${firstToUpperCase(task)} error: ${e}\n`, severity: 'error' });
|
||||
this.responseService.appendToOutput({ chunk: `${firstToUpperCase(task)} error: ${e}\n`, severity: 'error' });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@ -141,14 +141,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
result.on('data', (resp: BurnBootloaderResponse) => {
|
||||
this.outputService.append({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
|
||||
this.outputService.append({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
|
||||
});
|
||||
result.on('error', error => reject(error));
|
||||
result.on('end', () => resolve());
|
||||
});
|
||||
} catch (e) {
|
||||
this.outputService.append({ chunk: `Error while burning the bootloader: ${e}\n`, severity: 'error' });
|
||||
this.responseService.appendToOutput({ chunk: `Error while burning the bootloader: ${e}\n`, severity: 'error' });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export abstract class GrpcClientProvider<C> {
|
||||
const updateClient = () => {
|
||||
const cliConfig = this.configService.cliConfiguration;
|
||||
this.reconcileClient(cliConfig ? cliConfig.daemon.port : undefined);
|
||||
}
|
||||
};
|
||||
this.configService.onConfigChange(updateClient);
|
||||
this.daemon.ready.then(updateClient);
|
||||
this.daemon.onDaemonStopped(() => {
|
||||
@ -33,7 +33,7 @@ export abstract class GrpcClientProvider<C> {
|
||||
}
|
||||
this._client = undefined;
|
||||
this._port = undefined;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async client(): Promise<C | Error | undefined> {
|
||||
|
72
arduino-ide-extension/src/node/grpc-installable.ts
Normal file
72
arduino-ide-extension/src/node/grpc-installable.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { ProgressMessage, ResponseService } from '../common/protocol/response-service';
|
||||
import { DownloadProgress, TaskProgress } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
|
||||
export interface InstallResponse {
|
||||
getProgress?(): DownloadProgress | undefined;
|
||||
getTaskProgress(): TaskProgress | undefined;
|
||||
}
|
||||
|
||||
export namespace InstallWithProgress {
|
||||
|
||||
export interface Options {
|
||||
/**
|
||||
* _unknown_ progress if falsy.
|
||||
*/
|
||||
readonly progressId?: string;
|
||||
readonly responseService: ResponseService;
|
||||
}
|
||||
|
||||
export function createDataCallback({ responseService, progressId }: InstallWithProgress.Options): (response: InstallResponse) => void {
|
||||
let localFile = '';
|
||||
let localTotalSize = Number.NaN;
|
||||
return (response: InstallResponse) => {
|
||||
const download = response.getProgress ? response.getProgress() : undefined;
|
||||
const task = response.getTaskProgress();
|
||||
if (!download && !task) {
|
||||
throw new Error("Implementation error. Neither 'download' nor 'task' is available.");
|
||||
}
|
||||
if (task && download) {
|
||||
throw new Error("Implementation error. Both 'download' and 'task' are available.");
|
||||
}
|
||||
if (task) {
|
||||
const message = task.getName() || task.getMessage();
|
||||
if (message) {
|
||||
if (progressId) {
|
||||
responseService.reportProgress({ progressId, message, work: { done: Number.NaN, total: Number.NaN } });
|
||||
}
|
||||
responseService.appendToOutput({ chunk: `${message}\n` });
|
||||
}
|
||||
} else if (download) {
|
||||
if (download.getFile() && !localFile) {
|
||||
localFile = download.getFile();
|
||||
}
|
||||
if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) {
|
||||
localTotalSize = download.getTotalSize();
|
||||
}
|
||||
|
||||
// This happens only once per file download.
|
||||
if (download.getTotalSize() && localFile) {
|
||||
responseService.appendToOutput({ chunk: `${localFile}\n` });
|
||||
}
|
||||
|
||||
if (progressId && localFile) {
|
||||
let work: ProgressMessage.Work | undefined = undefined;
|
||||
if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) {
|
||||
work = { total: localTotalSize, done: download.getDownloaded() };
|
||||
}
|
||||
responseService.reportProgress({ progressId, message: `Downloading ${localFile}`, work });
|
||||
}
|
||||
if (download.getCompleted()) {
|
||||
// Discard local state.
|
||||
if (progressId && !Number.isNaN(localTotalSize)) {
|
||||
responseService.reportProgress({ progressId, message: '', work: { done: Number.NaN, total: Number.NaN } });
|
||||
}
|
||||
localFile = '';
|
||||
localTotalSize = Number.NaN;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,14 +2,15 @@ import { injectable, inject } from 'inversify';
|
||||
import { LibraryDependency, LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol/library-service';
|
||||
import { CoreClientAware } from './core-client-provider';
|
||||
import {
|
||||
InstalledLibrary, Library, LibraryInstallRequest, LibraryInstallResponse, LibraryListRequest, LibraryListResponse, LibraryLocation as GrpcLibraryLocation, LibraryRelease,
|
||||
LibraryResolveDependenciesRequest, LibraryUninstallRequest, LibraryUninstallResponse, ZipLibraryInstallRequest, ZipLibraryInstallResponse, LibrarySearchRequest,
|
||||
InstalledLibrary, Library, LibraryInstallRequest, LibraryListRequest, LibraryListResponse, LibraryLocation as GrpcLibraryLocation, LibraryRelease,
|
||||
LibraryResolveDependenciesRequest, LibraryUninstallRequest, ZipLibraryInstallRequest, LibrarySearchRequest,
|
||||
LibrarySearchResponse
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb';
|
||||
import { Installable } from '../common/protocol/installable';
|
||||
import { ILogger, notEmpty } from '@theia/core';
|
||||
import { FileUri } from '@theia/core/lib/node';
|
||||
import { OutputService, NotificationServiceServer } from '../common/protocol';
|
||||
import { ResponseService, NotificationServiceServer } from '../common/protocol';
|
||||
import { InstallWithProgress } from './grpc-installable';
|
||||
|
||||
@injectable()
|
||||
export class LibraryServiceImpl extends CoreClientAware implements LibraryService {
|
||||
@ -17,8 +18,8 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@inject(OutputService)
|
||||
protected readonly outputService: OutputService;
|
||||
@inject(ResponseService)
|
||||
protected readonly responseService: ResponseService;
|
||||
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly notificationServer: NotificationServiceServer;
|
||||
@ -157,7 +158,7 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
return filterSelf ? dependencies.filter(({ name }) => name !== item.name) : dependencies;
|
||||
}
|
||||
|
||||
async install(options: { item: LibraryPackage, version?: Installable.Version, installDependencies?: boolean }): Promise<void> {
|
||||
async install(options: { item: LibraryPackage, progressId?: string, version?: Installable.Version, installDependencies?: boolean }): Promise<void> {
|
||||
const item = options.item;
|
||||
const version = !!options.version ? options.version : item.availableVersions[0];
|
||||
const coreClient = await this.coreClient();
|
||||
@ -173,17 +174,12 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
|
||||
console.info('>>> Starting library package installation...', item);
|
||||
const resp = client.libraryInstall(req);
|
||||
resp.on('data', (r: LibraryInstallResponse) => {
|
||||
const prog = r.getProgress();
|
||||
if (prog) {
|
||||
this.outputService.append({ chunk: `downloading ${prog.getFile()}: ${prog.getCompleted()}%\n` });
|
||||
}
|
||||
});
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService }));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', error => {
|
||||
this.outputService.append({ chunk: `Failed to install library: ${item.name}${version ? `:${version}` : ''}.\n` });
|
||||
this.outputService.append({ chunk: error.toString() });
|
||||
this.responseService.appendToOutput({ chunk: `Failed to install library: ${item.name}${version ? `:${version}` : ''}.\n` });
|
||||
this.responseService.appendToOutput({ chunk: error.toString() });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
@ -194,7 +190,7 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
console.info('<<< Library package installation done.', item);
|
||||
}
|
||||
|
||||
async installZip({ zipUri, overwrite }: { zipUri: string, overwrite?: boolean }): Promise<void> {
|
||||
async installZip({ zipUri, progressId, overwrite }: { zipUri: string, progressId?: string, overwrite?: boolean }): Promise<void> {
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
const req = new ZipLibraryInstallRequest();
|
||||
@ -204,20 +200,15 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
req.setOverwrite(overwrite);
|
||||
}
|
||||
const resp = client.zipLibraryInstall(req);
|
||||
resp.on('data', (r: ZipLibraryInstallResponse) => {
|
||||
const task = r.getTaskProgress();
|
||||
if (task && task.getMessage()) {
|
||||
this.outputService.append({ chunk: task.getMessage() });
|
||||
}
|
||||
});
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async uninstall(options: { item: LibraryPackage }): Promise<void> {
|
||||
const item = options.item;
|
||||
async uninstall(options: { item: LibraryPackage, progressId?: string }): Promise<void> {
|
||||
const { item, progressId } = options;
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
@ -227,14 +218,8 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
req.setVersion(item.installedVersion!);
|
||||
|
||||
console.info('>>> Starting library package uninstallation...', item);
|
||||
let logged = false;
|
||||
const resp = client.libraryUninstall(req);
|
||||
resp.on('data', (_: LibraryUninstallResponse) => {
|
||||
if (!logged) {
|
||||
this.outputService.append({ chunk: `uninstalling ${item.name}:${item.installedVersion}%\n` });
|
||||
logged = true;
|
||||
}
|
||||
});
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', reject);
|
||||
|
Loading…
x
Reference in New Issue
Block a user