mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-17 00:06: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 { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
|
||||||
import { ConfirmDialog } from '@theia/core/lib/browser';
|
import { ConfirmDialog } from '@theia/core/lib/browser';
|
||||||
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
|
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()
|
@injectable()
|
||||||
@ -37,6 +39,12 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
|
|||||||
@inject(QuickPickService)
|
@inject(QuickPickService)
|
||||||
protected readonly quickPickService: QuickPickService;
|
protected readonly quickPickService: QuickPickService;
|
||||||
|
|
||||||
|
@inject(BoardsListWidgetFrontendContribution)
|
||||||
|
protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||||
|
|
||||||
|
@inject(BoardsNotificationService)
|
||||||
|
protected readonly boardsNotificationService: BoardsNotificationService;
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
protected async init(): Promise<void> {
|
protected async init(): Promise<void> {
|
||||||
// This is a hack. Otherwise, the backend services won't bind.
|
// This is a hack. Otherwise, the backend services won't bind.
|
||||||
@ -60,7 +68,12 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
|
|||||||
});
|
});
|
||||||
registry.registerItem({
|
registry.registerItem({
|
||||||
id: ConnectedBoards.TOOLBAR_ID,
|
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)
|
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 {
|
private isArduinoEditor(maybeEditorWidget: any): boolean {
|
||||||
if (maybeEditorWidget instanceof EditorWidget) {
|
if (maybeEditorWidget instanceof EditorWidget) {
|
||||||
return maybeEditorWidget.editor.uri.toString().endsWith('.ino');
|
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 '../../src/browser/style/index.css';
|
||||||
import { ToolOutputService } from '../common/protocol/tool-output-service';
|
import { ToolOutputService } from '../common/protocol/tool-output-service';
|
||||||
import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl';
|
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) => {
|
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
|
||||||
// Commands and toolbar items
|
// Commands and toolbar items
|
||||||
@ -44,6 +45,10 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
|||||||
}));
|
}));
|
||||||
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
|
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
|
// Boards service
|
||||||
bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope();
|
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 { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||||
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
|
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()
|
@injectable()
|
||||||
export abstract class ListWidget extends ReactWidget {
|
export abstract class ListWidget extends ReactWidget {
|
||||||
@ -15,6 +16,9 @@ export abstract class ListWidget extends ReactWidget {
|
|||||||
@inject(WindowService)
|
@inject(WindowService)
|
||||||
protected readonly windowService: WindowService;
|
protected readonly windowService: WindowService;
|
||||||
|
|
||||||
|
@inject(BoardsNotificationService)
|
||||||
|
protected readonly boardsNotificationService: BoardsNotificationService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const { id, title, iconClass } = this.widgetProps();
|
const { id, title, iconClass } = this.widgetProps();
|
||||||
@ -46,8 +50,19 @@ export abstract class ListWidget extends ReactWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(): React.ReactNode {
|
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
|
return <FilterableListContainer
|
||||||
service={this.boardsService}
|
service={boardsService}
|
||||||
windowService={this.windowService}
|
windowService={this.windowService}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||||||
import { BoardsService, Board } from '../../common/protocol/boards-service';
|
import { BoardsService, Board } from '../../common/protocol/boards-service';
|
||||||
// import { SelectBoardDialog } from './select-board-dialog';
|
// import { SelectBoardDialog } from './select-board-dialog';
|
||||||
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
|
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> {
|
export class ConnectedBoards extends React.Component<ConnectedBoards.Props, ConnectedBoards.State> {
|
||||||
static TOOLBAR_ID: 'connected-boards-toolbar';
|
static TOOLBAR_ID: 'connected-boards-toolbar';
|
||||||
@ -9,6 +10,8 @@ export class ConnectedBoards extends React.Component<ConnectedBoards.Props, Conn
|
|||||||
constructor(props: ConnectedBoards.Props) {
|
constructor(props: ConnectedBoards.Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { boardsLoading: false };
|
this.state = { boardsLoading: false };
|
||||||
|
|
||||||
|
props.boardsNotificationService.on('boards-installed', () => this.onBoardsInstalled());
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): React.ReactNode {
|
render(): React.ReactNode {
|
||||||
@ -44,14 +47,34 @@ export class ConnectedBoards extends React.Component<ConnectedBoards.Props, Conn
|
|||||||
this.reloadBoards();
|
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() {
|
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()
|
const { boards } = await this.props.boardsService.getAttachedBoards()
|
||||||
this.setState({ boards, boardsLoading: false });
|
this.setState({ boards, boardsLoading: false, selection: prevSelection });
|
||||||
|
|
||||||
if (boards) {
|
if (boards) {
|
||||||
this.setState({ selection: "0" });
|
this.setState({ selection: "0" });
|
||||||
await this.props.boardsService.selectBoard(boards[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>();
|
const idx = new Map<string, Board>();
|
||||||
items.filter(pkg => !!pkg.installedVersion).forEach(pkg => pkg.boards.forEach(brd => idx.set(`${brd.name}`, brd) ));
|
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()));
|
const selection = await this.props.quickPickService.show(Array.from(idx.keys()));
|
||||||
if (!selection) {
|
if (!selection) {
|
||||||
return;
|
return;
|
||||||
@ -99,7 +127,10 @@ export namespace ConnectedBoards {
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly boardsService: BoardsService;
|
readonly boardsService: BoardsService;
|
||||||
|
readonly boardsNotificationService: BoardsNotificationService;
|
||||||
readonly quickPickService: QuickPickService;
|
readonly quickPickService: QuickPickService;
|
||||||
|
readonly onNoBoardsInstalled: () => void;
|
||||||
|
readonly onUnknownBoard: (board: Board) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
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 { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp } from './cli-protocol/core_pb';
|
||||||
import { CoreClientProvider } from './core-client-provider';
|
import { CoreClientProvider } from './core-client-provider';
|
||||||
import { BoardListReq, BoardListResp } from './cli-protocol/board_pb';
|
import { BoardListReq, BoardListResp } from './cli-protocol/board_pb';
|
||||||
|
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsServiceImpl implements BoardsService {
|
export class BoardsServiceImpl implements BoardsService {
|
||||||
@ -10,6 +11,9 @@ export class BoardsServiceImpl implements BoardsService {
|
|||||||
@inject(CoreClientProvider)
|
@inject(CoreClientProvider)
|
||||||
protected readonly coreClientProvider: CoreClientProvider;
|
protected readonly coreClientProvider: CoreClientProvider;
|
||||||
|
|
||||||
|
@inject(ToolOutputServiceServer)
|
||||||
|
protected readonly toolOutputService: ToolOutputServiceServer;
|
||||||
|
|
||||||
protected selectedBoard: Board | undefined;
|
protected selectedBoard: Board | undefined;
|
||||||
|
|
||||||
public async getAttachedBoards(): Promise<{ boards: Board[] }> {
|
public async getAttachedBoards(): Promise<{ boards: Board[] }> {
|
||||||
@ -100,8 +104,8 @@ export class BoardsServiceImpl implements BoardsService {
|
|||||||
const resp = client.platformInstall(req);
|
const resp = client.platformInstall(req);
|
||||||
resp.on('data', (r: PlatformInstallResp) => {
|
resp.on('data', (r: PlatformInstallResp) => {
|
||||||
const prog = r.getProgress();
|
const prog = r.getProgress();
|
||||||
if (prog) {
|
if (prog && prog.getFile()) {
|
||||||
console.info(`downloading ${prog.getFile()}: ${prog.getCompleted()}%`)
|
this.toolOutputService.publishNewOutput("board download", `downloading ${prog.getFile()}\n`)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await new Promise<void>((resolve, reject) => {
|
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 URI from '@theia/core/lib/common/uri';
|
||||||
import { CoreClientProvider, Client } from './core-client-provider';
|
import { CoreClientProvider, Client } from './core-client-provider';
|
||||||
import * as PQueue from 'p-queue';
|
import * as PQueue from 'p-queue';
|
||||||
|
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
|
||||||
|
import { Instance } from './cli-protocol/common_pb';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CoreClientProviderImpl implements CoreClientProvider {
|
export class CoreClientProviderImpl implements CoreClientProvider {
|
||||||
@ -19,6 +21,9 @@ export class CoreClientProviderImpl implements CoreClientProvider {
|
|||||||
@inject(WorkspaceServiceExt)
|
@inject(WorkspaceServiceExt)
|
||||||
protected readonly workspaceServiceExt: WorkspaceServiceExt;
|
protected readonly workspaceServiceExt: WorkspaceServiceExt;
|
||||||
|
|
||||||
|
@inject(ToolOutputServiceServer)
|
||||||
|
protected readonly toolOutputService: ToolOutputServiceServer;
|
||||||
|
|
||||||
protected clients = new Map<string, Client>();
|
protected clients = new Map<string, Client>();
|
||||||
|
|
||||||
async getClient(workspaceRootOrResourceUri?: string): Promise<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.`);
|
throw new Error(`Could not retrieve instance from the initialize response.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// workaround to speed up startup on existing workspaces
|
// in a seperate promise, try and update the index
|
||||||
const updateReq = new UpdateIndexReq();
|
let succeeded = true;
|
||||||
updateReq.setInstance(instance);
|
for (let i = 0; i < 10; i++) {
|
||||||
const updateResp = client.updateIndex(updateReq);
|
try {
|
||||||
updateResp.on('data', (o: UpdateIndexResp) => {
|
await this.updateIndex(client, instance);
|
||||||
const progress = o.getDownloadProgress();
|
succeeded = true;
|
||||||
if (progress) {
|
break;
|
||||||
if (progress.getCompleted()) {
|
} catch (e) {
|
||||||
console.log(`Download${progress.getFile() ? ` of ${progress.getFile()}` : ''} completed.`);
|
this.toolOutputService.publishNewOutput("daemon", `Error while updating index in attempt ${i}: ${e}`);
|
||||||
} else {
|
|
||||||
console.log(`Downloading${progress.getFile() ? ` ${progress.getFile()}:` : ''} ${progress.getDownloaded()}.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
await new Promise<void>((resolve, reject) => {
|
if (!succeeded) {
|
||||||
updateResp.on('error', reject);
|
this.toolOutputService.publishNewOutput("daemon", `Was unable to update the index. Please restart to try again.`);
|
||||||
updateResp.on('end', resolve);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
client,
|
client,
|
||||||
@ -96,4 +97,23 @@ export class CoreClientProviderImpl implements CoreClientProvider {
|
|||||||
console.info(` <<< New client has been successfully created and cached for ${rootUri}.`);
|
console.info(` <<< New client has been successfully created and cached for ${rootUri}.`);
|
||||||
return result;
|
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