mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-15 23:36:33 +00:00
Improved initial board installation experience
This commit is contained in:
parent
43ab17fd34
commit
6584b0d5b1
@ -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');
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
@ -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}
|
||||
/>;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) => {
|
||||
|
@ -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
3
known-issues.txt
Normal 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
|
Loading…
x
Reference in New Issue
Block a user