mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-04-19 12:57:17 +00:00
[ATL-1533] Firmware&Certificate Uploader (#469)
Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
This commit is contained in:
parent
6233e1fa98
commit
302fb7b6af
@ -58,3 +58,13 @@ The Config Service knows about your system, like for example the default sketch
|
||||
#### Rebuild gRPC protocol interfaces
|
||||
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
|
||||
`yarn --cwd arduino-ide-extension generate-protocol`
|
||||
|
||||
### Customize Icons
|
||||
ArduinoIde uses a customized version of FontAwesome.
|
||||
In order to update/replace icons follow the following steps:
|
||||
- import the file `arduino-icons.json` in [Icomoon](https://icomoon.io/app/#/projects)
|
||||
- load it
|
||||
- edit the icons as needed
|
||||
- !! download the **new** `arduino-icons.json` file and put it in this repo
|
||||
- Click on "Generate Font" in Icomoon, then download
|
||||
- place the updated fonts in the `src/style/fonts` directory
|
||||
|
15709
arduino-ide-extension/arduino-icons.json
Normal file
15709
arduino-ide-extension/arduino-icons.json
Normal file
File diff suppressed because one or more lines are too long
@ -29,11 +29,12 @@
|
||||
"@theia/monaco": "next",
|
||||
"@theia/navigator": "next",
|
||||
"@theia/outline-view": "next",
|
||||
"@theia/preferences": "next",
|
||||
"@theia/output": "next",
|
||||
"@theia/preferences": "next",
|
||||
"@theia/search-in-workspace": "next",
|
||||
"@theia/terminal": "next",
|
||||
"@theia/workspace": "next",
|
||||
"@tippyjs/react": "^4.2.5",
|
||||
"@types/atob": "^2.1.2",
|
||||
"@types/auth0-js": "^9.14.0",
|
||||
"@types/btoa": "^1.2.3",
|
||||
@ -140,7 +141,7 @@
|
||||
"version": "0.18.3"
|
||||
},
|
||||
"fwuploader": {
|
||||
"version": "1.0.2"
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -223,7 +223,7 @@ import { CloudSketchbookWidget } from './widgets/cloud-sketchbook/cloud-sketchbo
|
||||
import { CloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
|
||||
import { createCloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-container';
|
||||
import { CreateApi } from './create/create-api';
|
||||
import { ShareSketchDialog } from './dialogs.ts/cloud-share-sketch-dialog';
|
||||
import { ShareSketchDialog } from './dialogs/cloud-share-sketch-dialog';
|
||||
import { AuthenticationClientService } from './auth/authentication-client-service';
|
||||
import {
|
||||
AuthenticationService,
|
||||
@ -237,6 +237,23 @@ import { SketchbookWidget } from './widgets/sketchbook/sketchbook-widget';
|
||||
import { SketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-widget';
|
||||
import { createSketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-container';
|
||||
import { SketchCache } from './widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
import { UploadFirmware } from './contributions/upload-firmware';
|
||||
import {
|
||||
UploadFirmwareDialog,
|
||||
UploadFirmwareDialogProps,
|
||||
UploadFirmwareDialogWidget,
|
||||
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
|
||||
|
||||
import { UploadCertificate } from './contributions/upload-certificate';
|
||||
import {
|
||||
ArduinoFirmwareUploader,
|
||||
ArduinoFirmwareUploaderPath,
|
||||
} from '../common/protocol/arduino-firmware-uploader';
|
||||
import {
|
||||
UploadCertificateDialog,
|
||||
UploadCertificateDialogProps,
|
||||
UploadCertificateDialogWidget,
|
||||
} from './dialogs/certificate-uploader/certificate-uploader-dialog';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
@ -522,6 +539,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
bind(ArduinoFirmwareUploader)
|
||||
.toDynamicValue((context) =>
|
||||
WebSocketConnectionProvider.createProxy(
|
||||
context.container,
|
||||
ArduinoFirmwareUploaderPath
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
// File-system extension
|
||||
bind(FileSystemExt)
|
||||
.toDynamicValue((context) =>
|
||||
@ -571,6 +597,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, About);
|
||||
Contribution.configure(bind, Debug);
|
||||
Contribution.configure(bind, Sketchbook);
|
||||
Contribution.configure(bind, UploadFirmware);
|
||||
Contribution.configure(bind, UploadCertificate);
|
||||
Contribution.configure(bind, BoardSelection);
|
||||
Contribution.configure(bind, OpenRecentSketch);
|
||||
Contribution.configure(bind, Help);
|
||||
@ -713,4 +741,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
id: 'cloud-sketchbook-composite-widget',
|
||||
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
|
||||
}));
|
||||
|
||||
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
|
||||
bind(UploadFirmwareDialog).toSelf().inSingletonScope();
|
||||
bind(UploadFirmwareDialogProps).toConstantValue({
|
||||
title: 'UploadFirmware',
|
||||
});
|
||||
bind(UploadCertificateDialogWidget).toSelf().inSingletonScope();
|
||||
bind(UploadCertificateDialog).toSelf().inSingletonScope();
|
||||
bind(UploadCertificateDialogProps).toConstantValue({
|
||||
title: 'UploadCertificate',
|
||||
});
|
||||
});
|
||||
|
@ -55,6 +55,11 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
|
||||
default: true,
|
||||
},
|
||||
'arduino.board.certificates': {
|
||||
type: 'string',
|
||||
description: 'List of certificates that can be uploaded to boards',
|
||||
default: '',
|
||||
},
|
||||
'arduino.sketchbook.showAllFiles': {
|
||||
type: 'boolean',
|
||||
description:
|
||||
@ -123,6 +128,7 @@ export interface ArduinoConfiguration {
|
||||
'arduino.window.autoScale': boolean;
|
||||
'arduino.window.zoomLevel': number;
|
||||
'arduino.ide.autoUpdate': boolean;
|
||||
'arduino.board.certificates': string;
|
||||
'arduino.sketchbook.showAllFiles': boolean;
|
||||
'arduino.cloud.enabled': boolean;
|
||||
'arduino.cloud.pull.warn': boolean;
|
||||
|
@ -0,0 +1,136 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import {
|
||||
Command,
|
||||
MenuModelRegistry,
|
||||
CommandRegistry,
|
||||
Contribution,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { UploadCertificateDialog } from '../dialogs/certificate-uploader/certificate-uploader-dialog';
|
||||
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
||||
import {
|
||||
PreferenceScope,
|
||||
PreferenceService,
|
||||
} from '@theia/core/lib/browser/preferences/preference-service';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import {
|
||||
arduinoCert,
|
||||
certificateList,
|
||||
} from '../dialogs/certificate-uploader/utils';
|
||||
import { ArduinoFirmwareUploader } from '../../common/protocol/arduino-firmware-uploader';
|
||||
|
||||
@injectable()
|
||||
export class UploadCertificate extends Contribution {
|
||||
@inject(UploadCertificateDialog)
|
||||
protected readonly dialog: UploadCertificateDialog;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@inject(ArduinoFirmwareUploader)
|
||||
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||
|
||||
protected dialogOpened = false;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UploadCertificate.Commands.OPEN, {
|
||||
execute: async () => {
|
||||
try {
|
||||
this.dialogOpened = true;
|
||||
await this.dialog.open();
|
||||
} finally {
|
||||
this.dialogOpened = false;
|
||||
}
|
||||
},
|
||||
isEnabled: () => !this.dialogOpened,
|
||||
});
|
||||
|
||||
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
|
||||
execute: async (certToRemove) => {
|
||||
const certs = this.arduinoPreferences.get('arduino.board.certificates');
|
||||
|
||||
this.preferenceService.set(
|
||||
'arduino.board.certificates',
|
||||
certificateList(certs)
|
||||
.filter((c) => c !== certToRemove)
|
||||
.join(','),
|
||||
PreferenceScope.User
|
||||
);
|
||||
},
|
||||
isEnabled: (certToRemove) => certToRemove !== arduinoCert,
|
||||
});
|
||||
|
||||
registry.registerCommand(UploadCertificate.Commands.UPLOAD_CERT, {
|
||||
execute: async ({ fqbn, address, urls }) => {
|
||||
return this.arduinoFirmwareUploader.uploadCertificates(
|
||||
`-b ${fqbn} -a ${address} ${urls
|
||||
.map((url: string) => `-u ${url}`)
|
||||
.join(' ')}`
|
||||
);
|
||||
},
|
||||
isEnabled: () => true,
|
||||
});
|
||||
|
||||
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
|
||||
execute: async (args: any) => {
|
||||
this.contextMenuRenderer.render({
|
||||
menuPath: ArduinoMenus.ROOT_CERTIFICATES__CONTEXT,
|
||||
anchor: {
|
||||
x: args.x,
|
||||
y: args.y,
|
||||
},
|
||||
args: [args.cert],
|
||||
});
|
||||
},
|
||||
isEnabled: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
|
||||
commandId: UploadCertificate.Commands.OPEN.id,
|
||||
label: UploadCertificate.Commands.OPEN.label,
|
||||
order: '1',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.ROOT_CERTIFICATES__CONTEXT, {
|
||||
commandId: UploadCertificate.Commands.REMOVE_CERT.id,
|
||||
label: UploadCertificate.Commands.REMOVE_CERT.label,
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UploadCertificate {
|
||||
export namespace Commands {
|
||||
export const OPEN: Command = {
|
||||
id: 'arduino-upload-certificate-open',
|
||||
label: 'Upload SSL Root Certificates',
|
||||
category: 'Arduino',
|
||||
};
|
||||
|
||||
export const OPEN_CERT_CONTEXT: Command = {
|
||||
id: 'arduino-certificate-open-context',
|
||||
label: 'Open context',
|
||||
category: 'Arduino',
|
||||
};
|
||||
|
||||
export const REMOVE_CERT: Command = {
|
||||
id: 'arduino-certificate-remove',
|
||||
label: 'Remove',
|
||||
category: 'Arduino',
|
||||
};
|
||||
|
||||
export const UPLOAD_CERT: Command = {
|
||||
id: 'arduino-certificate-upload',
|
||||
label: 'Upload',
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import {
|
||||
Command,
|
||||
MenuModelRegistry,
|
||||
CommandRegistry,
|
||||
Contribution,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { UploadFirmwareDialog } from '../dialogs/firmware-uploader/firmware-uploader-dialog';
|
||||
|
||||
@injectable()
|
||||
export class UploadFirmware extends Contribution {
|
||||
@inject(UploadFirmwareDialog)
|
||||
protected readonly dialog: UploadFirmwareDialog;
|
||||
|
||||
protected dialogOpened = false;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UploadFirmware.Commands.OPEN, {
|
||||
execute: async () => {
|
||||
try {
|
||||
this.dialogOpened = true;
|
||||
await this.dialog.open();
|
||||
} finally {
|
||||
this.dialogOpened = false;
|
||||
}
|
||||
},
|
||||
isEnabled: () => !this.dialogOpened,
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
|
||||
commandId: UploadFirmware.Commands.OPEN.id,
|
||||
label: UploadFirmware.Commands.OPEN.label,
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UploadFirmware {
|
||||
export namespace Commands {
|
||||
export const OPEN: Command = {
|
||||
id: 'arduino-upload-firmware-open',
|
||||
label: 'WiFi101 / WiFiNINA Firmware Updater',
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
@ -97,6 +97,7 @@
|
||||
"editorWhitespace.foreground": "#bfbfbf",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#ffcb00",
|
||||
"editorWidget.background": "#F7F9F9",
|
||||
"focusBorder": "#7fcbcd99",
|
||||
"menubar.selectionBackground": "#ffffff",
|
||||
"menubar.selectionForeground": "#212121",
|
||||
|
@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const CertificateAddComponent = ({
|
||||
addCertificate,
|
||||
}: {
|
||||
addCertificate: (cert: string) => void;
|
||||
}): React.ReactElement => {
|
||||
const [value, setValue] = React.useState('');
|
||||
|
||||
const handleChange = React.useCallback((event) => {
|
||||
setValue(event.target.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="certificate-add"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
addCertificate(value);
|
||||
setValue('');
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
<div>Add URL to fetch SSL certificate</div>
|
||||
<input
|
||||
className="theia-input"
|
||||
placeholder="Enter URL"
|
||||
type="text"
|
||||
name="add"
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const CertificateListComponent = ({
|
||||
certificates,
|
||||
selectedCerts,
|
||||
setSelectedCerts,
|
||||
openContextMenu,
|
||||
}: {
|
||||
certificates: string[];
|
||||
selectedCerts: string[];
|
||||
setSelectedCerts: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
openContextMenu: (x: number, y: number, cert: string) => void;
|
||||
}): React.ReactElement => {
|
||||
const handleOnChange = (event: any) => {
|
||||
const target = event.target;
|
||||
|
||||
const newSelectedCerts = selectedCerts.filter(
|
||||
(cert) => cert !== target.name
|
||||
);
|
||||
|
||||
if (target.checked) {
|
||||
newSelectedCerts.push(target.name);
|
||||
}
|
||||
|
||||
setSelectedCerts(newSelectedCerts);
|
||||
};
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent, cert: string) => {
|
||||
openContextMenu(event.clientX, event.clientY, cert);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="certificate-list">
|
||||
{certificates.map((certificate, i) => (
|
||||
<label
|
||||
key={i}
|
||||
className="certificate-row"
|
||||
onContextMenu={(e) => handleContextMenu(e, certificate)}
|
||||
>
|
||||
<span className="fl1">{certificate}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={certificate}
|
||||
checked={selectedCerts.includes(certificate)}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,157 @@
|
||||
import * as React from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import { AvailableBoard } from '../../boards/boards-service-provider';
|
||||
import { CertificateListComponent } from './certificate-list';
|
||||
import { SelectBoardComponent } from './select-board-components';
|
||||
import { CertificateAddComponent } from './certificate-add-new';
|
||||
|
||||
export const CertificateUploaderComponent = ({
|
||||
availableBoards,
|
||||
certificates,
|
||||
addCertificate,
|
||||
updatableFqbns,
|
||||
uploadCertificates,
|
||||
openContextMenu,
|
||||
}: {
|
||||
availableBoards: AvailableBoard[];
|
||||
certificates: string[];
|
||||
addCertificate: (cert: string) => void;
|
||||
updatableFqbns: string[];
|
||||
uploadCertificates: (
|
||||
fqbn: string,
|
||||
address: string,
|
||||
urls: string[]
|
||||
) => Promise<any>;
|
||||
openContextMenu: (x: number, y: number, cert: string) => void;
|
||||
}): React.ReactElement => {
|
||||
const [installFeedback, setInstallFeedback] = React.useState<
|
||||
'ok' | 'fail' | 'installing' | null
|
||||
>(null);
|
||||
|
||||
const [showAdd, setShowAdd] = React.useState(false);
|
||||
|
||||
const [selectedCerts, setSelectedCerts] = React.useState<string[]>([]);
|
||||
|
||||
const [selectedBoard, setSelectedBoard] =
|
||||
React.useState<AvailableBoard | null>(null);
|
||||
|
||||
const installCertificates = async () => {
|
||||
if (!selectedBoard || !selectedBoard.fqbn || !selectedBoard.port) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInstallFeedback('installing');
|
||||
|
||||
try {
|
||||
await uploadCertificates(
|
||||
selectedBoard.fqbn,
|
||||
selectedBoard.port.address,
|
||||
selectedCerts
|
||||
);
|
||||
setInstallFeedback('ok');
|
||||
} catch {
|
||||
setInstallFeedback('fail');
|
||||
}
|
||||
};
|
||||
|
||||
const onBoardSelect = React.useCallback(
|
||||
(board: AvailableBoard) => {
|
||||
const newFqbn = (board && board.fqbn) || null;
|
||||
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
|
||||
|
||||
if (newFqbn !== prevFqbn) {
|
||||
setInstallFeedback(null);
|
||||
setSelectedBoard(board);
|
||||
}
|
||||
},
|
||||
[selectedBoard]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialogSection">
|
||||
<div className="dialogRow">
|
||||
<strong className="fl1">1. Select certificate to upload</strong>
|
||||
<Tippy
|
||||
content={
|
||||
<CertificateAddComponent
|
||||
addCertificate={(cert) => {
|
||||
addCertificate(cert);
|
||||
setShowAdd(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
placement="bottom-end"
|
||||
onClickOutside={() => setShowAdd(false)}
|
||||
visible={showAdd}
|
||||
interactive={true}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="theia-button primary add-cert-btn"
|
||||
onClick={() => {
|
||||
showAdd ? setShowAdd(false) : setShowAdd(true);
|
||||
}}
|
||||
>
|
||||
Add New <span className="fa fa-caret-down caret"></span>
|
||||
</button>
|
||||
</Tippy>
|
||||
</div>
|
||||
<div className="dialogRow">
|
||||
<CertificateListComponent
|
||||
certificates={certificates}
|
||||
selectedCerts={selectedCerts}
|
||||
setSelectedCerts={setSelectedCerts}
|
||||
openContextMenu={openContextMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dialogSection">
|
||||
<div className="dialogRow">
|
||||
<strong>2. Select destination board and upload certificate</strong>
|
||||
</div>
|
||||
<div className="dialogRow">
|
||||
<div className="fl1">
|
||||
<SelectBoardComponent
|
||||
availableBoards={availableBoards}
|
||||
updatableFqbns={updatableFqbns}
|
||||
onBoardSelect={onBoardSelect}
|
||||
selectedBoard={selectedBoard}
|
||||
busy={installFeedback === 'installing'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dialogRow">
|
||||
<div className="upload-status">
|
||||
{installFeedback === 'installing' && (
|
||||
<div className="success">
|
||||
<div className="spinner" />
|
||||
Uploading certificates.
|
||||
</div>
|
||||
)}
|
||||
{installFeedback === 'ok' && (
|
||||
<div className="success">
|
||||
<i className="fa fa-info status-icon" />
|
||||
Cetificates uploaded.
|
||||
</div>
|
||||
)}
|
||||
{installFeedback === 'fail' && (
|
||||
<div className="warn">
|
||||
<i className="fa fa-exclamation status-icon" />
|
||||
Upload failed. Please try again.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="theia-button primary install-cert-btn"
|
||||
onClick={installCertificates}
|
||||
disabled={selectedCerts.length === 0 || !selectedBoard}
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,190 @@
|
||||
import * as React from 'react';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { Widget } from '@phosphor/widgets';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import {
|
||||
AvailableBoard,
|
||||
BoardsServiceProvider,
|
||||
} from '../../boards/boards-service-provider';
|
||||
import { CertificateUploaderComponent } from './certificate-uploader-component';
|
||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
import {
|
||||
PreferenceScope,
|
||||
PreferenceService,
|
||||
} from '@theia/core/lib/browser/preferences/preference-service';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { certificateList, sanifyCertString } from './utils';
|
||||
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
|
||||
|
||||
@injectable()
|
||||
export class UploadCertificateDialogWidget extends ReactWidget {
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(ArduinoFirmwareUploader)
|
||||
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||
|
||||
protected certificates: string[] = [];
|
||||
protected updatableFqbns: string[] = [];
|
||||
protected availableBoards: AvailableBoard[] = [];
|
||||
|
||||
public busyCallback = (busy: boolean) => {
|
||||
return;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.arduinoPreferences.ready.then(() => {
|
||||
this.certificates = certificateList(
|
||||
this.arduinoPreferences.get('arduino.board.certificates')
|
||||
);
|
||||
});
|
||||
this.arduinoPreferences.onPreferenceChanged((event) => {
|
||||
if (
|
||||
event.preferenceName === 'arduino.board.certificates' &&
|
||||
event.newValue !== event.oldValue
|
||||
) {
|
||||
this.certificates = certificateList(event.newValue);
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
|
||||
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
|
||||
this.updatableFqbns = fqbns;
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
|
||||
this.availableBoards = availableBoards;
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
|
||||
private addCertificate(certificate: string) {
|
||||
const certString = sanifyCertString(certificate);
|
||||
|
||||
if (certString.length > 0) {
|
||||
this.certificates.push(sanifyCertString(certificate));
|
||||
}
|
||||
|
||||
this.preferenceService.set(
|
||||
'arduino.board.certificates',
|
||||
this.certificates.join(','),
|
||||
PreferenceScope.User
|
||||
);
|
||||
}
|
||||
|
||||
protected openContextMenu(x: number, y: number, cert: string): void {
|
||||
this.commandRegistry.executeCommand(
|
||||
'arduino-certificate-open-context',
|
||||
Object.assign({}, { x, y, cert })
|
||||
);
|
||||
}
|
||||
|
||||
protected uploadCertificates(
|
||||
fqbn: string,
|
||||
address: string,
|
||||
urls: string[]
|
||||
): Promise<any> {
|
||||
this.busyCallback(true);
|
||||
return this.commandRegistry
|
||||
.executeCommand('arduino-certificate-upload', {
|
||||
fqbn,
|
||||
address,
|
||||
urls,
|
||||
})
|
||||
.finally(() => this.busyCallback(false));
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return (
|
||||
<CertificateUploaderComponent
|
||||
availableBoards={this.availableBoards}
|
||||
certificates={this.certificates}
|
||||
updatableFqbns={this.updatableFqbns}
|
||||
addCertificate={this.addCertificate.bind(this)}
|
||||
uploadCertificates={this.uploadCertificates.bind(this)}
|
||||
openContextMenu={this.openContextMenu.bind(this)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class UploadCertificateDialogProps extends DialogProps {}
|
||||
|
||||
@injectable()
|
||||
export class UploadCertificateDialog extends AbstractDialog<void> {
|
||||
@inject(UploadCertificateDialogWidget)
|
||||
protected readonly widget: UploadCertificateDialogWidget;
|
||||
|
||||
private busy = false;
|
||||
|
||||
constructor(
|
||||
@inject(UploadCertificateDialogProps)
|
||||
protected readonly props: UploadCertificateDialogProps
|
||||
) {
|
||||
super({ title: 'Upload SSL Root Certificates' });
|
||||
this.contentNode.classList.add('certificate-uploader-dialog');
|
||||
this.acceptButton = undefined;
|
||||
}
|
||||
|
||||
get value(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
protected onAfterAttach(msg: Message): void {
|
||||
if (this.widget.isAttached) {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
this.widget.busyCallback = this.busyCallback.bind(this);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.widget.update();
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.widget.activate();
|
||||
}
|
||||
|
||||
protected handleEnter(event: KeyboardEvent): boolean | void {
|
||||
return false;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
super.close();
|
||||
}
|
||||
|
||||
busyCallback(busy: boolean): void {
|
||||
this.busy = busy;
|
||||
if (busy) {
|
||||
this.closeCrossNode.classList.add('disabled');
|
||||
} else {
|
||||
this.closeCrossNode.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import * as React from 'react';
|
||||
import { AvailableBoard } from '../../boards/boards-service-provider';
|
||||
import { ArduinoSelect } from '../../widgets/arduino-select';
|
||||
|
||||
type BoardOption = { value: string; label: string };
|
||||
|
||||
export const SelectBoardComponent = ({
|
||||
availableBoards,
|
||||
updatableFqbns,
|
||||
onBoardSelect,
|
||||
selectedBoard,
|
||||
busy,
|
||||
}: {
|
||||
availableBoards: AvailableBoard[];
|
||||
updatableFqbns: string[];
|
||||
onBoardSelect: (board: AvailableBoard | null) => void;
|
||||
selectedBoard: AvailableBoard | null;
|
||||
busy: boolean;
|
||||
}): React.ReactElement => {
|
||||
const [selectOptions, setSelectOptions] = React.useState<BoardOption[]>([]);
|
||||
|
||||
const [selectBoardPlaceholder, setSelectBoardPlaceholder] =
|
||||
React.useState('');
|
||||
|
||||
const selectOption = React.useCallback(
|
||||
(boardOpt: BoardOption) => {
|
||||
onBoardSelect(
|
||||
(boardOpt &&
|
||||
availableBoards.find((board) => board.fqbn === boardOpt.value)) ||
|
||||
null
|
||||
);
|
||||
},
|
||||
[availableBoards, onBoardSelect]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// if there is activity going on, skip updating the boards (avoid flickering)
|
||||
if (busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
let placeholderTxt = 'Select a board...';
|
||||
let selBoard = -1;
|
||||
const updatableBoards = availableBoards.filter(
|
||||
(board) => board.port && board.fqbn && updatableFqbns.includes(board.fqbn)
|
||||
);
|
||||
const boardsList: BoardOption[] = updatableBoards.map((board, i) => {
|
||||
if (board.selected) {
|
||||
selBoard = i;
|
||||
}
|
||||
return {
|
||||
label: `${board.name} at ${board.port?.address}`,
|
||||
value: board.fqbn || '',
|
||||
};
|
||||
});
|
||||
|
||||
if (boardsList.length === 0) {
|
||||
placeholderTxt = 'No supported board connected';
|
||||
}
|
||||
|
||||
setSelectBoardPlaceholder(placeholderTxt);
|
||||
setSelectOptions(boardsList);
|
||||
|
||||
if (selectedBoard) {
|
||||
selBoard = boardsList
|
||||
.map((boardOpt) => boardOpt.value)
|
||||
.indexOf(selectedBoard.fqbn || '');
|
||||
}
|
||||
|
||||
selectOption(boardsList[selBoard] || null);
|
||||
}, [busy, availableBoards, selectOption, updatableFqbns, selectedBoard]);
|
||||
|
||||
return (
|
||||
<ArduinoSelect
|
||||
id="board-select"
|
||||
menuPosition="fixed"
|
||||
isDisabled={selectOptions.length === 0 || busy}
|
||||
placeholder={selectBoardPlaceholder}
|
||||
options={selectOptions}
|
||||
value={
|
||||
(selectedBoard && {
|
||||
value: selectedBoard.fqbn,
|
||||
label: `${selectedBoard.name} at ${selectedBoard.port?.address}`,
|
||||
}) ||
|
||||
null
|
||||
}
|
||||
tabSelectsValue={false}
|
||||
onChange={selectOption}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
export const arduinoCert = 'arduino.cc:443';
|
||||
|
||||
export function sanifyCertString(cert: string): string {
|
||||
const regex = /^(?:.*:\/\/)*(\S+\.+[^:]*):*(\d*)*$/gm;
|
||||
|
||||
const m = regex.exec(cert);
|
||||
|
||||
if (!m) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const domain = m[1] || '';
|
||||
const port = m[2] || '443';
|
||||
|
||||
if (domain.length === 0 || port.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${domain}:${port}`;
|
||||
}
|
||||
|
||||
export function certificateList(certificates: string): string[] {
|
||||
let certs = certificates
|
||||
.split(',')
|
||||
.map((cert) => sanifyCertString(cert.trim()))
|
||||
.filter((cert) => {
|
||||
// remove empty certificates
|
||||
if (!cert || cert.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// add arduino certificate at the top of the list
|
||||
certs = certs.filter((cert) => cert !== arduinoCert);
|
||||
certs.unshift(arduinoCert);
|
||||
return certs;
|
||||
}
|
@ -42,10 +42,6 @@ export const ShareSketchComponent = ({
|
||||
createApi: CreateApi;
|
||||
domain?: string;
|
||||
}): React.ReactElement => {
|
||||
// const [publicVisibility, setPublicVisibility] = React.useState<boolean>(
|
||||
// treeNode.isPublic
|
||||
// );
|
||||
|
||||
const [loading, setloading] = React.useState<boolean>(false);
|
||||
|
||||
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {
|
@ -8,7 +8,7 @@ import {
|
||||
} from '@theia/core/lib/browser/dialogs';
|
||||
|
||||
@injectable()
|
||||
export class DoNotAskAgainConfirmDialogProps extends ConfirmDialogProps {
|
||||
export class DoNotAskAgainDialogProps extends ConfirmDialogProps {
|
||||
readonly onAccept: () => Promise<void>;
|
||||
}
|
||||
|
||||
@ -17,8 +17,8 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
|
||||
protected readonly doNotAskAgainCheckbox: HTMLInputElement;
|
||||
|
||||
constructor(
|
||||
@inject(DoNotAskAgainConfirmDialogProps)
|
||||
protected readonly props: DoNotAskAgainConfirmDialogProps
|
||||
@inject(DoNotAskAgainDialogProps)
|
||||
protected readonly props: DoNotAskAgainDialogProps
|
||||
) {
|
||||
super(props);
|
||||
this.controlPanel.removeChild(this.errorMessageNode);
|
@ -0,0 +1,204 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ArduinoFirmwareUploader,
|
||||
FirmwareInfo,
|
||||
} from '../../../common/protocol/arduino-firmware-uploader';
|
||||
import { AvailableBoard } from '../../boards/boards-service-provider';
|
||||
import { ArduinoSelect } from '../../widgets/arduino-select';
|
||||
import { SelectBoardComponent } from '../certificate-uploader/select-board-components';
|
||||
|
||||
type FirmwareOption = { value: string; label: string };
|
||||
|
||||
export const FirmwareUploaderComponent = ({
|
||||
availableBoards,
|
||||
firmwareUploader,
|
||||
updatableFqbns,
|
||||
flashFirmware,
|
||||
isOpen,
|
||||
}: {
|
||||
availableBoards: AvailableBoard[];
|
||||
firmwareUploader: ArduinoFirmwareUploader;
|
||||
updatableFqbns: string[];
|
||||
flashFirmware: (firmware: FirmwareInfo, port: string) => Promise<any>;
|
||||
isOpen: any;
|
||||
}): React.ReactElement => {
|
||||
// boolean states for buttons
|
||||
const [firmwaresFetching, setFirmwaresFetching] = React.useState(false);
|
||||
|
||||
const [installFeedback, setInstallFeedback] = React.useState<
|
||||
'ok' | 'fail' | 'installing' | null
|
||||
>(null);
|
||||
|
||||
const [selectedBoard, setSelectedBoard] =
|
||||
React.useState<AvailableBoard | null>(null);
|
||||
|
||||
const [availableFirmwares, setAvailableFirmwares] = React.useState<
|
||||
FirmwareInfo[]
|
||||
>([]);
|
||||
React.useEffect(() => {
|
||||
setAvailableFirmwares([]);
|
||||
}, [isOpen]);
|
||||
const [selectedFirmware, setSelectedFirmware] =
|
||||
React.useState<FirmwareOption | null>(null);
|
||||
|
||||
const [firmwareOptions, setFirmwareOptions] = React.useState<
|
||||
FirmwareOption[]
|
||||
>([]);
|
||||
|
||||
const fetchFirmwares = React.useCallback(async () => {
|
||||
setInstallFeedback(null);
|
||||
setFirmwaresFetching(true);
|
||||
if (!selectedBoard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch the firmwares for the selected board
|
||||
const firmwaresForFqbn = await firmwareUploader.availableFirmwares(
|
||||
selectedBoard.fqbn || ''
|
||||
);
|
||||
setAvailableFirmwares(firmwaresForFqbn);
|
||||
|
||||
const firmwaresOpts = firmwaresForFqbn.map((f) => ({
|
||||
label: f.firmware_version,
|
||||
value: f.firmware_version,
|
||||
}));
|
||||
|
||||
setFirmwareOptions(firmwaresOpts);
|
||||
|
||||
if (firmwaresForFqbn.length > 0) setSelectedFirmware(firmwaresOpts[0]);
|
||||
setFirmwaresFetching(false);
|
||||
}, [firmwareUploader, selectedBoard]);
|
||||
|
||||
const installFirmware = React.useCallback(async () => {
|
||||
setInstallFeedback('installing');
|
||||
|
||||
const firmwareToFlash = availableFirmwares.find(
|
||||
(firmware) => firmware.firmware_version === selectedFirmware?.value
|
||||
);
|
||||
|
||||
try {
|
||||
const installStatus =
|
||||
!!firmwareToFlash &&
|
||||
!!selectedBoard?.port &&
|
||||
(await flashFirmware(firmwareToFlash, selectedBoard?.port.address));
|
||||
|
||||
setInstallFeedback((installStatus && 'ok') || 'fail');
|
||||
} catch {
|
||||
setInstallFeedback('fail');
|
||||
}
|
||||
}, [firmwareUploader, selectedBoard, selectedFirmware, availableFirmwares]);
|
||||
|
||||
const onBoardSelect = React.useCallback(
|
||||
(board: AvailableBoard) => {
|
||||
const newFqbn = (board && board.fqbn) || null;
|
||||
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
|
||||
|
||||
if (newFqbn !== prevFqbn) {
|
||||
setInstallFeedback(null);
|
||||
setAvailableFirmwares([]);
|
||||
setSelectedBoard(board);
|
||||
}
|
||||
},
|
||||
[selectedBoard]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialogSection">
|
||||
<div className="dialogRow">
|
||||
<label htmlFor="board-select">Select board</label>
|
||||
</div>
|
||||
<div className="dialogRow">
|
||||
<div className="fl1">
|
||||
<SelectBoardComponent
|
||||
availableBoards={availableBoards}
|
||||
updatableFqbns={updatableFqbns}
|
||||
onBoardSelect={onBoardSelect}
|
||||
selectedBoard={selectedBoard}
|
||||
busy={installFeedback === 'installing'}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="theia-button secondary"
|
||||
disabled={
|
||||
selectedBoard === null ||
|
||||
firmwaresFetching ||
|
||||
installFeedback === 'installing'
|
||||
}
|
||||
onClick={fetchFirmwares}
|
||||
>
|
||||
Check Updates
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{availableFirmwares.length > 0 && (
|
||||
<>
|
||||
<div className="dialogSection">
|
||||
<div className="dialogRow">
|
||||
<label htmlFor="firmware-select" className="fl1">
|
||||
Select firmware version
|
||||
</label>
|
||||
<ArduinoSelect
|
||||
id="firmware-select"
|
||||
menuPosition="fixed"
|
||||
isDisabled={
|
||||
!selectedBoard ||
|
||||
firmwaresFetching ||
|
||||
installFeedback === 'installing'
|
||||
}
|
||||
options={firmwareOptions}
|
||||
value={selectedFirmware}
|
||||
tabSelectsValue={false}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
setInstallFeedback(null);
|
||||
setSelectedFirmware(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="theia-button primary"
|
||||
disabled={
|
||||
selectedFirmware === null ||
|
||||
firmwaresFetching ||
|
||||
installFeedback === 'installing'
|
||||
}
|
||||
onClick={installFirmware}
|
||||
>
|
||||
Install
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dialogSection">
|
||||
{installFeedback === null && (
|
||||
<div className="dialogRow warn">
|
||||
<i className="fa fa-exclamation status-icon" />
|
||||
Installation will overwrite the Sketch on the board.
|
||||
</div>
|
||||
)}
|
||||
{installFeedback === 'installing' && (
|
||||
<div className="dialogRow success">
|
||||
<div className="spinner" />
|
||||
Installing firmware.
|
||||
</div>
|
||||
)}
|
||||
{installFeedback === 'ok' && (
|
||||
<div className="dialogRow success">
|
||||
<i className="fa fa-info status-icon" />
|
||||
Firmware succesfully installed.
|
||||
</div>
|
||||
)}
|
||||
{installFeedback === 'fail' && (
|
||||
<div className="dialogRow warn">
|
||||
<i className="fa fa-exclamation status-icon" />
|
||||
Installation failed. Please try again.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,141 @@
|
||||
import * as React from 'react';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { Widget } from '@phosphor/widgets';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import {
|
||||
AvailableBoard,
|
||||
BoardsServiceProvider,
|
||||
} from '../../boards/boards-service-provider';
|
||||
import {
|
||||
ArduinoFirmwareUploader,
|
||||
FirmwareInfo,
|
||||
} from '../../../common/protocol/arduino-firmware-uploader';
|
||||
import { FirmwareUploaderComponent } from './firmware-uploader-component';
|
||||
import { UploadFirmware } from '../../contributions/upload-firmware';
|
||||
|
||||
@injectable()
|
||||
export class UploadFirmwareDialogWidget extends ReactWidget {
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(ArduinoFirmwareUploader)
|
||||
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||
|
||||
protected updatableFqbns: string[] = [];
|
||||
protected availableBoards: AvailableBoard[] = [];
|
||||
protected isOpen = new Object();
|
||||
|
||||
public busyCallback = (busy: boolean) => {
|
||||
return;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
|
||||
this.updatableFqbns = fqbns;
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
|
||||
this.availableBoards = availableBoards;
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
|
||||
protected flashFirmware(firmware: FirmwareInfo, port: string): Promise<any> {
|
||||
this.busyCallback(true);
|
||||
return this.arduinoFirmwareUploader
|
||||
.flash(firmware, port)
|
||||
.finally(() => this.busyCallback(false));
|
||||
}
|
||||
|
||||
onCloseRequest(msg: Message): void {
|
||||
super.onCloseRequest(msg);
|
||||
this.isOpen = new Object();
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return (
|
||||
<form>
|
||||
<FirmwareUploaderComponent
|
||||
availableBoards={this.availableBoards}
|
||||
firmwareUploader={this.arduinoFirmwareUploader}
|
||||
flashFirmware={this.flashFirmware.bind(this)}
|
||||
updatableFqbns={this.updatableFqbns}
|
||||
isOpen={this.isOpen}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class UploadFirmwareDialogProps extends DialogProps {}
|
||||
|
||||
@injectable()
|
||||
export class UploadFirmwareDialog extends AbstractDialog<void> {
|
||||
@inject(UploadFirmwareDialogWidget)
|
||||
protected readonly widget: UploadFirmwareDialogWidget;
|
||||
|
||||
private busy = false;
|
||||
|
||||
constructor(
|
||||
@inject(UploadFirmwareDialogProps)
|
||||
protected readonly props: UploadFirmwareDialogProps
|
||||
) {
|
||||
super({ title: UploadFirmware.Commands.OPEN.label || '' });
|
||||
this.contentNode.classList.add('firmware-uploader-dialog');
|
||||
this.acceptButton = undefined;
|
||||
}
|
||||
|
||||
get value(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
protected onAfterAttach(msg: Message): void {
|
||||
if (this.widget.isAttached) {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
this.widget.busyCallback = this.busyCallback.bind(this);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.widget.update();
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.widget.activate();
|
||||
}
|
||||
|
||||
protected handleEnter(event: KeyboardEvent): boolean | void {
|
||||
return false;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
this.widget.close();
|
||||
super.close();
|
||||
}
|
||||
|
||||
busyCallback(busy: boolean): void {
|
||||
this.busy = busy;
|
||||
if (busy) {
|
||||
this.closeCrossNode.classList.add('disabled');
|
||||
} else {
|
||||
this.closeCrossNode.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
}
|
@ -86,8 +86,13 @@ export namespace ArduinoMenus {
|
||||
|
||||
// -- Tools
|
||||
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
|
||||
// `Auto Format`, `Library Manager...`, `Boards Manager...`
|
||||
// `Auto Format`, `Archive Sketch`, `Manage Libraries...`, `Serial Monitor`
|
||||
export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main'];
|
||||
// `WiFi101 / WiFiNINA Firmware Updater`
|
||||
export const TOOLS__FIRMWARE_UPLOADER_GROUP = [
|
||||
...TOOLS,
|
||||
'1_firmware_uploader',
|
||||
];
|
||||
// `Board`, `Port`, and `Get Board Info`.
|
||||
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
|
||||
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
|
||||
@ -143,6 +148,11 @@ export namespace ArduinoMenus {
|
||||
...SKETCH_CONTROL__CONTEXT,
|
||||
'2_resources',
|
||||
];
|
||||
|
||||
// -- ROOT SSL CERTIFICATES
|
||||
export const ROOT_CERTIFICATES__CONTEXT = [
|
||||
'arduino-root-certificates--context',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,74 @@
|
||||
.certificate-uploader-dialog {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.certificate-uploader-dialog .theia-select {
|
||||
border: none !important;
|
||||
}
|
||||
.certificate-uploader-dialog .arduino-select__control {
|
||||
height: 31px;
|
||||
background: var(--theia-menubar-selectionBackground) !important;
|
||||
}
|
||||
|
||||
.certificate-uploader-dialog .dialogRow > button{
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.certificate-uploader-dialog .certificate-list {
|
||||
border: 1px solid #BDC7C7;
|
||||
border-radius: 2px;;
|
||||
background: var(--theia-menubar-selectionBackground) !important;
|
||||
overflow: auto;
|
||||
height: 120px;
|
||||
flex: 1;
|
||||
}
|
||||
.certificate-uploader-dialog .certificate-list .certificate-row {
|
||||
display: flex;
|
||||
padding: 6px 10px 5px 10px
|
||||
}
|
||||
.certificate-uploader-dialog .certificate-list .certificate-row:hover {
|
||||
background-color: var(--theia-list-activeSelectionBackground);
|
||||
}
|
||||
|
||||
.certificate-uploader-dialog .upload-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
.certificate-uploader-dialog .success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #1DA086;
|
||||
}
|
||||
|
||||
.certificate-uploader-dialog .warn {
|
||||
color: #C11F09;
|
||||
}
|
||||
|
||||
.certificate-uploader-dialog .status-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.certificate-uploader-dialog .add-cert-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.certificate-uploader-dialog .add-cert-btn .caret {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.certificate-add {
|
||||
padding: 16px;
|
||||
background-color: var(--theia-list-hoverBackground);
|
||||
border-radius: 3px;
|
||||
border: 1px solid #BDC7C7;
|
||||
}
|
||||
|
||||
.certificate-add input {
|
||||
margin-top: 12px;
|
||||
padding: 0 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
65
arduino-ide-extension/src/browser/style/dialogs.css
Normal file
65
arduino-ide-extension/src/browser/style/dialogs.css
Normal file
@ -0,0 +1,65 @@
|
||||
.p-Widget.dialogOverlay .dialogBlock {
|
||||
border-radius: 3px;
|
||||
padding: 0 28px;
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.25);
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogTitle {
|
||||
padding: 36px 0 28px;
|
||||
font-weight: 500;
|
||||
background-color: transparent;
|
||||
font-size: var(--theia-ui-font-size2);
|
||||
color: var(--theia-list-inactiveSelectionForeground);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogControl {
|
||||
padding: 0 0 36px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent > div {
|
||||
padding: 0 0 12px;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection {
|
||||
margin-top: 28px;
|
||||
}
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner {
|
||||
background: var(--theia-icon-loading) center center no-repeat;
|
||||
animation: theia-spin 1.25s linear infinite;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.fl1{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.fa.disabled {
|
||||
opacity: .4;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
.firmware-uploader-dialog {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.firmware-uploader-dialog .theia-select {
|
||||
border: none !important;
|
||||
}
|
||||
.firmware-uploader-dialog .arduino-select__control {
|
||||
height: 31px;
|
||||
background: var(--theia-menubar-selectionBackground) !important;
|
||||
}
|
||||
|
||||
.firmware-uploader-dialog .dialogRow > button{
|
||||
width: 33%;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.firmware-uploader-dialog #firmware-select {
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.firmware-uploader-dialog .success {
|
||||
color: #1DA086;
|
||||
}
|
||||
|
||||
.firmware-uploader-dialog .warn {
|
||||
color: #C11F09;
|
||||
}
|
||||
|
||||
.firmware-uploader-dialog .status-icon {
|
||||
margin-right: 10px;
|
||||
}
|
2384
arduino-ide-extension/src/browser/style/fonts.css
Normal file
2384
arduino-ide-extension/src/browser/style/fonts.css
Normal file
File diff suppressed because it is too large
Load Diff
685
arduino-ide-extension/src/browser/style/fonts/FontAwesome.svg
Normal file
685
arduino-ide-extension/src/browser/style/fonts/FontAwesome.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 667 KiB |
BIN
arduino-ide-extension/src/browser/style/fonts/FontAwesome.ttf
Normal file
BIN
arduino-ide-extension/src/browser/style/fonts/FontAwesome.ttf
Normal file
Binary file not shown.
BIN
arduino-ide-extension/src/browser/style/fonts/FontAwesome.woff
Normal file
BIN
arduino-ide-extension/src/browser/style/fonts/FontAwesome.woff
Normal file
Binary file not shown.
@ -1,15 +1,19 @@
|
||||
@import './list-widget.css';
|
||||
@import './boards-config-dialog.css';
|
||||
@import './main.css';
|
||||
@import './dialogs.css';
|
||||
@import './monitor.css';
|
||||
@import './arduino-select.css';
|
||||
@import './status-bar.css';
|
||||
@import './terminal.css';
|
||||
@import './editor.css';
|
||||
@import './settings-dialog.css';
|
||||
@import './firmware-uploader-dialog.css';
|
||||
@import './certificate-uploader-dialog.css';
|
||||
@import './debug.css';
|
||||
@import './sketchbook.css';
|
||||
@import './cloud-sketchbook.css';
|
||||
@import './fonts.css';
|
||||
|
||||
.theia-input.warning:focus {
|
||||
outline-width: 1px;
|
||||
|
@ -169,10 +169,6 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock {
|
||||
background-color: var(--theia-arduino-foreground);
|
||||
}
|
||||
|
||||
#arduino-open-sketch-control--toolbar--container {
|
||||
background-color: var(--theia-arduino-toolbar-background);
|
||||
}
|
||||
|
@ -50,7 +50,9 @@
|
||||
.additional-urls-dialog .link:hover {
|
||||
color: var(--theia-textLink-activeForeground);
|
||||
}
|
||||
|
||||
.arduino-settings-dialog .react-tabs__tab-panel {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
.arduino-settings-dialog .react-tabs__tab-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -0,0 +1 @@
|
||||
Sketchcache = is a cache that holds sketches and fileStat objects.
|
@ -15,7 +15,7 @@ import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||
import { CloudUserCommands } from '../../auth/cloud-user-commands';
|
||||
import { ShareSketchDialog } from '../../dialogs.ts/cloud-share-sketch-dialog';
|
||||
import { ShareSketchDialog } from '../../dialogs/cloud-share-sketch-dialog';
|
||||
import { CreateApi } from '../../create/create-api';
|
||||
import {
|
||||
PreferenceService,
|
||||
|
@ -25,7 +25,7 @@ import {
|
||||
LocalCacheUri,
|
||||
} from '../../local-cache/local-cache-fs-provider';
|
||||
import { CloudSketchbookCommands } from './cloud-sketchbook-contributions';
|
||||
import { DoNotAskAgainConfirmDialog } from '../../dialogs.ts/dialogs';
|
||||
import { DoNotAskAgainConfirmDialog } from '../../dialogs/do-not-ask-again-dialog';
|
||||
import { SketchbookTree } from '../sketchbook/sketchbook-tree';
|
||||
import { firstToUpperCase } from '../../../common/utils';
|
||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
|
@ -0,0 +1,17 @@
|
||||
export const ArduinoFirmwareUploaderPath =
|
||||
'/services/arduino-firmware-uploader';
|
||||
export const ArduinoFirmwareUploader = Symbol('ArduinoFirmwareUploader');
|
||||
export type FirmwareInfo = {
|
||||
board_name: string;
|
||||
board_fqbn: string;
|
||||
module: string;
|
||||
firmware_version: string;
|
||||
Latest: boolean;
|
||||
};
|
||||
export interface ArduinoFirmwareUploader {
|
||||
list(fqbn?: string): Promise<FirmwareInfo[]>;
|
||||
flash(firmware: FirmwareInfo, port: string): Promise<string>;
|
||||
uploadCertificates(command: string): Promise<any>;
|
||||
updatableBoards(): Promise<string[]>;
|
||||
availableFirmwares(fqbn: string): Promise<FirmwareInfo[]>;
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
export const ExecutableServicePath = '/services/executable-service';
|
||||
export const ExecutableService = Symbol('ExecutableService');
|
||||
export interface ExecutableService {
|
||||
list(): Promise<{ clangdUri: string; cliUri: string; lsUri: string }>;
|
||||
list(): Promise<{
|
||||
clangdUri: string;
|
||||
cliUri: string;
|
||||
lsUri: string;
|
||||
fwuploaderUri: string;
|
||||
}>;
|
||||
}
|
||||
|
@ -0,0 +1,80 @@
|
||||
import {
|
||||
ArduinoFirmwareUploader,
|
||||
FirmwareInfo,
|
||||
} from '../common/protocol/arduino-firmware-uploader';
|
||||
import { injectable, inject, named } from 'inversify';
|
||||
import { ExecutableService } from '../common/protocol';
|
||||
import { getExecPath, spawnCommand } from './exec-util';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
|
||||
@inject(ExecutableService)
|
||||
protected executableService: ExecutableService;
|
||||
|
||||
protected _execPath: string | undefined;
|
||||
|
||||
@inject(ILogger)
|
||||
@named('fwuploader')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
protected onError(error: any): void {
|
||||
this.logger.error(error);
|
||||
}
|
||||
|
||||
async getExecPath(): Promise<string> {
|
||||
if (this._execPath) {
|
||||
return this._execPath;
|
||||
}
|
||||
this._execPath = await getExecPath('arduino-fwuploader');
|
||||
return this._execPath;
|
||||
}
|
||||
|
||||
async runCommand(args: string[]): Promise<any> {
|
||||
const execPath = await this.getExecPath();
|
||||
return await spawnCommand(`"${execPath}"`, args, this.onError.bind(this));
|
||||
}
|
||||
|
||||
async uploadCertificates(command: string): Promise<any> {
|
||||
return await this.runCommand(['certificates', 'flash', command]);
|
||||
}
|
||||
|
||||
async list(fqbn?: string): Promise<FirmwareInfo[]> {
|
||||
const fqbnFlag = fqbn ? ['--fqbn', fqbn] : [];
|
||||
const firmwares: FirmwareInfo[] =
|
||||
JSON.parse(
|
||||
await this.runCommand([
|
||||
'firmware',
|
||||
'list',
|
||||
...fqbnFlag,
|
||||
'--format',
|
||||
'json',
|
||||
])
|
||||
) || [];
|
||||
return firmwares.reverse();
|
||||
}
|
||||
|
||||
async updatableBoards(): Promise<string[]> {
|
||||
return (await this.list()).reduce(
|
||||
(a, b) => (a.includes(b.board_fqbn) ? a : [...a, b.board_fqbn]),
|
||||
[] as string[]
|
||||
);
|
||||
}
|
||||
|
||||
async availableFirmwares(fqbn: string): Promise<FirmwareInfo[]> {
|
||||
return await this.list(fqbn);
|
||||
}
|
||||
|
||||
async flash(firmware: FirmwareInfo, port: string): Promise<string> {
|
||||
return await this.runCommand([
|
||||
'firmware',
|
||||
'flash',
|
||||
'--fqbn',
|
||||
firmware.board_fqbn,
|
||||
'--address',
|
||||
port,
|
||||
'--module',
|
||||
`${firmware.module}@${firmware.firmware_version}`,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
|
||||
import {
|
||||
ArduinoFirmwareUploader,
|
||||
ArduinoFirmwareUploaderPath,
|
||||
} from '../common/protocol/arduino-firmware-uploader';
|
||||
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import {
|
||||
BackendApplicationContribution,
|
||||
@ -80,6 +85,7 @@ import {
|
||||
AuthenticationServiceClient,
|
||||
AuthenticationServicePath,
|
||||
} from '../common/protocol/authentication-service';
|
||||
import { ArduinoFirmwareUploaderImpl } from './arduino-firmware-uploader-impl';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BackendApplication).toSelf().inSingletonScope();
|
||||
@ -245,6 +251,18 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
bind(ArduinoFirmwareUploaderImpl).toSelf().inSingletonScope();
|
||||
bind(ArduinoFirmwareUploader).toService(ArduinoFirmwareUploaderImpl);
|
||||
bind(BackendApplicationContribution).toService(ArduinoFirmwareUploaderImpl);
|
||||
bind(ConnectionHandler)
|
||||
.toDynamicValue(
|
||||
(context) =>
|
||||
new JsonRpcConnectionHandler(ArduinoFirmwareUploaderPath, () =>
|
||||
context.container.get(ArduinoFirmwareUploader)
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
// Logger for the Arduino daemon
|
||||
bind(ILogger)
|
||||
.toDynamicValue((ctx) => {
|
||||
@ -254,6 +272,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('daemon');
|
||||
|
||||
// Logger for the Arduino daemon
|
||||
bind(ILogger)
|
||||
.toDynamicValue((ctx) => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('fwuploader');
|
||||
})
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('fwuploader');
|
||||
|
||||
// Logger for the "serial discovery".
|
||||
bind(ILogger)
|
||||
.toDynamicValue((ctx) => {
|
||||
|
19
yarn.lock
19
yarn.lock
@ -1989,6 +1989,11 @@
|
||||
"@phosphor/signaling" "^1.3.1"
|
||||
"@phosphor/virtualdom" "^1.2.0"
|
||||
|
||||
"@popperjs/core@^2.8.3":
|
||||
version "2.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.3.tgz#8b68da1ebd7fc603999cf6ebee34a4899a14b88e"
|
||||
integrity sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==
|
||||
|
||||
"@primer/octicons-react@^9.0.0":
|
||||
version "9.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@primer/octicons-react/-/octicons-react-9.6.0.tgz#996f621cb063757a4985cd6b45e59ed00e3444bf"
|
||||
@ -2628,6 +2633,13 @@
|
||||
moment "2.24.0"
|
||||
valid-filename "^2.0.1"
|
||||
|
||||
"@tippyjs/react@^4.2.5":
|
||||
version "4.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.5.tgz#9b5837db93a1cac953962404df906aef1a18e80d"
|
||||
integrity sha512-YBLgy+1zznBNbx4JOoOdFXWMLXjBh9hLPwRtq3s8RRdrez2l3tPBRt2m2909wZd9S1KUeKjOOYYsnitccI9I3A==
|
||||
dependencies:
|
||||
tippy.js "^6.3.1"
|
||||
|
||||
"@types/anymatch@*":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
||||
@ -15616,6 +15628,13 @@ timers-browserify@^2.0.4:
|
||||
dependencies:
|
||||
setimmediate "^1.0.4"
|
||||
|
||||
tippy.js@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.1.tgz#3788a007be7015eee0fd589a66b98fb3f8f10181"
|
||||
integrity sha512-JnFncCq+rF1dTURupoJ4yPie5Cof978inW6/4S6kmWV7LL9YOSEVMifED3KdrVPEG+Z/TFH2CDNJcQEfaeuQww==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.8.3"
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
|
Loading…
x
Reference in New Issue
Block a user