Commit c8b4a337 authored by zeroleak's avatar zeroleak
Browse files

add GUI proxy + easier remote CLI initialization

parent 83748ebd
......@@ -59,6 +59,7 @@ export const STORE_CLILOCAL = 'cli.local';
export const CLI_LOG_FILE = computeLogPath('whirlpool-cli.log');
export const CLI_LOG_ERROR_FILE = computeLogPath('whirlpool-cli.error.log');
export const GUI_LOG_FILE = logger.getFile();
export const GUI_CONFIG_FILENAME = 'config.json';
export const CLI_CONFIG_FILENAME = 'whirlpool-cli-config.properties';
const app = electron.app || electron.remote.app;
......
......@@ -96,26 +96,22 @@ export default class ConfigPage extends Component<Props> {
</div>
<Card>
<Card.Header>General configuration</Card.Header>
<Card.Header>CLI General configuration</Card.Header>
<Card.Body>
<div className="form-group row">
<label htmlFor="mixsTarget" className="col-sm-2 col-form-label">Mixs target min</label>
<div className="col-sm-10">
<div className='row'>
<input type="number" className='form-control col-sm-1' onChange={e => {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.mixsTarget = myValue)
}} defaultValue={cliConfig.mix.mixsTarget} id="mixsTarget"/>
<label className='col-form-label col-sm-11'>Minimum number of mixs to achieve per UTXO</label>
</div>
</div>
<input type="number" className='form-control col-sm-3' onChange={e => {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.mixsTarget = myValue)
}} defaultValue={cliConfig.mix.mixsTarget} id="mixsTarget"/>
<label className='col-form-label col-sm-5 text-muted'>Minimum number of mixs to achieve per UTXO</label>
</div>
<div className="form-group row">
<label htmlFor="autoMix" className="col-sm-2 col-form-label">Auto-MIX</label>
<div className="col-sm-10 custom-control custom-switch">
<input type="checkbox" className="custom-control-input" onChange={e => myThis.onChangeCliConfig(cliConfig => cliConfig.mix.autoMix = checked(e))} defaultChecked={cliConfig.mix.autoMix} id="autoMix"/>
<label className="custom-control-label" htmlFor="autoMix">Automatically QUEUE premix & postmix</label>
<label className="custom-control-label" htmlFor="autoMix">Automatically mix premix & postmix</label>
</div>
</div>
......@@ -144,90 +140,72 @@ export default class ConfigPage extends Component<Props> {
</div>
<div className="form-group row">
<label htmlFor="proxy" className="col-sm-2 col-form-label">Proxy</label>
<div className="col-sm-10">
<div className='row'>
<input type="text" className='form-control col-sm-4' onChange={e => {
const myValue = e.target.value
myThis.onChangeCliConfig(cliConfig => cliConfig.proxy = myValue)
}} defaultValue={cliConfig.proxy} id="proxy"/>
<label className='col-form-label col-sm-8'>
Use SOCKS or HTTP proxy.<br/>
<small>socks://host:port or http://host:port</small>
</label>
</div>
</div>
<label htmlFor="scode" className="col-sm-2 col-form-label">SCODE</label>
<input type="text" className='form-control col-sm-3' onChange={e => {
const myValue = e.target.value
myThis.onChangeCliConfig(cliConfig => cliConfig.scode = myValue)
}} defaultValue={cliConfig.scode} id="scode"/>
<label className='col-form-label col-sm-5 text-muted'>A Samourai Discount Code for reduced-cost mixing.</label>
</div>
</Card.Body>
</Card>
<small><a onClick={this.toogleDevelopersConfig} style={{cursor:'pointer'}}>Toggle developers settings</a></small><br/>
<br/>
{this.state.showDevelopersConfig && <Card>
<Card.Header>Developers settings</Card.Header>
<Card.Header>CLI Developers settings</Card.Header>
<Card.Body>
<div className="form-group row">
<label htmlFor="server" className="col-sm-2 col-form-label">Server</label>
<select className="col-sm-8 form-control" id="server" onChange={e => {
const myValue = e.target.value
myThis.onChangeCliConfig(cliConfig => cliConfig.server = myValue)
}} defaultValue={cliConfig.server}>
{Object.keys(WHIRLPOOL_SERVER).map((value) => <option value={value} key={value}>{WHIRLPOOL_SERVER[value]}</option>)}
</select>
<label htmlFor="clientDelay" className="col-sm-2 col-form-label">Client delay</label>
<input type="number" className='form-control col-sm-3' onChange={e => {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.clientDelay = myValue)
}} defaultValue={cliConfig.mix.clientDelay} id="clientDelay"/>
<label className='col-form-label col-sm-5 text-muted'>Delay (in seconds) between each client connection</label>
</div>
<div className="form-group row">
<label htmlFor="clientDelay" className="col-sm-2 col-form-label">Client delay</label>
<div className="col-sm-10">
<div className='row'>
<input type="number" className='form-control col-sm-1' onChange={e => {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.clientDelay = myValue)
}} defaultValue={cliConfig.mix.clientDelay} id="clientDelay"/>
<label className='col-form-label col-sm-11'>Delay (in seconds) between each client connection</label>
</div>
</div>
<label htmlFor="tx0MaxOutputs" className="col-sm-2 col-form-label">TX0 max outputs</label>
<input type="number" className='form-control col-sm-3' onChange={e => {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.tx0MaxOutputs = myValue)
}} defaultValue={cliConfig.mix.tx0MaxOutputs} id="tx0MaxOutputs"/>
<label className='col-form-label col-sm-5 text-muted'>Max premixes per TX0 (0 = no limit)</label>
</div>
{clientsPerPoolEditable && <div className="form-group row">
<label htmlFor="clientsPerPool" className="col-sm-2 col-form-label">Max clients per pool</label>
<input type="number" className='form-control col-sm-3' onChange={e => {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.clientsPerPool = myValue)
}} defaultValue={cliConfig.mix.clientsPerPool} id="clientsPerPool"/>
<label className='col-form-label col-sm-5 text-muted'>Max simultaneous mixing clients per pool</label>
</div>}
<div className="form-group row">
<label htmlFor="tx0MaxOutputs" className="col-sm-2 col-form-label">TX0 max outputs</label>
<div className="col-sm-10">
<div className='row'>
<input type="number" className='form-control col-sm-1' onChange={e => {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.tx0MaxOutputs = myValue)
}} defaultValue={cliConfig.mix.tx0MaxOutputs} id="tx0MaxOutputs"/>
<label className='col-form-label col-sm-11'>Max premixes per TX0 (0 = no limit)</label>
</div>
</div>
<label htmlFor="proxy" className="col-sm-2 col-form-label">CLI proxy</label>
<input type="text" className='form-control col-sm-3' onChange={e => {
const myValue = e.target.value
myThis.onChangeCliConfig(cliConfig => cliConfig.proxy = myValue)
}} defaultValue={cliConfig.proxy} id="proxy"/>
<label className='col-form-label col-sm-7 text-muted'>
Set it only when blocked by a firewall (this is not <i>GUI Tor proxy</i>).<br/>
<code>socks://host:port</code> or <code>http://host:port</code>
</label>
</div>
<div className="form-group row">
<label htmlFor="scode" className="col-sm-2 col-form-label">SCODE</label>
<div className="col-sm-10">
<div className='row'>
<input type="text" className='form-control col-sm-2' onChange={e => {
const myValue = e.target.value
myThis.onChangeCliConfig(cliConfig => cliConfig.scode = myValue)
}} defaultValue={cliConfig.scode} id="scode"/>
<label className='col-form-label col-sm-11'>A Samourai Discount Code for reduced-cost mixing.</label>
</div>
</div>
<label htmlFor="server" className="col-sm-2 col-form-label">Server</label>
<select className="col-sm-3 form-control" id="server" onChange={e => {
const myValue = e.target.value
myThis.onChangeCliConfig(cliConfig => cliConfig.server = myValue)
}} defaultValue={cliConfig.server}>
{Object.keys(WHIRLPOOL_SERVER).map((value) => <option value={value} key={value}>{WHIRLPOOL_SERVER[value]}</option>)}
</select>
</div>
{clientsPerPoolEditable && <div className="form-group row">
<label htmlFor="clientsPerPool" className="col-sm-2 col-form-label">Max clients per pool</label>
<div className="col-sm-10">
<div className='row'>
<input type="number" className='form-control col-sm-1' onChange={e => {
const myValue = parseInt(e.target.value)
myThis.onChangeCliConfig(cliConfig => cliConfig.mix.clientsPerPool = myValue)
}} defaultValue={cliConfig.mix.clientsPerPool} id="clientsPerPool"/>
<label className='col-form-label col-sm-11'>Max simultaneous mixing clients per pool</label>
</div>
</div>
</div>}
</Card.Body>
</Card>}
<br/>
......
// @flow
import React, { Component } from 'react';
import { Alert } from 'react-bootstrap';
import { Alert, Card } from 'react-bootstrap';
import { connect } from 'react-redux';
import { ipcRenderer } from "electron";
import { ipcRenderer } from 'electron';
import * as Icons from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import WebcamPayloadModal from '../components/Modals/WebcamPayloadModal';
import cliService from '../services/cliService';
import { CLI_CONFIG_FILENAME, DEFAULT_CLIPORT, IPC_CAMERA } from '../const';
import { DEFAULT_CLIPORT, IPC_CAMERA } from '../const';
import { cliLocalService } from '../services/cliLocalService';
import utils from '../services/utils';
import guiConfig from '../mainProcess/guiConfig';
const STEP_LAST = 3
const DEFAULT_CLIHOST = 'https://my-remote-CLI'
const DEFAULT_CLIHOSTPORT = 'https://myRemoteCLI:'+DEFAULT_CLIPORT
const DEFAULT_APIKEY = ''
const DEFAUT_GUI_PROXY = 'socks5://127.0.0.1:9050'
const CLILOCAL_URL = 'https://localhost:'+DEFAULT_CLIPORT
class InitPage extends Component<Props> {
props: Props;
......@@ -27,9 +30,10 @@ class InitPage extends Component<Props> {
navNextStep: false,
cliLocal: cliService.isCliLocal(),
cliUrl: undefined,
currentCliHost: DEFAULT_CLIHOST,
currentCliPort: DEFAULT_CLIPORT,
currentCliHostPort: DEFAULT_CLIHOSTPORT,
currentApiKey: DEFAULT_APIKEY,
showGuiProxy: false,
showApiKey: false,
cliError: undefined,
hasPairingPayload: false,
hasPairingDojo: false,
......@@ -46,8 +50,7 @@ class InitPage extends Component<Props> {
// configuration data
this.hasPairingPayload = undefined
this.inputCliHost = React.createRef()
this.inputCliPort = React.createRef()
this.inputCliHostPort = React.createRef()
this.inputApiKey = React.createRef()
this.goNextStep = this.goNextStep.bind(this)
......@@ -58,6 +61,7 @@ class InitPage extends Component<Props> {
this.onChangePairingPayload = this.onChangePairingPayload.bind(this)
this.onChangeTor = this.onChangeTor.bind(this)
this.onChangeDojo = this.onChangeDojo.bind(this)
this.onChangeGuiProxy = this.onChangeGuiProxy.bind(this)
this.onSubmitInitialize = this.onSubmitInitialize.bind(this)
}
......@@ -146,29 +150,43 @@ class InitPage extends Component<Props> {
this.resetCliUrl()
}
resetCliUrl() {
this.setState({
resetCliUrl(resetToggles=true) {
const newState = {
cliUrl: undefined,
cliError: undefined,
currentCliHost: DEFAULT_CLIHOST,
currentCliPort: DEFAULT_CLIPORT,
currentApiKey: DEFAULT_APIKEY,
});
currentCliHostPort: DEFAULT_CLIHOSTPORT,
currentApiKey: DEFAULT_APIKEY
}
if (resetToggles) {
newState.showApiKey = false
newState.showGuiProxy = false
}
this.setState(newState);
this.resetPairingPayload()
}
onChangeInputCliHostPort(e) {
this.resetCliUrl()
this.setState({
currentCliHost: this.inputCliHost.current.value,
currentCliPort: this.inputCliPort.current.value,
currentApiKey: this.inputApiKey.current.value
})
this.resetCliUrl(false)
const cliHostPort = this.inputCliHostPort.current ? this.inputCliHostPort.current.value : undefined
const apiKey = this.inputApiKey.current ? this.inputApiKey.current.value : undefined
const newState = {
currentCliHostPort: cliHostPort,
currentApiKey: apiKey
}
const showGuiProxy = cliHostPort && cliHostPort.indexOf('.onion')!==-1
if (showGuiProxy) {
newState.showGuiProxy = showGuiProxy
if (!guiConfig.getGuiProxy()) {
// set default GUI proxy
guiConfig.setGuiProxy(DEFAUT_GUI_PROXY)
}
}
this.setState(newState)
}
computeCliUrl() {
const cliUrlRemote = this.state.currentCliHost+':'+this.state.currentCliPort
const cliUrl = (this.state.cliLocal ? CLILOCAL_URL:cliUrlRemote)
const cliUrl = (this.state.cliLocal ? CLILOCAL_URL:this.state.currentCliHostPort)
return cliUrl
}
......@@ -194,6 +212,9 @@ class InitPage extends Component<Props> {
this.goNextStep()
}
}).catch(error => {
if (error && error.message.indexOf('API Key')) {
this.setState({showApiKey:true})
}
console.error('testCliUrl failed',error)
this.setState({
cliError: error.message
......@@ -214,45 +235,79 @@ class InitPage extends Component<Props> {
<div className="form-check">
<input className="form-check-input" type="radio" name="cliLocal" id="cliLocalTrue" value='true' checked={this.state.cliLocal} onChange={this.onChangeCliLocal}/>
<label className="form-check-label" htmlFor="cliLocalTrue">
<strong>Standalone GUI</strong>
<strong>Standard: standalone GUI</strong>
</label>
{this.state.cliLocal && <div className="col-sm-12"><div className="row">
{cliLocalService.getStatusIcon((icon,text)=><div className='col-sm-8'><Alert variant='success'>{icon} {text}</Alert></div>)}
{cliLocalService.isValid() && <div className='col-sm-2'><button type='button' className='btn btn-primary' onClick={this.connectCli} disabled={!cliLocalService.isValid()}>Connect</button></div>}
{!cliLocalService.isStatusDownloading() && !cliLocalService.isValid() && <div className='col-sm-12'><Alert variant='danger'>No valid CLI found. Please reinstall GUI.</Alert></div>}
</div></div>}
</div>
<div className="form-check">
<input className="form-check-input" type="radio" name="cliLocal" id="cliLocalFalse" value='false' checked={!this.state.cliLocal} onChange={this.onChangeCliLocal} />
<label className="form-check-label" htmlFor="cliLocalFalse">
<strong>Connect to remote CLI</strong><br/>
{!this.state.cliLocal &&
<div className="row">
<div className="col-sm-5">
<input type="text" className="form-control" placeholder="host" defaultValue={this.state.currentCliHost} ref={this.inputCliHost} onChange={this.onChangeInputCliHostPort} required/>
</div>
<div className="col-sm-2">
<input type="number" className="form-control" placeholder="port" defaultValue={this.state.currentCliPort} ref={this.inputCliPort} onChange={this.onChangeInputCliHostPort} required/>
</div>
<div className="col-sm-2">
<input type="password" className="form-control" placeholder="apiKey" defaultValue={this.state.currentApiKey} ref={this.inputApiKey} onChange={this.onChangeInputCliHostPort} />
</div>
<div className="col-sm-3">
{this.state.currentCliHost && this.state.currentCliPort
&& !this.state.cliUrl && <button type='button' className='btn btn-primary' onClick={this.connectCli}>Connect</button>}
</div>
</div>
}
<strong>Advanced: remote CLI</strong>
</label>
</div>
</div>
</div>
{this.state.cliLocal && <div>
<div className="row">
<div className="col-sm-1"></div>
{cliLocalService.getStatusIcon((icon,text)=><div className='col-sm-8'><Alert variant='success'>{icon} {text}</Alert></div>)}
{!cliLocalService.isStatusDownloading() && !cliLocalService.isValid() && <div className='col-sm-12'><Alert variant='danger'>No valid CLI found. Please reinstall GUI.</Alert></div>}
</div>
{cliLocalService.isValid() && <div className="row">
<div className="col-sm-3"></div>
<button type="button" className="btn btn-primary col-sm-3" onClick={this.connectCli}> Continue <FontAwesomeIcon icon={Icons.faArrowRight} /></button>
</div>}
</div>}
{!this.state.cliLocal &&
<Card>
<Card.Header>Remote CLI</Card.Header>
<Card.Body>
<div className="form-group row">
<div className="col-sm-11">
<div className="row">
<label htmlFor="cliHostPort" className="col-sm-2 col-form-label">CLI address</label>
<input type="text" id="cliHostPort" className="form-control col-sm-4" placeholder={DEFAULT_CLIHOSTPORT} defaultValue={this.state.currentCliHostPort} ref={this.inputCliHostPort} onChange={this.onChangeInputCliHostPort} required/>
&nbsp;
{this.state.currentCliHostPort
&& !this.state.cliUrl && <button type='button' className='btn btn-primary col-sm-2' onClick={this.connectCli}>Connect</button>}
</div>
&nbsp;
{this.state.showGuiProxy && <div className="row">
<label htmlFor="guiProxy" className="col-sm-2 col-form-label">Tor proxy</label>
<input type="text" id="guiProxy" className="form-control col-sm-4" defaultValue={guiConfig.getGuiProxy()} onChange={this.onChangeGuiProxy}/>
<label className='col-form-label col-sm-6 text-muted'>
Only required when CLI behind a Hidden Service.<br/>
<code>{DEFAUT_GUI_PROXY}</code>
</label>
</div>}
{this.state.showApiKey && <div className="row">
<label htmlFor="apiKey" className="col-sm-2 col-form-label">API Key</label>
<input type="password" id="apiKey" className="form-control col-sm-4" defaultValue={this.state.currentApiKey} ref={this.inputApiKey} onChange={this.onChangeInputCliHostPort} />
<label className='col-form-label col-sm-6 text-muted'>
Only required when CLI already initialized<br/>(<code>cli.apiKey</code> in <code>whirlpool-cli-config.properties</code>)
</label>
</div>}
<div className="row">
<div className="col-sm-1"></div>
{!this.state.showGuiProxy && <div className="col-sm-3 col-form-label">
<a onClick={() => this.setState({showGuiProxy:true})}>Use a Tor proxy?</a>
</div>}
{!this.state.showApiKey && <div className="col-sm-3 col-form-label">
<a onClick={() => this.setState({showApiKey:true})}>Configure API key?</a>
</div>}
</div>
</div>
</div>
</Card.Body>
</Card>
}
{this.state.cliError && <div className="row">
<div className="col-sm-12">
<div className="col-sm-12"><br/>
<Alert variant='danger'>Connection failed: {this.state.cliError}</Alert>
</div>
</div>}
{this.navButtons(this.state.cliUrl ? true : false)}
{this.navButtons(false)}
</div>
}
......@@ -311,6 +366,11 @@ class InitPage extends Component<Props> {
})
}
onChangeGuiProxy(e) {
const value = e.target.value
guiConfig.setGuiProxy(value)
}
onSubmitInitialize() {
cliService.initializeCli(this.state.cliUrl, this.state.currentApiKey, this.state.cliLocal, this.state.pairingPayload, this.state.tor, this.state.dojo).then(() => {
// success!
......
......@@ -8,7 +8,7 @@ import {
CLI_CONFIG_FILENAME,
CLI_LOG_ERROR_FILE,
CLI_LOG_FILE,
cliApiService,
cliApiService, GUI_CONFIG_FILENAME,
GUI_LOG_FILE,
GUI_VERSION
} from '../const';
......@@ -16,6 +16,7 @@ import * as Icons from '@fortawesome/free-solid-svg-icons';
import LinkExternal from '../components/Utils/LinkExternal';
import { Card } from 'react-bootstrap';
import utils from '../services/utils';
import guiConfig from '../mainProcess/guiConfig';
type Props = {};
......@@ -50,24 +51,53 @@ export default class StatusPage extends Component<Props> {
<Card>
<Card.Header>
<div className='row'>
<div className='col-sm-6'>
<div className='col-sm-12'>
<strong>GUI</strong>
</div>
<div className='col-sm-6'>
{cliService.getLoginStatusIcon((icon,text)=><div>{icon} {text}</div>)}
</div>
</div>
</Card.Header>
<Card.Body>
<div style={{'float':'right'}}>
<button type='button' className='btn btn-danger' onClick={this.onResetConfig}><FontAwesomeIcon icon={Icons.faExclamationTriangle} /> Reset {cliService.getResetLabel()}</button>
</div>
<div className='row'>
<div className='col-sm-2'>
<strong>Version:</strong><br/>
<strong>Path:</strong><br/>
<strong>GUI logs:</strong>
<strong>Version:</strong>
</div>
<div className='col-sm-10'>
<div>GUI <strong>{GUI_VERSION}</strong>, API <strong>{cliApiService.getVersionName()}</strong></div>
</div>
</div>
{cliService.isConfigured() && <div className='row'>
<div className='col-sm-2'>
<strong>Mode:</strong>
</div>
<div className='col-sm-10'>
<div><strong>{cliService.isCliLocal()?'Standalone':'Remote CLI'}</strong></div>
</div>
</div>}
{cliService.isConfigured() && !cliService.isCliLocal() && <div className='row'>
<div className='col-sm-2'>
<strong>GUI proxy:</strong>
</div>
<div className='col-sm-10'>
<div><strong>{guiConfig.getGuiProxy()||'None'}</strong></div>
</div>
</div>}
<div className='row small'>
<div className='col-sm-12'>
<hr/>
</div>
</div>
<div className='row small'>
<div className='col-sm-2'>
<strong>Path:</strong><br/>
<strong>Config:</strong><br/>
<strong>Logs:</strong>
</div>
<div className='col-sm-10'>
<div><LinkExternal href={'file://'+APP_USERDATA}>{APP_USERDATA}</LinkExternal></div>
<div></div><LinkExternal href={'file://'+APP_USERDATA+'/'+GUI_CONFIG_FILENAME}>{APP_USERDATA+'/'+GUI_CONFIG_FILENAME}</LinkExternal><br/>
<div><LinkExternal href={'file://'+this.guiLogFile}>{this.guiLogFile}</LinkExternal></div>
</div>
</div>
......@@ -77,35 +107,11 @@ export default class StatusPage extends Component<Props> {
</div>
</div>
{!cliService.isCliLocal() && <Card>
<Card.Header>
<div className='row'>
<div className='col-sm-6'>
<strong>CLI: remote</strong>
</div>
<div className='col-sm-6'>
{cliStatusIcon}
</div>
</div>
</Card.Header>
<Card.Body>
<div className='row'>
<div className='col-sm-12'>
{cliStatusIcon}<br/>
{cliService.isConfigured() && <span>Remote CLI: {cliService.getCliUrl()}</span>}
</div>
</div>
</Card.Body>
</Card>}
{cliService.isCliLocal() && <Card>
<Card>
<Card.Header>
<div className='row'>
<div className='col-sm-6'>
<strong>CLI: standalone</strong>
</div>
<div className='col-sm-6'>
{cliStatusIcon}
<strong>CLI</strong>
</div>
</div>
</Card.Header>
......@@ -115,75 +121,82 @@ export default class StatusPage extends Component<Props> {
<strong>Status:</strong>
</div>
<div className='col-sm-10'>
{cliLocalService.getStatusIcon((icon,text)=><div>{icon} {text}</div>)}
{cliStatusIcon}
</div>
</div>
<div className='row'>
<div className='col-sm-2'>
<strong>Path:</strong><br/>
<strong>Config:</strong><br/>
{cliLocalService.hasCliApi() && <strong>JAR:<br/></strong>}
{cliLocalService.hasCliApi() && <strong>Version:<br/></strong>}<br/>
</div>
<div className='col-sm-10'>
<LinkExternal href={'file://'+cliApiService.getCliPath()}>{cliApiService.getCliPath()}</LinkExternal><br/>
<LinkExternal href={'file://'+cliApiService.getCliPath()+CLI_CONFIG_FILENAME}>{CLI_CONFIG_FILENAME}</LinkExternal><br/>
{cliLocalService.hasCliApi() && <div>{cliLocalService.getCliFilename()} ({cliLocalService.getCliChecksum()})</div>}
{cliLocalService.hasCliApi() && <div>{cliLocalService.getCliVersionStr()}</div>}
{!cliLocalService.hasCliApi() && <div><strong>CLI_API {cliApiService.getVersionName()} could not be resolved</strong><br/></div>}
{cliService.isConfigured() && !cliService.isCliLocal() && <div>
<div className='row'>
<div className='col-sm-2'>
<strong>Remote CLI:</strong>
</div>
<div className='col-sm-10'>
{cliService.getCliUrl()}
</div>
</div>
</div>