diff --git a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts index 62eaa6f1..2f6ff16f 100644 --- a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts +++ b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts @@ -4,7 +4,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten 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 { InstallationProgressDialog } from '../components/progress-dialog'; import { BoardsConfig } from './boards-config'; @@ -60,4 +60,4 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { } } -} \ No newline at end of file +} diff --git a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts index e2ac6a80..d88d284e 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts @@ -3,7 +3,7 @@ import { Emitter } from '@theia/core/lib/common/event'; import { ILogger } from '@theia/core/lib/common/logger'; import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; import { RecursiveRequired } from '../../common/types'; -import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port } from '../../common/protocol/boards-service'; +import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service'; import { BoardsConfig } from './boards-config'; @injectable() @@ -16,6 +16,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient { protected storageService: LocalStorageService; protected readonly onBoardInstalledEmitter = new Emitter(); + protected readonly onBoardUninstalledEmitter = new Emitter(); protected readonly onAttachedBoardsChangedEmitter = new Emitter(); protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter(); @@ -31,6 +32,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient { readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event; readonly onBoardInstalled = this.onBoardInstalledEmitter.event; + readonly onBoardUninstalled = this.onBoardUninstalledEmitter.event; readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event; @postConstruct() @@ -87,6 +89,11 @@ export class BoardsServiceClientImpl implements BoardsServiceClient { this.onBoardInstalledEmitter.fire(event); } + notifyBoardUninstalled(event: BoardUninstalledEvent): void { + this.logger.info('Board uninstalled: ', JSON.stringify(event)); + this.onBoardUninstalledEmitter.fire(event); + } + set boardsConfig(config: BoardsConfig.Config) { this.logger.info('Board config changed: ', JSON.stringify(config)); this._boardsConfig = config; diff --git a/arduino-ide-extension/src/browser/components/component-list/component-list-item.tsx b/arduino-ide-extension/src/browser/components/component-list/component-list-item.tsx index a9bd967c..8944455c 100644 --- a/arduino-ide-extension/src/browser/components/component-list/component-list-item.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/component-list-item.tsx @@ -19,13 +19,22 @@ export class ComponentListItem extends React.Compone await this.props.install(item, this.state.selectedVersion); } + protected async uninstall(item: T): Promise { + await this.props.uninstall(item); + } + protected onVersionChange(version: Installable.Version) { this.setState({ selectedVersion: version }); } render(): React.ReactNode { const { item, itemRenderer } = this.props; - return itemRenderer.renderItem(Object.assign(this.state, { item }), this.install.bind(this), this.onVersionChange.bind(this)); + return itemRenderer.renderItem( + Object.assign(this.state, { item }), + this.install.bind(this), + this.uninstall.bind(this), + this.onVersionChange.bind(this) + ); } } @@ -35,6 +44,7 @@ export namespace ComponentListItem { export interface Props { readonly item: T; readonly install: (item: T, version?: Installable.Version) => Promise; + readonly uninstall: (item: T) => Promise; readonly itemRenderer: ListItemRenderer; } diff --git a/arduino-ide-extension/src/browser/components/component-list/component-list.tsx b/arduino-ide-extension/src/browser/components/component-list/component-list.tsx index d2ea87d5..8c40e8d3 100644 --- a/arduino-ide-extension/src/browser/components/component-list/component-list.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/component-list.tsx @@ -31,7 +31,8 @@ export class ComponentList extends React.Component + install={this.props.install} + uninstall={this.props.uninstall} /> } } @@ -43,6 +44,7 @@ export namespace ComponentList { readonly itemLabel: (item: T) => string; readonly itemRenderer: ListItemRenderer; readonly install: (item: T, version?: Installable.Version) => Promise; + readonly uninstall: (item: T) => Promise; readonly resolveContainer: (element: HTMLElement) => void; } diff --git a/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx index 8493de1e..4c717149 100644 --- a/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import debounce = require('lodash.debounce'); import { Event } from '@theia/core/lib/common/event'; +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 } from '../installation-progress-dialog'; +import { InstallationProgressDialog, UninstallationProgressDialog } from '../progress-dialog'; import { SearchBar } from './search-bar'; import { ListWidget } from './list-widget'; import { ComponentList } from './component-list'; @@ -59,6 +60,7 @@ export class FilterableListContainer extends React.C itemLabel={itemLabel} itemRenderer={itemRenderer} install={this.install.bind(this)} + uninstall={this.uninstall.bind(this)} resolveContainer={resolveContainer} /> } @@ -96,6 +98,28 @@ export class FilterableListContainer extends React.C } } + protected async uninstall(item: T): Promise { + const uninstall = await new ConfirmDialog({ + title: 'Uninstall', + msg: `Do you want to uninstall ${item.name}?`, + ok: 'Yes', + cancel: 'No' + }).open(); + if (!uninstall) { + return; + } + const { installable, searchable, itemLabel } = this.props; + const dialog = new UninstallationProgressDialog(itemLabel(item)); + dialog.open(); + try { + await installable.uninstall({ item }); + const { items } = await searchable.search({ query: this.state.filterText }); + this.setState({ items: this.sort(items) }); + } finally { + dialog.close(); + } + } + } export namespace FilterableListContainer { diff --git a/arduino-ide-extension/src/browser/components/component-list/list-item-renderer.tsx b/arduino-ide-extension/src/browser/components/component-list/list-item-renderer.tsx index f8baa001..478caaab 100644 --- a/arduino-ide-extension/src/browser/components/component-list/list-item-renderer.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/list-item-renderer.tsx @@ -22,15 +22,17 @@ export class ListItemRenderer { renderItem( input: ComponentListItem.State & { item: T }, install: (item: T) => Promise, + uninstall: (item: T) => Promise, onVersionChange: (version: Installable.Version) => void ): React.ReactNode { const { item } = input; const name = {item.name}; const author = {item.author}; + const onClickUninstall = () => uninstall(item); const installedVersion = !!item.installedVersion &&
Version {item.installedVersion} - INSTALLED +
; const summary =
{item.summary}
; diff --git a/arduino-ide-extension/src/browser/components/installation-progress-dialog.tsx b/arduino-ide-extension/src/browser/components/progress-dialog.tsx similarity index 55% rename from arduino-ide-extension/src/browser/components/installation-progress-dialog.tsx rename to arduino-ide-extension/src/browser/components/progress-dialog.tsx index a59c49f4..bfe564d1 100644 --- a/arduino-ide-extension/src/browser/components/installation-progress-dialog.tsx +++ b/arduino-ide-extension/src/browser/components/progress-dialog.tsx @@ -10,3 +10,14 @@ export class InstallationProgressDialog extends AbstractDialog { } } + +export class UninstallationProgressDialog extends AbstractDialog { + + readonly value = undefined; + + constructor(componentName: string) { + super({ title: 'Uninstallation in progress' }); + this.contentNode.textContent = `Uninstalling ${componentName}. Please wait...`; + } + +} diff --git a/arduino-ide-extension/src/browser/style/list-widget.css b/arduino-ide-extension/src/browser/style/list-widget.css index 2ca4f3a5..3f8d44b5 100644 --- a/arduino-ide-extension/src/browser/style/list-widget.css +++ b/arduino-ide-extension/src/browser/style/list-widget.css @@ -116,8 +116,9 @@ https://github.com/arduino/arduino-pro-ide/issues/82 */ color: var(--theia-ui-font-color2); } -.component-list-item .header .installed { +.component-list-item .header .installed:before { margin-left: 4px; + display: inline-block; justify-self: end; background-color: var(--theia-accent-color1); padding: 2px 4px 2px 4px; @@ -125,6 +126,13 @@ https://github.com/arduino/arduino-pro-ide/issues/82 */ font-weight: bold; max-height: calc(1em + 4px); color: var(--theia-inverse-ui-font-color0); + content: 'INSTALLED'; +} + +.component-list-item .header .installed:hover:before { + background-color: var(--theia-inverse-ui-font-color0); + color: var(--theia-accent-color1); + content: 'UNINSTALL'; } .component-list-item[min-width~="170px"] .footer { diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index a6b55c3a..32cbc1d8 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -46,10 +46,15 @@ export interface BoardInstalledEvent { readonly pkg: Readonly; } +export interface BoardUninstalledEvent { + readonly pkg: Readonly; +} + export const BoardsServiceClient = Symbol('BoardsServiceClient'); export interface BoardsServiceClient { notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void; notifyBoardInstalled(event: BoardInstalledEvent): void + notifyBoardUninstalled(event: BoardUninstalledEvent): void } export const BoardsServicePath = '/services/boards-service'; diff --git a/arduino-ide-extension/src/common/protocol/installable.ts b/arduino-ide-extension/src/common/protocol/installable.ts index 4d4df38a..ae7334d2 100644 --- a/arduino-ide-extension/src/common/protocol/installable.ts +++ b/arduino-ide-extension/src/common/protocol/installable.ts @@ -6,6 +6,11 @@ export interface Installable { * If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`. */ install(options: { item: T, version?: Installable.Version }): Promise; + + /** + * Uninstalls the given component. It is a NOOP if not installed. + */ + uninstall(options: { item: T }): Promise; } export namespace Installable { export type Version = string; diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index f6be2e0c..dcfaeae6 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -2,7 +2,7 @@ import * as PQueue from 'p-queue'; import { injectable, inject, postConstruct, named } from 'inversify'; import { ILogger } from '@theia/core/lib/common/logger'; import { BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient, Port } from '../common/protocol/boards-service'; -import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp, Platform } from './cli-protocol/commands/core_pb'; +import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp, Platform, PlatformUninstallReq } from './cli-protocol/commands/core_pb'; import { CoreClientProvider } from './core-client-provider'; import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb'; import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; @@ -288,4 +288,37 @@ export class BoardsServiceImpl implements BoardsService { console.info("Board installation done", pkg); } + async uninstall(options: { item: BoardPackage }): Promise { + const pkg = options.item; + const coreClient = await this.coreClientProvider.getClient(); + if (!coreClient) { + return; + } + const { client, instance } = coreClient; + + const [platform, boardName] = pkg.id.split(":"); + + const req = new PlatformUninstallReq(); + req.setInstance(instance); + req.setArchitecture(boardName); + req.setPlatformPackage(platform); + + console.info("Starting board uninstallation", pkg); + const resp = client.platformUninstall(req); + resp.on('data', (r: PlatformInstallResp) => { + const prog = r.getProgress(); + if (prog && prog.getFile()) { + this.toolOutputService.publishNewOutput("board uninstall", `uninstalling ${prog.getFile()}\n`) + } + }); + await new Promise((resolve, reject) => { + resp.on('end', resolve); + resp.on('error', reject); + }); + if (this.client) { + this.client.notifyBoardUninstalled({ pkg }); + } + console.info("Board uninstallation done", pkg); + } + } diff --git a/arduino-ide-extension/src/node/library-service-impl.ts b/arduino-ide-extension/src/node/library-service-impl.ts index d56664b2..34193c67 100644 --- a/arduino-ide-extension/src/node/library-service-impl.ts +++ b/arduino-ide-extension/src/node/library-service-impl.ts @@ -3,7 +3,7 @@ import { Library, LibraryService } from '../common/protocol/library-service'; import { CoreClientProvider } from './core-client-provider'; import { LibrarySearchReq, LibrarySearchResp, LibraryListReq, LibraryListResp, LibraryRelease, - InstalledLibrary, LibraryInstallReq, LibraryInstallResp + InstalledLibrary, LibraryInstallReq, LibraryInstallResp, LibraryUninstallReq } from './cli-protocol/commands/lib_pb'; import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; import { Installable } from '../common/protocol/installable'; @@ -90,6 +90,32 @@ export class LibraryServiceImpl implements LibraryService { }); } + async uninstall(options: { item: Library }): Promise { + const library = options.item; + const coreClient = await this.coreClientProvider.getClient(); + if (!coreClient) { + return; + } + const { client, instance } = coreClient; + + const req = new LibraryUninstallReq(); + req.setInstance(instance); + req.setName(library.name); + req.setVersion(library.installedVersion!); + + const resp = client.libraryInstall(req); + resp.on('data', (r: LibraryInstallResp) => { + const prog = r.getProgress(); + if (prog) { + this.toolOutputService.publishNewOutput("library uninstall", `uninstalling ${prog.getFile()}: ${prog.getCompleted()}%\n`) + } + }); + await new Promise((resolve, reject) => { + resp.on('end', resolve); + resp.on('error', reject); + }); + } + } function toLibrary(tpl: Partial, release: LibraryRelease, availableVersions: string[]): Library {