Improved initial board installation experience

This commit is contained in:
Christian Weichel 2019-05-08 15:47:04 +02:00
parent 43ab17fd34
commit 6584b0d5b1
8 changed files with 153 additions and 24 deletions

View File

@ -14,6 +14,8 @@ import { WorkspaceServiceExt } from './workspace-service-ext';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { ConfirmDialog } from '@theia/core/lib/browser';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
import { BoardsNotificationService } from './boards-notification-service';
@injectable()
@ -37,6 +39,12 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
@inject(QuickPickService)
protected readonly quickPickService: QuickPickService;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
@postConstruct()
protected async init(): Promise<void> {
// This is a hack. Otherwise, the backend services won't bind.
@ -60,7 +68,12 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
});
registry.registerItem({
id: ConnectedBoards.TOOLBAR_ID,
render: () => <ConnectedBoards boardsService={this.boardService} quickPickService={this.quickPickService} />,
render: () => <ConnectedBoards
boardsService={this.boardService}
boardsNotificationService={this.boardsNotificationService}
quickPickService={this.quickPickService}
onNoBoardsInstalled={this.onNoBoardsInstalled.bind(this)}
onUnknownBoard={this.onUnknownBoard.bind(this)} />,
isVisible: widget => this.isArduinoEditor(widget)
})
}
@ -103,6 +116,25 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
});
}
private async onNoBoardsInstalled() {
const action = await this.messageService.info("You have no boards installed. Use the boards mangager to install one.", "Open Boards Manager");
if (!action) {
return;
}
this.boardsListWidgetFrontendContribution.openView({reveal: true});
}
private async onUnknownBoard() {
const action = await this.messageService.warn("There's a board connected for which you need to install software." +
" If this were not a PoC we would offer you the right package now.", "Open Boards Manager");
if (!action) {
return;
}
this.boardsListWidgetFrontendContribution.openView({reveal: true});
}
private isArduinoEditor(maybeEditorWidget: any): boolean {
if (maybeEditorWidget instanceof EditorWidget) {
return maybeEditorWidget.editor.uri.toString().endsWith('.ino');

View File

@ -22,6 +22,7 @@ import { ToolOutputServiceClient } from '../common/protocol/tool-output-service'
import '../../src/browser/style/index.css';
import { ToolOutputService } from '../common/protocol/tool-output-service';
import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl';
import { BoardsNotificationService } from './boards-notification-service';
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
// Commands and toolbar items
@ -44,6 +45,10 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
}));
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
// Boards Notification service for updating boards list
// TODO (post-PoC): move this to boards service/backend
bind(BoardsNotificationService).toSelf().inSingletonScope();
// Boards service
bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope();

View File

@ -0,0 +1,19 @@
import { EventEmitter } from "events";
import { injectable } from "inversify";
// TODO (post-PoC): move this to the backend / BoardsService
@injectable()
export class BoardsNotificationService {
protected readonly emitter = new EventEmitter();
public on(event: 'boards-installed', listener: (...args: any[]) => void): this {
this.emitter.on(event, listener);
return this;
}
public notifyBoardsInstalled() {
this.emitter.emit('boards-installed');
}
}

View File

