From 68b1f8d4f29d42fd138e52c10e38f65a9b1a65a3 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Fri, 29 Jan 2021 09:09:38 +0100 Subject: [PATCH] Implemented the Network tab. Signed-off-by: Akos Kitta --- arduino-ide-extension/package.json | 5 +- .../src/browser/settings.tsx | 198 +++++++++++++++++- .../src/browser/style/settings-dialog.css | 22 +- .../src/common/protocol/config-service.ts | 71 ++++++- arduino-ide-extension/src/node/cli-config.ts | 5 + .../src/node/config-service-impl.ts | 14 +- yarn.lock | 24 ++- 7 files changed, 322 insertions(+), 17 deletions(-) diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index dc7416ed..ffaab8c3 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -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" } } } diff --git a/arduino-ide-extension/src/browser/settings.tsx b/arduino-ide-extension/src/browser/settings.tsx index 1d3e78b2..2c1b2ba8 100644 --- a/arduino-ide-extension/src/browser/settings.tsx +++ b/arduino-ide-extension/src/browser/settings.tsx @@ -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('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; } + return + + Settings + Network + + + {this.renderSettings()} + + + {this.renderNetwork()} + + ; + } + + protected renderSettings(): React.ReactNode { return
Sketchbook location:
@@ -343,12 +364,106 @@ export class SettingsComponent extends React.Component; } + protected renderNetwork(): React.ReactNode { + return
+
+ + +
+ {this.renderProxySettings()} +
; + } + + protected renderProxySettings(): React.ReactNode { + const disabled = this.state.network === 'none'; + return +
+
+ + HTTP + +
+
+
+
Host name:
+
Port number:
+
Username:
+
Password:
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
; + } + + private isControlKey(event: React.KeyboardEvent): boolean { + return !!event.key && ['tab', 'delete', 'backspace', 'arrowleft', 'arrowright'].some(key => event.key.toLocaleLowerCase() === key); + } + protected noopKeyDown = (event: React.KeyboardEvent) => { + if (this.isControlKey(event)) { + return; + } event.nativeEvent.preventDefault(); event.nativeEvent.returnValue = false; } protected numbersOnlyKeyDown = (event: React.KeyboardEvent) => { + 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) => { + if (event.target.checked) { + this.setState({ network: 'none' }); + } else { + this.setState({ network: Network.Default() }); + } + }; + + protected manualProxyDidChange = (event: React.ChangeEvent) => { + if (event.target.checked) { + this.setState({ network: Network.Default() }); + } else { + this.setState({ network: 'none' }); + } + }; + + protected httpProtocolDidChange = (event: React.ChangeEvent) => { + if (this.state.network !== 'none') { + const network = this.cloneProxySettings; + network.protocol = event.target.checked ? 'http' : 'socks'; + this.setState({ network }); + } + }; + + protected socksProtocolDidChange = (event: React.ChangeEvent) => { + if (this.state.network !== 'none') { + const network = this.cloneProxySettings; + network.protocol = event.target.checked ? 'socks' : 'http'; + this.setState({ network }); + } + }; + + protected hostnameDidChange = (event: React.ChangeEvent) => { + if (this.state.network !== 'none') { + const network = this.cloneProxySettings; + network.hostname = event.target.value; + this.setState({ network }); + } + }; + + protected portDidChange = (event: React.ChangeEvent) => { + if (this.state.network !== 'none') { + const network = this.cloneProxySettings; + network.port = event.target.value; + this.setState({ network }); + } + }; + + protected usernameDidChange = (event: React.ChangeEvent) => { + if (this.state.network !== 'none') { + const network = this.cloneProxySettings; + network.username = event.target.value; + this.setState({ network }); + } + }; + + protected passwordDidChange = (event: React.ChangeEvent) => { + 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 { diff --git a/arduino-ide-extension/src/browser/style/settings-dialog.css b/arduino-ide-extension/src/browser/style/settings-dialog.css index 74c5f7be..126d089c 100644 --- a/arduino-ide-extension/src/browser/style/settings-dialog.css +++ b/arduino-ide-extension/src/browser/style/settings-dialog.css @@ -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; +} diff --git a/arduino-ide-extension/src/common/protocol/config-service.ts b/arduino-ide-extension/src/common/protocol/config-service.ts index cc130109..929274e2 100644 --- a/arduino-ide-extension/src/common/protocol/config-service.ts +++ b/arduino-ide-extension/src/common/protocol/config-service.ts @@ -8,11 +8,79 @@ export interface ConfigService { isInSketchDir(uri: string): Promise; } +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); } } diff --git a/arduino-ide-extension/src/node/cli-config.ts b/arduino-ide-extension/src/node/cli-config.ts index 07686130..e4321366 100644 --- a/arduino-ide-extension/src/node/cli-config.ts +++ b/arduino-ide-extension/src/node/cli-config.ts @@ -92,11 +92,16 @@ export namespace Logging { } +export interface Network { + proxy?: string; +} + // Arduino CLI config scheme export interface CliConfig { board_manager?: RecursivePartial; directories?: RecursivePartial; logging?: RecursivePartial; + network?: RecursivePartial; } // Bare minimum required CLI config. diff --git a/arduino-ide-extension/src/node/config-service-impl.ts b/arduino-ide-extension/src/node/config-service-impl.ts index 2f5035ac..925f0103 100644 --- a/arduino-ide-extension/src/node/config-service-impl.ts +++ b/arduino-ide-extension/src/node/config-service-impl.ts @@ -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 { 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((resolve, reject) => { client.merge(data, error => { try { diff --git a/yarn.lock b/yarn.lock index 73e10cdc..325a1332 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"