PROEDITOR-46: Added a core auto-installer.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2019-10-16 11:00:44 +02:00
parent fb6785c5d3
commit de1caf1451
5 changed files with 93 additions and 3 deletions

View File

@ -67,6 +67,7 @@ import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-de
import { ArduinoTabBarDecoratorService } from './shell/arduino-tab-bar-decorator';
import { ProblemManager } from '@theia/markers/lib/browser';
import { ArduinoProblemManager } from './markers/arduino-problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
const ElementQueries = require('css-element-queries/src/ElementQueries');
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
@ -120,6 +121,10 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
return client;
}).inSingletonScope();
// boards auto-installer
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);
// Boards list widget
bind(BoardsListWidget).toSelf();
bindViewContribution(bind, BoardsListWidgetFrontendContribution);

View File

@ -0,0 +1,63 @@
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, Board } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { InstallationProgressDialog } from '../components/installation-progress-dialog';
import { BoardsConfig } from './boards-config';
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
* have the corresponding core installed, it proposes the user to install the core.
*/
@injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this));
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
if (selectedBoard) {
this.boardsService.search({}).then(({ items }) => {
const candidates = items
.filter(item => item.boards.some(board => Board.sameAs(board, selectedBoard)))
.filter(({ installable, installedVersion }) => installable && !installedVersion);
for (const candidate of candidates) {
// 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?`, 'Yes', 'Install Manually').then(async answer => {
if (answer === 'Yes') {
const dialog = new InstallationProgressDialog(candidate.name);
dialog.open();
try {
await this.boardsService.install(candidate);
} finally {
dialog.close();
}
}
if (answer) {
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
}
});
}
})
}
}
}

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import debounce = require('lodash.debounce');
import { Event } from '@theia/core/lib/common/event';
import { Searchable } from '../../../common/protocol/searchable';
import { Installable } from '../../../common/protocol/installable';
import { InstallationProgressDialog } from '../installation-progress-dialog';
@ -20,6 +21,7 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
componentWillMount(): void {
this.search = debounce(this.search, 500);
this.handleFilterTextChange('');
this.props.filterTextChangeEvent(this.handleFilterTextChange.bind(this));
}
render(): React.ReactNode {
@ -57,8 +59,8 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
this.setState({ filterText });
this.search(filterText);
}
protected search (query: string): void {
protected search(query: string): void {
const { searchable } = this.props;
searchable.search({ query: query.trim() }).then(result => {
const { items } = result;
@ -97,6 +99,7 @@ export namespace FilterableListContainer {
readonly itemRenderer: ListItemRenderer<T>;
readonly resolveContainer: (element: HTMLElement) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
readonly filterTextChangeEvent: Event<string>;
}
export interface State<T> {

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Emitter } from '@theia/core/lib/common/event';
import { MaybePromise } from '@theia/core/lib/common/types';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { Installable } from '../../../common/protocol/installable';
@ -17,6 +18,7 @@ export abstract class ListWidget<T> extends ReactWidget {
*/
protected focusNode: HTMLElement | undefined;
protected readonly deferredContainer = new Deferred<HTMLElement>();
protected readonly filterTextChangeEmitter = new Emitter<string>();
constructor(protected options: ListWidget.Options<T>) {
super();
@ -31,6 +33,7 @@ export abstract class ListWidget<T> extends ReactWidget {
this.scrollOptions = {
suppressScrollX: true
}
this.toDispose.push(this.filterTextChangeEmitter);
}
@postConstruct()
@ -63,7 +66,12 @@ export abstract class ListWidget<T> extends ReactWidget {
searchable={this.options.searchable}
installable={this.options.installable}
itemLabel={this.options.itemLabel}
itemRenderer={this.options.itemRenderer} />;
itemRenderer={this.options.itemRenderer}
filterTextChangeEvent={this.filterTextChangeEmitter.event}/>;
}
refresh(filterText: string): void {
this.deferredContainer.promise.then(() => this.filterTextChangeEmitter.fire(filterText));
}
}

View File

@ -59,6 +59,17 @@ export namespace Board {
return left.name === right.name && left.fqbn === right.fqbn;
}
export function sameAs(left: Board, right: string | Board): boolean {
// How to associate a selected board with one of the available cores: https://typefox.slack.com/archives/CJJHJCJSJ/p1571142327059200
// 1. How to use the FQBN if any and infer the package ID from it: https://typefox.slack.com/archives/CJJHJCJSJ/p1571147549069100
// 2. How to trim the `/Genuino` from the name: https://arduino.slack.com/archives/CJJHJCJSJ/p1571146951066800?thread_ts=1571142327.059200&cid=CJJHJCJSJ
const other = typeof right === 'string' ? { name: right } : right;
if (left.fqbn && other.fqbn) {
return left.fqbn === other.fqbn;
}
return left.name.replace('/Genuino', '') === other.name.replace('/Genuino', '');
}
export function compare(left: Board, right: Board): number {
let result = left.name.localeCompare(right.name);
if (result === 0) {