mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-12 19:59:27 +00:00
[ATL-1533] Firmware&Certificate Uploader (#469)
Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import * as React from 'react';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Widget } from '@phosphor/widgets';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import { clipboard } from 'electron';
|
||||
import {
|
||||
AbstractDialog,
|
||||
ReactWidget,
|
||||
DialogProps,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { CreateApi } from '../create/create-api';
|
||||
|
||||
const RadioButton = (props: {
|
||||
id: string;
|
||||
changed: (evt: React.BaseSyntheticEvent) => void;
|
||||
value: string;
|
||||
isSelected: boolean;
|
||||
isDisabled: boolean;
|
||||
label: string;
|
||||
}) => {
|
||||
return (
|
||||
<p className="RadioButton">
|
||||
<input
|
||||
id={props.id}
|
||||
onChange={props.changed}
|
||||
value={props.value}
|
||||
type="radio"
|
||||
checked={props.isSelected}
|
||||
disabled={props.isDisabled}
|
||||
/>
|
||||
<label htmlFor={props.id}>{props.label}</label>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShareSketchComponent = ({
|
||||
treeNode,
|
||||
createApi,
|
||||
domain = 'https://create.arduino.cc',
|
||||
}: {
|
||||
treeNode: any;
|
||||
createApi: CreateApi;
|
||||
domain?: string;
|
||||
}): React.ReactElement => {
|
||||
const [loading, setloading] = React.useState<boolean>(false);
|
||||
|
||||
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {
|
||||
setloading(true);
|
||||
const sketch = await createApi.editSketch({
|
||||
id: treeNode.sketchId,
|
||||
params: {
|
||||
is_public: event.target.value === 'private' ? false : true,
|
||||
},
|
||||
});
|
||||
// setPublicVisibility(sketch.is_public);
|
||||
treeNode.isPublic = sketch.is_public;
|
||||
setloading(false);
|
||||
};
|
||||
|
||||
const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`;
|
||||
const embedLink = `<iframe src="${sketchLink}?embed" style="height:510px;width:100%;margin:10px 0" frameborder=0></iframe>`;
|
||||
|
||||
return (
|
||||
<div id="widget-container arduino-sharesketch-dialog">
|
||||
<p>Choose visibility of your Sketch:</p>
|
||||
<RadioButton
|
||||
changed={radioChangeHandler}
|
||||
id="1"
|
||||
isSelected={treeNode.isPublic === false}
|
||||
label="Private. Only you can view the Sketch."
|
||||
value="private"
|
||||
isDisabled={loading}
|
||||
/>
|
||||
<RadioButton
|
||||
changed={radioChangeHandler}
|
||||
id="2"
|
||||
isSelected={treeNode.isPublic === true}
|
||||
label="Public. Anyone with the link can view the Sketch."
|
||||
value="public"
|
||||
isDisabled={loading}
|
||||
/>
|
||||
|
||||
{treeNode.isPublic && (
|
||||
<div>
|
||||
<p>Link:</p>
|
||||
<div className="sketch-link">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={sketchLink}
|
||||
className="theia-input"
|
||||
/>
|
||||
<button
|
||||
onClick={() => clipboard.writeText(sketchLink)}
|
||||
value="copy"
|
||||
className="theia-button secondary"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p>Embed:</p>
|
||||
<div className="sketch-link-embed">
|
||||
<textarea
|
||||
readOnly
|
||||
value={embedLink}
|
||||
className="theia-input stretch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class ShareSketchWidget extends ReactWidget {
|
||||
constructor(private treeNode: any, private createApi: CreateApi) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return (
|
||||
<ShareSketchComponent
|
||||
treeNode={this.treeNode}
|
||||
createApi={this.createApi}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ShareSketchDialogProps extends DialogProps {
|
||||
readonly node: any;
|
||||
readonly createApi: CreateApi;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ShareSketchDialog extends AbstractDialog<void> {
|
||||
protected widget: ShareSketchWidget;
|
||||
|
||||
constructor(
|
||||
@inject(ShareSketchDialogProps)
|
||||
protected readonly props: ShareSketchDialogProps
|
||||
) {
|
||||
super({ title: props.title });
|
||||
this.contentNode.classList.add('arduino-share-sketch-dialog');
|
||||
this.widget = new ShareSketchWidget(props.node, props.createApi);
|
||||
}
|
||||
|
||||
get value(): void {
|
||||
return;
|
||||
}
|
||||
protected onAfterAttach(msg: Message): void {
|
||||
if (this.widget.isAttached) {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Widget } from '@phosphor/widgets';
|
||||
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
||||
import {
|
||||
ConfirmDialog,
|
||||
ConfirmDialogProps,
|
||||
DialogError,
|
||||
} from '@theia/core/lib/browser/dialogs';
|
||||
|
||||
@injectable()
|
||||
export class DoNotAskAgainDialogProps extends ConfirmDialogProps {
|
||||
readonly onAccept: () => Promise<void>;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
|
||||
protected readonly doNotAskAgainCheckbox: HTMLInputElement;
|
||||
|
||||
constructor(
|
||||
@inject(DoNotAskAgainDialogProps)
|
||||
protected readonly props: DoNotAskAgainDialogProps
|
||||
) {
|
||||
super(props);
|
||||
this.controlPanel.removeChild(this.errorMessageNode);
|
||||
const doNotAskAgainNode = document.createElement('div');
|
||||
doNotAskAgainNode.setAttribute('style', 'flex: 2');
|
||||
this.controlPanel.insertBefore(
|
||||
doNotAskAgainNode,
|
||||
this.controlPanel.firstChild
|
||||
);
|
||||
const doNotAskAgainLabel = document.createElement('label');
|
||||
doNotAskAgainLabel.classList.add('flex-line');
|
||||
doNotAskAgainNode.appendChild(doNotAskAgainLabel);
|
||||
doNotAskAgainLabel.textContent = "Don't ask again";
|
||||
this.doNotAskAgainCheckbox = document.createElement('input');
|
||||
this.doNotAskAgainCheckbox.setAttribute('align-self', 'center');
|
||||
doNotAskAgainLabel.appendChild(this.doNotAskAgainCheckbox);
|
||||
this.doNotAskAgainCheckbox.type = 'checkbox';
|
||||
}
|
||||
|
||||
protected async accept(): Promise<void> {
|
||||
if (!this.resolve) {
|
||||
return;
|
||||
}
|
||||
this.acceptCancellationSource.cancel();
|
||||
this.acceptCancellationSource = new CancellationTokenSource();
|
||||
const token = this.acceptCancellationSource.token;
|
||||
const value = this.value;
|
||||
const error = await this.isValid(value, 'open');
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
if (!DialogError.getResult(error)) {
|
||||
this.setErrorMessage(error);
|
||||
} else {
|
||||
if (this.doNotAskAgainCheckbox.checked) {
|
||||
await this.props.onAccept();
|
||||
}
|
||||
this.resolve(value);
|
||||
Widget.detach(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected setErrorMessage(error: DialogError): void {
|
||||
if (this.acceptButton) {
|
||||
this.acceptButton.disabled = !DialogError.getResult(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user