@ -4,7 +4,8 @@ import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsService, Board, BoardPackage } from '../../common/protocol/boards-service';
import { BoardsNotificationService } from '../boards-notification-service';
@injectable()
export abstract class ListWidget extends ReactWidget {
@ -15,6 +16,9 @@ export abstract class ListWidget extends ReactWidget {
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
constructor() {
super();
const { id, title, iconClass } = this.widgetProps();
@ -46,8 +50,19 @@ export abstract class ListWidget extends ReactWidget {
}
render(): React.ReactNode {
const boardsServiceDelegate = this.boardsService;
const boardsService: BoardsService = {
getAttachedBoards: () => boardsServiceDelegate.getAttachedBoards(),
selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board),
getSelectBoard: () => boardsServiceDelegate.getSelectBoard(),
search: (options: { query?: string }) => boardsServiceDelegate.search(options),
install: async (item: BoardPackage) => {
await boardsServiceDelegate.install(item);
this.boardsNotificationService.notifyBoardsInstalled();
}
}
return <FilterableListContainer
service={this.boardsService}
service={boardsService}
windowService={this.windowService}
/>;
}

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { BoardsService, Board } from '../../common/protocol/boards-service';
// import { SelectBoardDialog } from './select-board-dialog';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { BoardsNotificationService } from '../boards-notification-service';
export class ConnectedBoards extends React.Component<ConnectedBoards.Props, ConnectedBoards.State> {
static TOOLBAR_ID: 'connected-boards-toolbar';
@ -9,6 +10,8 @@ export class ConnectedBoards extends React.Component<ConnectedBoards.Props, Conn
constructor(props: ConnectedBoards.Props) {
super(props);
this.state = { boardsLoading: false };
props.boardsNotificationService.on('boards-installed', () => this.onBoardsInstalled());
}
render(): React.ReactNode {
@ -44,14 +47,34 @@ export class ConnectedBoards extends React.Component<ConnectedBoards.Props, Conn
this.reloadBoards();
}
protected onBoardsInstalled() {
if (!!this.findUnknownBoards()) {
this.reloadBoards();
}
}
protected findUnknownBoards(): Board[] {
if (!this.state || !this.state.boards) {
return [];
}
return this.state.boards.filter(b => !b.fqbn || b.name === "unknown");
}
protected async reloadBoards() {
this.setState({ boardsLoading: true, boards: undefined, selection: undefined });
const prevSelection = this.state.selection;
this.setState({ boardsLoading: true, boards: undefined, selection: "loading" });
const { boards } = await this.props.boardsService.getAttachedBoards()
this.setState({ boards, boardsLoading: false });
this.setState({ boards, boardsLoading: false, selection: prevSelection });
if (boards) {
this.setState({ selection: "0" });
await this.props.boardsService.selectBoard(boards[0]);
const unknownBoards = this.findUnknownBoards();
if (unknownBoards && unknownBoards.length > 0) {
this.props.onUnknownBoard(unknownBoards[0]);
}
}
}
@ -85,6 +108,11 @@ export class ConnectedBoards extends React.Component<ConnectedBoards.Props, Conn
const idx = new Map<string, Board>();
items.filter(pkg => !!pkg.installedVersion).forEach(pkg => pkg.boards.forEach(brd => idx.set(`${brd.name}`, brd) ));
if (idx.size === 0) {
this.props.onNoBoardsInstalled();
return;
}
const selection = await this.props.quickPickService.show(Array.from(idx.keys()));
if (!selection) {
return;
@ -99,7 +127,10 @@ export namespace ConnectedBoards {
export interface Props {
readonly boardsService: BoardsService;
readonly boardsNotificationService: BoardsNotificationService;
readonly quickPickService: QuickPickService;
readonly onNoBoardsInstalled: () => void;
readonly onUnknownBoard: (board: Board) => void;
}
export interface State {

View File

@ -3,6 +3,7 @@ import { BoardsService, AttachedSerialBoard, AttachedNetworkBoard, BoardPackage,
import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp } from './cli-protocol/core_pb';
import { CoreClientProvider } from './core-client-provider';
import { BoardListReq, BoardListResp } from './cli-protocol/board_pb';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
@injectable()
export class BoardsServiceImpl implements BoardsService {
@ -10,6 +11,9 @@ export class BoardsServiceImpl implements BoardsService {
@inject(CoreClientProvider)
protected readonly coreClientProvider: CoreClientProvider;
@inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer;
protected selectedBoard: Board | undefined;
public async getAttachedBoards(): Promise<{ boards: Board[] }> {
@ -100,8 +104,8 @@ export class BoardsServiceImpl implements BoardsService {
const resp = client.platformInstall(req);
resp.on('data', (r: PlatformInstallResp) => {
const prog = r.getProgress();
if (prog) {
console.info(`downloading ${prog.getFile()}: ${prog.getCompleted()}%`)
if (prog && prog.getFile()) {
this.toolOutputService.publishNewOutput("board download", `downloading ${prog.getFile()}\n`)
}
});
await new Promise<void>((resolve, reject) => {

View File

@ -7,6 +7,8 @@ import { FileSystem } from '@theia/filesystem/lib/common';
import URI from '@theia/core/lib/common/uri';
import { CoreClientProvider, Client } from './core-client-provider';
import * as PQueue from 'p-queue';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { Instance } from './cli-protocol/common_pb';
@injectable()
export class CoreClientProviderImpl implements CoreClientProvider {
@ -19,6 +21,9 @@ export class CoreClientProviderImpl implements CoreClientProvider {
@inject(WorkspaceServiceExt)
protected readonly workspaceServiceExt: WorkspaceServiceExt;
@inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer;
protected clients = new Map<string, Client>();
async getClient(workspaceRootOrResourceUri?: string): Promise<Client> {
@ -69,24 +74,20 @@ export class CoreClientProviderImpl implements CoreClientProvider {
throw new Error(`Could not retrieve instance from the initialize response.`);
}
// workaround to speed up startup on existing workspaces
const updateReq = new UpdateIndexReq();
updateReq.setInstance(instance);
const updateResp = client.updateIndex(updateReq);
updateResp.on('data', (o: UpdateIndexResp) => {
const progress = o.getDownloadProgress();
if (progress) {
if (progress.getCompleted()) {
console.log(`Download${progress.getFile() ? ` of ${progress.getFile()}` : ''} completed.`);
} else {
console.log(`Downloading${progress.getFile() ? ` ${progress.getFile()}:` : ''} ${progress.getDownloaded()}.`);
}
// in a seperate promise, try and update the index
let succeeded = true;
for (let i = 0; i < 10; i++) {
try {
await this.updateIndex(client, instance);
succeeded = true;
break;
} catch (e) {
this.toolOutputService.publishNewOutput("daemon", `Error while updating index in attempt ${i}: ${e}`);
}
});
await new Promise<void>((resolve, reject) => {
updateResp.on('error', reject);
updateResp.on('end', resolve);
});
}
if (!succeeded) {
this.toolOutputService.publishNewOutput("daemon", `Was unable to update the index. Please restart to try again.`);
}
const result = {
client,
@ -96,4 +97,23 @@ export class CoreClientProviderImpl implements CoreClientProvider {
console.info(` <<< New client has been successfully created and cached for ${rootUri}.`);
return result;
}
protected async updateIndex(client: ArduinoCoreClient, instance: Instance): Promise<void> {
const updateReq = new UpdateIndexReq();
updateReq.setInstance(instance);
const updateResp = client.updateIndex(updateReq);
updateResp.on('data', (o: UpdateIndexResp) => {
const progress = o.getDownloadProgress();
if (progress) {
if (progress.getCompleted()) {
this.toolOutputService.publishNewOutput("daemon", `Download${progress.getFile() ? ` of ${progress.getFile()}` : ''} completed.\n`);
}
}
});
await new Promise<void>((resolve, reject) => {
updateResp.on('error', reject);
updateResp.on('end', resolve);
});
}
}

3
known-issues.txt Normal file
View File

@ -0,0 +1,3 @@
Known issues:
- arduino-cli does not get stopped reliably upon app shutdown
- startup time is not as fast we'd like to have it