Implemented the Network tab.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2021-01-29 09:09:38 +01:00 committed by Akos Kitta
parent e957ac4331
commit 68b1f8d4f2
7 changed files with 322 additions and 17 deletions

View File

@ -41,6 +41,7 @@
"@types/ncp": "^2.0.4",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2",
"@types/sinon": "^7.5.2",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
@ -56,7 +57,9 @@
"ncp": "^2.0.0",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"react-disable": "^0.1.0",
"react-select": "^3.0.4",
"react-tabs": "^3.1.2",
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
"temp": "^0.9.1",
@ -120,7 +123,7 @@
],
"arduino": {
"cli": {
"version": "0.15.0-rc1"
"version": "20210203"
}
}
}

View File

@ -2,6 +2,9 @@ import * as React from 'react';
import { injectable, inject, postConstruct } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
import { Disable } from 'react-disable';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { Deferred } from '@theia/core/lib/common/promise-util';
@ -15,7 +18,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { AbstractDialog, DialogProps, PreferenceService, PreferenceScope, DialogError, ReactWidget } from '@theia/core/lib/browser';
import { Index } from '../common/types';
import { ConfigService, FileSystemExt } from '../common/protocol';
import { ConfigService, FileSystemExt, Network, ProxySettings } from '../common/protocol';
export interface Settings extends Index {
editorFontSize: number; // `editor.fontSize`
@ -32,6 +35,7 @@ export interface Settings extends Index {
sketchbookPath: string; // CLI
additionalUrls: string[]; // CLI
network: Network; // CLI
}
export namespace Settings {
@ -40,7 +44,6 @@ export namespace Settings {
}
}
export type SettingsKey = keyof Settings;
@injectable()
export class SettingsService {
@ -101,7 +104,7 @@ export class SettingsService {
this.preferenceService.get<boolean>('arduino.language.log', true),
this.configService.getConfiguration()
]);
const { additionalUrls, sketchDirUri } = cliConfig;
const { additionalUrls, sketchDirUri, network } = cliConfig;
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
return {
editorFontSize,
@ -115,7 +118,8 @@ export class SettingsService {
verifyAfterUpload,
enableLsLogs,
additionalUrls,
sketchbookPath
sketchbookPath,
network
};
}
@ -175,7 +179,8 @@ export class SettingsService {
verifyAfterUpload,
enableLsLogs,
sketchbookPath,
additionalUrls
additionalUrls,
network
} = this._settings;
const [config, sketchDirUri] = await Promise.all([
this.configService.getConfiguration(),
@ -183,6 +188,7 @@ export class SettingsService {
]);
(config as any).additionalUrls = additionalUrls;
(config as any).sketchDirUri = sketchDirUri;
(config as any).network = network;
await Promise.all([
this.preferenceService.set('editor.fontSize', editorFontSize, PreferenceScope.User),
@ -230,6 +236,21 @@ export class SettingsComponent extends React.Component<SettingsComponent.Props,
if (!this.state) {
return <div />;
}
return <Tabs>
<TabList>
<Tab>Settings</Tab>
<Tab>Network</Tab>
</TabList>
<TabPanel>
{this.renderSettings()}
</TabPanel>
<TabPanel>
{this.renderNetwork()}
</TabPanel>
</Tabs>;
}
protected renderSettings(): React.ReactNode {
return <div className='content noselect'>
Sketchbook location:
<div className='flex-line'>
@ -343,12 +364,106 @@ export class SettingsComponent extends React.Component<SettingsComponent.Props,
</div>;
}
protected renderNetwork(): React.ReactNode {
return <div className='content noselect'>
<form>
<label className='flex-line'>
<input
type='radio'
checked={this.state.network === 'none'}
onChange={this.noProxyDidChange} />
No proxy
</label>
<label className='flex-line'>
<input
type='radio'
checked={this.state.network !== 'none'}
onChange={this.manualProxyDidChange} />
Manual proxy configuration
</label>
</form>
{this.renderProxySettings()}
</div>;
}
protected renderProxySettings(): React.ReactNode {
const disabled = this.state.network === 'none';
return <Disable disabled={disabled}>
<div className='proxy-settings' aria-disabled={disabled}>
<form className='flex-line'>
<input
type='radio'
checked={this.state.network === 'none' ? true : this.state.network.protocol === 'http'}
onChange={this.httpProtocolDidChange} />
HTTP
<label className='flex-line'>
<input
type='radio'
checked={this.state.network === 'none' ? false : this.state.network.protocol !== 'http'}
onChange={this.socksProtocolDidChange} />
SOCKS
</label>
</form>
<div className='flex-line proxy-settings'>
<div className='column'>
<div className='flex-line'>Host name:</div>
<div className='flex-line'>Port number:</div>
<div className='flex-line'>Username:</div>
<div className='flex-line'>Password:</div>
</div>
<div className='column stretch'>
<div className='flex-line'>
<input
className='theia-input stretch with-margin'
type='text'
value={this.state.network === 'none' ? '' : this.state.network.hostname}
onChange={this.hostnameDidChange} />
</div>
<div className='flex-line'>
<input
className='theia-input small with-margin'
type='number'
pattern='[0-9]'
value={this.state.network === 'none' ? '' : this.state.network.port}
onKeyDown={this.numbersOnlyKeyDown}
onChange={this.portDidChange} />
</div>
<div className='flex-line'>
<input
className='theia-input stretch with-margin'
type='text'
value={this.state.network === 'none' ? '' : this.state.network.username}
onChange={this.usernameDidChange} />
</div>
<div className='flex-line'>
<input
className='theia-input stretch with-margin'
type='password'
value={this.state.network === 'none' ? '' : this.state.network.password}
onChange={this.passwordDidChange} />
</div>
</div>
</div>
</div>
</Disable>;
}
private isControlKey(event: React.KeyboardEvent<HTMLInputElement>): boolean {
return !!event.key && ['tab', 'delete', 'backspace', 'arrowleft', 'arrowright'].some(key => event.key.toLocaleLowerCase() === key);
}
protected noopKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (this.isControlKey(event)) {
return;
}
event.nativeEvent.preventDefault();
event.nativeEvent.returnValue = false;
}
protected numbersOnlyKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (this.isControlKey(event)) {
return;
}
const key = Number(event.key)
if (isNaN(key) || event.key === null || event.key === ' ') {
event.nativeEvent.preventDefault();
@ -444,6 +559,79 @@ export class SettingsComponent extends React.Component<SettingsComponent.Props,
}
};
protected noProxyDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
this.setState({ network: 'none' });
} else {
this.setState({ network: Network.Default() });
}
};
protected manualProxyDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
this.setState({ network: Network.Default() });
} else {
this.setState({ network: 'none' });
}
};
protected httpProtocolDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'http' : 'socks';
this.setState({ network });
}
};
protected socksProtocolDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'socks' : 'http';
this.setState({ network });
}
};
protected hostnameDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.hostname = event.target.value;
this.setState({ network });
}
};
protected portDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.port = event.target.value;
this.setState({ network });
}
};
protected usernameDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.username = event.target.value;
this.setState({ network });
}
};
protected passwordDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.password = event.target.value;
this.setState({ network });
}
};
private get cloneProxySettings(): ProxySettings {
const { network } = this.state;
if (network === 'none') {
throw new Error('Must be called when proxy is enabled.');
}
const copyNetwork = deepClone(network);
return copyNetwork;
}
}
export namespace SettingsComponent {
export interface Props {

View File

@ -4,6 +4,7 @@
.arduino-settings-dialog .content {
padding: 5px;
height: 250px;
}
.arduino-settings-dialog .flex-line {
@ -25,19 +26,32 @@
vertical-align: middle;
}
.arduino-settings-dialog .stretch {
width: 100% !important;
}
.arduino-settings-dialog .flex-line .theia-button.shrink {
min-width: unset;
}
.arduino-settings-dialog .theia-input.stretch {
width: 100% !important;
.arduino-settings-dialog .proxy-settings {
margin: 5px;
}
.arduino-settings-dialog input[type="radio"] {
margin: 3px !important;
}
.arduino-settings-dialog .theia-input.small {
max-width: 40px;
width: 40px;
max-width: 50px;
width: 50px;
}
.additional-urls-dialog .link:hover {
color: var(--theia-textLink-activeForeground);
}
.arduino-settings-dialog .react-tabs__tab-list {
display: flex;
justify-content: center;
}

View File

@ -8,11 +8,79 @@ export interface ConfigService {
isInSketchDir(uri: string): Promise<boolean>;
}
export interface ProxySettings {
protocol: string;
hostname: string;
port: string;
username: string;
password: string;
}
export type Network = 'none' | ProxySettings;
export namespace Network {
export function Default(): Network {
return {
protocol: 'http',
hostname: '',
port: '',
username: '',
password: ''
}
}
export function parse(raw: string | undefined): Network {
if (!raw) {
return 'none';
}
try {
// Patter: PROTOCOL://USER:PASS@HOSTNAME:PORT/
const { protocol, hostname, password, username, port } = new URL(raw);
return {
protocol,
hostname,
password,
username,
port
};
} catch {
return 'none';
}
};
export function stringify(network: Network): string | undefined {
if (network === 'none') {
return undefined;
}
const { protocol, hostname, password, username, port } = network;
try {
const defaultUrl = new URL(`${protocol ? protocol : 'http'}://${hostname ? hostname : '_'}`);
return Object.assign(defaultUrl, { protocol, hostname, password, username, port }).toString();
} catch {
return undefined;
}
}
export function sameAs(left: Network, right: Network): boolean {
if (left === 'none') {
return right === 'none';
}
if (right === 'none') {
return false;
}
return left.hostname === right.hostname
&& left.password === right.password
&& left.protocol === right.protocol
&& left.username === right.username;
};
}
export interface Config {
readonly sketchDirUri: string;
readonly dataDirUri: string;
readonly downloadsDirUri: string;
readonly additionalUrls: string[];
readonly network: Network;
}
export namespace Config {
export function sameAs(left: Config, right: Config): boolean {
@ -28,6 +96,7 @@ export namespace Config {
}
return left.dataDirUri === right.dataDirUri
&& left.downloadsDirUri === right.downloadsDirUri
&& left.sketchDirUri === right.sketchDirUri;
&& left.sketchDirUri === right.sketchDirUri
&& Network.sameAs(left.network, right.network);
}
}

View File

@ -92,11 +92,16 @@ export namespace Logging {
}
export interface Network {
proxy?: string;
}
// Arduino CLI config scheme
export interface CliConfig {
board_manager?: RecursivePartial<BoardManager>;
directories?: RecursivePartial<Directories>;
logging?: RecursivePartial<Logging>;
network?: RecursivePartial<Network>;
}
// Bare minimum required CLI config.

View File

@ -11,9 +11,9 @@ import { ILogger } from '@theia/core/lib/common/logger';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { ConfigService, Config, NotificationServiceServer } from '../common/protocol';
import { ConfigService, Config, NotificationServiceServer, Network } from '../common/protocol';
import { spawnCommand } from './exec-util';
import { RawData, WriteRequest } from './cli-protocol/settings/settings_pb';
import { WriteRequest, RawData } from './cli-protocol/settings/settings_pb';
import { SettingsClient } from './cli-protocol/settings/settings_grpc_pb';
import * as serviceGrpcPb from './cli-protocol/settings/settings_grpc_pb';
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
@ -78,7 +78,7 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
if (!copyDefaultCliConfig) {
copyDefaultCliConfig = await this.getFallbackCliConfig();
}
const { additionalUrls, dataDirUri, downloadsDirUri, sketchDirUri } = config;
const { additionalUrls, dataDirUri, downloadsDirUri, sketchDirUri, network } = config;
copyDefaultCliConfig.directories = {
data: FileUri.fsPath(dataDirUri),
downloads: FileUri.fsPath(downloadsDirUri),
@ -89,6 +89,8 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
...additionalUrls
]
};
const proxy = Network.stringify(network);
copyDefaultCliConfig.network = { proxy };
const { port } = copyDefaultCliConfig.daemon;
await this.updateDaemon(port, copyDefaultCliConfig);
await this.writeDaemonState(port);
@ -175,11 +177,13 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
if (cliConfig.board_manager && cliConfig.board_manager.additional_urls) {
additionalUrls.push(...Array.from(new Set(cliConfig.board_manager.additional_urls)));
}
const network = Network.parse(cliConfig.network?.proxy);
return {
dataDirUri: FileUri.create(data).toString(),
sketchDirUri: FileUri.create(user).toString(),
downloadsDirUri: FileUri.create(downloads).toString(),
additionalUrls,
network
};
}
@ -195,7 +199,9 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
protected async updateDaemon(port: string | number, config: DefaultCliConfig): Promise<void> {
const client = this.createClient(port);
const data = new RawData();
data.setJsondata(JSON.stringify(config, null, 2));
const json = JSON.stringify(config, null, 2);
data.setJsondata(json);
console.log(`Updating daemon with 'data': ${json}`);
return new Promise<void>((resolve, reject) => {
client.merge(data, error => {
try {

View File

@ -2825,6 +2825,13 @@
"@types/react-dom" "*"
"@types/react-transition-group" "*"
"@types/react-tabs@^2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@types/react-tabs/-/react-tabs-2.3.2.tgz#99fb6866bbc6912d44f7bbc99eca03fbbd217960"
integrity sha512-QfMelaJSdMcp+CenKhATp12XFFqqUcLXILgwpX3dgWfVYNZPtsLXZDDCRsVn+kwmBOWB+DFPKpQorxbUhnXINw==
dependencies:
"@types/react" "*"
"@types/react-transition-group@*":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d"
@ -5527,7 +5534,7 @@ cloneable-readable@^1.0.0:
process-nextick-args "^2.0.0"
readable-stream "^2.3.5"
clsx@^1.0.4:
clsx@^1.0.4, clsx@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@ -12290,7 +12297,7 @@ promzard@^0.3.0:
dependencies:
read "1"
prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.0, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -12560,6 +12567,11 @@ react-autosize-textarea@^7.0.0:
line-height "^0.3.1"
prop-types "^15.5.6"
react-disable@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/react-disable/-/react-disable-0.1.0.tgz#e3474aefcb2b91fcb534693c66b7497fb28b85af"
integrity sha512-3RbSYuUtakIy1ulCfDf6yoJAsjHFNLv467bSBwyxkNDyez/z7CbcTZ6QUWCrhfybvfm81IN6QT3pUfDoKcvaFQ==
react-dom@^16.8.0:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
@ -12609,6 +12621,14 @@ react-select@^3.0.4:
react-input-autosize "^2.2.2"
react-transition-group "^4.3.0"
react-tabs@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.1.2.tgz#9047ad7d8a53d357a67c14ad4c4a64cc16660fa8"
integrity sha512-OKS1l7QzSNcn+L2uFsxyGFHdXp9YsPGf/YOURWcImp7xLN36n0Wz+/j9HwlwGtlXCZexwshScR5BrcFbw/3P9Q==
dependencies:
clsx "^1.1.0"
prop-types "^15.5.0"
react-transition-group@^4.3.0:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"