diff --git a/static/skywire-manager-src/src/app/app-routing.module.ts b/static/skywire-manager-src/src/app/app-routing.module.ts index a52233d6a8..74306fdc2c 100644 --- a/static/skywire-manager-src/src/app/app-routing.module.ts +++ b/static/skywire-manager-src/src/app/app-routing.module.ts @@ -129,6 +129,10 @@ const routes: Routes = [ }, ], }, + { + path: 'vpnlogin/:key', + component: LoginComponent + }, { path: 'vpn', canActivate: [VpnAuthGuardService], diff --git a/static/skywire-manager-src/src/app/app.component.ts b/static/skywire-manager-src/src/app/app.component.ts index 27319f9489..614fcba912 100644 --- a/static/skywire-manager-src/src/app/app.component.ts +++ b/static/skywire-manager-src/src/app/app.component.ts @@ -50,7 +50,7 @@ export class AppComponent { // Check if the app is showing the VPN client. router.events.subscribe(() => { - this.inVpnClient = router.url.includes('/vpn/'); + this.inVpnClient = router.url.includes('/vpn/') || router.url.includes('vpnlogin'); // Show the correct document title. if (router.url.length > 2) { diff --git a/static/skywire-manager-src/src/app/components/pages/login/login.component.ts b/static/skywire-manager-src/src/app/components/pages/login/login.component.ts index b0ea768a37..86cb4be5c8 100644 --- a/static/skywire-manager-src/src/app/components/pages/login/login.component.ts +++ b/static/skywire-manager-src/src/app/components/pages/login/login.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { Subscription } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; @@ -22,23 +22,34 @@ import { processServiceError } from '../../../utils/errors'; export class LoginComponent implements OnInit, OnDestroy { form: FormGroup; loading = false; + isForVpn = false; + vpnKey = ''; private verificationSubscription: Subscription; private loginSubscription: Subscription; + private routeSubscription: Subscription; constructor( private authService: AuthService, private router: Router, private snackbarService: SnackbarService, private dialog: MatDialog, + private route: ActivatedRoute, ) { } ngOnInit() { - // Check if the user is already logged. - this.verificationSubscription = this.authService.checkLogin().subscribe(response => { - if (response !== AuthStates.NotLogged) { - this.router.navigate(['nodes'], { replaceUrl: true }); - } + this.routeSubscription = this.route.paramMap.subscribe(params => { + this.vpnKey = params.get('key'); + + this.isForVpn = window.location.href.indexOf('vpnlogin') !== -1; + + // Check if the user is already logged. + this.verificationSubscription = this.authService.checkLogin().subscribe(response => { + if (response !== AuthStates.NotLogged) { + const destination = !this.isForVpn ? ['nodes'] : ['vpn', this.vpnKey, 'status']; + this.router.navigate(destination, { replaceUrl: true }); + } + }); }); this.form = new FormGroup({ @@ -52,6 +63,7 @@ export class LoginComponent implements OnInit, OnDestroy { } this.verificationSubscription.unsubscribe(); + this.routeSubscription.unsubscribe(); } login() { @@ -71,7 +83,8 @@ export class LoginComponent implements OnInit, OnDestroy { } private onLoginSuccess() { - this.router.navigate(['nodes'], { replaceUrl: true }); + const destination = !this.isForVpn ? ['nodes'] : ['vpn', this.vpnKey, 'status']; + this.router.navigate(destination, { replaceUrl: true }); } private onLoginError(err: OperationError) { diff --git a/static/skywire-manager-src/src/app/components/pages/node/node-info/node-info-content/node-info-content.component.ts b/static/skywire-manager-src/src/app/components/pages/node/node-info/node-info-content/node-info-content.component.ts index cfeadfc0fa..0dee7aa98d 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/node-info/node-info-content/node-info-content.component.ts +++ b/static/skywire-manager-src/src/app/components/pages/node/node-info/node-info-content/node-info-content.component.ts @@ -7,7 +7,7 @@ import { NodeComponent } from '../../node.component'; import TimeUtils, { ElapsedTime } from 'src/app/utils/timeUtils'; import { LabeledElementTypes, StorageService } from 'src/app/services/storage.service'; import { NodeService, HealthStatus } from 'src/app/services/node.service'; -import { RouterConfigComponent } from './router-config/router-config.component'; +import { RouterConfigComponent, RouterConfigParams } from './router-config/router-config.component'; /** * Shows the basic info of a node. @@ -52,7 +52,8 @@ export class NodeInfoContentComponent { } changeRouterConfig() { - RouterConfigComponent.openDialog(this.dialog, this.node).afterClosed().subscribe((changed: boolean) => { + const params: RouterConfigParams = {nodePk: this.node.localPk, minHops: this.node.minHops}; + RouterConfigComponent.openDialog(this.dialog, params).afterClosed().subscribe((changed: boolean) => { if (changed) { NodeComponent.refreshCurrentDisplayedData(); } diff --git a/static/skywire-manager-src/src/app/components/pages/node/node-info/node-info-content/router-config/router-config.component.ts b/static/skywire-manager-src/src/app/components/pages/node/node-info/node-info-content/router-config/router-config.component.ts index 0a75a9f3f3..5c46f19d91 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/node-info/node-info-content/router-config/router-config.component.ts +++ b/static/skywire-manager-src/src/app/components/pages/node/node-info/node-info-content/router-config/router-config.component.ts @@ -11,6 +11,20 @@ import { OperationError } from 'src/app/utils/operation-error'; import { processServiceError } from 'src/app/utils/errors'; import { RouteService } from 'src/app/services/route.service'; +/** + * Params for RouterConfigComponent. + */ +export interface RouterConfigParams { + /** + * PK of the node. + */ + nodePk: string; + /** + * Current value of the min hops property in the node. + */ + minHops: number; +} + /** * Modal window for changing the configuration related to the router. It changes the values * and shows a confirmation msg by itself. @@ -31,7 +45,7 @@ export class RouterConfigComponent implements OnInit, OnDestroy { /** * Opens the modal window. Please use this function instead of opening the window "by hand". */ - public static openDialog(dialog: MatDialog, node: Node): MatDialogRef { + public static openDialog(dialog: MatDialog, node: RouterConfigParams): MatDialogRef { const config = new MatDialogConfig(); config.data = node; config.autoFocus = false; @@ -42,7 +56,7 @@ export class RouterConfigComponent implements OnInit, OnDestroy { constructor( private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) private data: Node, + @Inject(MAT_DIALOG_DATA) private data: RouterConfigParams, private formBuilder: FormBuilder, private snackbarService: SnackbarService, private routeService: RouteService, @@ -74,7 +88,7 @@ export class RouterConfigComponent implements OnInit, OnDestroy { this.button.showLoading(); this.operationSubscription = this.routeService.setMinHops( - this.data.localPk, + this.data.nodePk, Number.parseInt(this.form.get('min').value, 10) ).subscribe({ next: this.onSuccess.bind(this), diff --git a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.html b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.html index e8e95af489..0fcbb79970 100644 --- a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.html +++ b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.html @@ -145,6 +145,8 @@ {{ dataSorter.sortingArrow }} +
info_outline @@ -231,9 +234,15 @@ {{ 'vpn.server-list.unknown' | translate }} + + + diff --git a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.scss b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.scss index 2f5bd8d5f4..806a392f07 100644 --- a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.scss +++ b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.scss @@ -114,7 +114,6 @@ tr { } .date-column { - max-width: 0; width: 150px; @extend .single-line; } @@ -140,7 +139,8 @@ tr { .history-pk-column { width: 20% !important; } - +/* +// TODO: for currently commented columns, must be deleted or reactivated depending on what happens to the columns. .public-pk-column { width: 12% !important; } @@ -158,7 +158,7 @@ tr { min-width: 60px; @extend .single-line; } - +*/ .icon-fixer { line-height: 0px; } @@ -193,7 +193,8 @@ tr { @extend .single-line; flex-grow: 1; } - + /* + // TODO: for currently commented columns, must be deleted or reactivated depending on what happens to the columns. .star-container { height: 0px; position: relative; @@ -203,6 +204,7 @@ tr { font-size: 10px; } } + */ } .flag { @@ -220,6 +222,11 @@ tr { } } +.center { + text-align: center; +} +/* +// TODO: for currently commented columns, must be deleted or reactivated depending on what happens to the columns. .rating { width: 14px; height: 14px; @@ -227,10 +234,6 @@ tr { background-size: contain; } -.center { - text-align: center; -} - .green-value { color: $green-clear !important; } @@ -242,7 +245,7 @@ tr { .red-value { color: $red-clear !important; } - +*/ .alert-icon { vertical-align: middle; margin-right: 10px; diff --git a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.ts b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.ts index 67054695db..24ef861abd 100644 --- a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.ts +++ b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-server-list/vpn-server-list.component.ts @@ -8,7 +8,7 @@ import { SortingModes, SortingColumn, DataSorter } from 'src/app/utils/lists/dat import { DataFilterer } from 'src/app/utils/lists/data-filterer'; import { FilterProperties, FilterFieldTypes, PrintableLabel } from 'src/app/utils/filters'; import { countriesList } from 'src/app/utils/countries-list'; -import { VpnClientDiscoveryService, VpnServer, Ratings } from 'src/app/services/vpn-client-discovery.service'; +import { VpnClientDiscoveryService, VpnServer } from 'src/app/services/vpn-client-discovery.service'; import { VpnHelpers } from '../../vpn-helpers'; import { VpnClientService } from 'src/app/services/vpn-client.service'; import { SnackbarService } from 'src/app/services/snackbar.service'; @@ -48,6 +48,10 @@ interface VpnServerForList { * 2 letter code of the country the server is in. */ countryCode: string; + /** + * Name of the country. + */ + countryName: string; /** * Sever name, obtained from the discovery service. */ @@ -64,26 +68,32 @@ interface VpnServerForList { * Public key. */ pk: string; + + + // TODO: for currently commented columns, must be deleted or reactivated depending on what + // happens to the columns. /** * Current congestion of the server, obtained from the discovery service. */ - congestion?: number; + // congestion?: number; /** * Rating of the congestion the server normally has, obtained from the discovery service. */ - congestionRating?: Ratings; + // congestionRating?: Ratings; /** * Latency of the server, obtained from the discovery service. */ - latency?: number; + // latency?: number; /** * Rating of the latency the server normally has, obtained from the discovery service. */ - latencyRating?: Ratings; + // latencyRating?: Ratings; /** * Hops needed for reaching the server. */ - hops?: number; + // hops?: number; + + /** * Note with information about the server, obtained from the discovery service. */ @@ -142,10 +152,14 @@ export class VpnServerListComponent implements OnDestroy { // Vars with the data of the columns used for sorting the data. dateSortData = new SortingColumn(['lastUsed'], 'vpn.server-list.date-small-table-label', SortingModes.NumberReversed); - countrySortData = new SortingColumn(['countryCode'], 'vpn.server-list.country-small-table-label', SortingModes.Text); + countrySortData = new SortingColumn(['countryName'], 'vpn.server-list.country-small-table-label', SortingModes.Text); nameSortData = new SortingColumn(['name'], 'vpn.server-list.name-small-table-label', SortingModes.Text); locationSortData = new SortingColumn(['location'], 'vpn.server-list.location-small-table-label', SortingModes.Text); pkSortData = new SortingColumn(['pk'], 'vpn.server-list.public-key-small-table-label', SortingModes.Text); + noteSortData = new SortingColumn(['note'], 'vpn.server-list.note-small-table-label', SortingModes.Text); + /* + // TODO: for currently commented columns, must be deleted or reactivated depending on what + // happens to the columns. congestionSortData = new SortingColumn(['congestion'], 'vpn.server-list.congestion-small-table-label', SortingModes.Number); congestionRatingSortData = new SortingColumn( ['congestionRating'], @@ -155,7 +169,7 @@ export class VpnServerListComponent implements OnDestroy { latencySortData = new SortingColumn(['latency'], 'vpn.server-list.latency-small-table-label', SortingModes.Number); latencyRatingSortData = new SortingColumn(['latencyRating'], 'vpn.server-list.latency-rating-small-table-label', SortingModes.Number); hopsSortData = new SortingColumn(['hops'], 'vpn.server-list.hops-small-table-label', SortingModes.Number); - noteSortData = new SortingColumn(['note'], 'vpn.server-list.note-small-table-label', SortingModes.Text); + */ private dataSortedSubscription: Subscription; private dataFiltererSubscription: Subscription; @@ -202,7 +216,6 @@ export class VpnServerListComponent implements OnDestroy { private dataSubscription: Subscription; private currentServerSubscription: Subscription; private backendDataSubscription: Subscription; - private checkVpnSubscription: Subscription; constructor( private dialog: MatDialog, @@ -260,12 +273,7 @@ export class VpnServerListComponent implements OnDestroy { // Load the data, if needed. if (!this.initialLoadStarted) { this.initialLoadStarted = true; - - if (this.currentList === Lists.Public) { - this.loadTestData(); - } else { - this.loadData(); - } + this.loadData(); } }); @@ -294,8 +302,6 @@ export class VpnServerListComponent implements OnDestroy { this.dataSubscription.unsubscribe(); } - this.closeCheckVpnSubscription(); - if (this.dataFilterer) { this.dataFilterer.dispose(); } @@ -433,17 +439,23 @@ export class VpnServerListComponent implements OnDestroy { this.allServers = response.map(server => { return { countryCode: server.countryCode, + countryName: this.getCountryName(server.countryCode), name: server.name, customName: null, location: server.location, pk: server.pk, + note: server.note, + personalNote: null, + + /* + // TODO: for currently commented columns, must be deleted or reactivated depending on + // what happens to the columns. congestion: server.congestion, congestionRating: server.congestionRating, latency: server.latency, latencyRating: server.latencyRating, hops: server.hops, - note: server.note, - personalNote: null, + */ originalDiscoveryData: server, }; @@ -473,6 +485,7 @@ export class VpnServerListComponent implements OnDestroy { response.forEach(server => { processedList.push({ countryCode: server.countryCode, + countryName: this.getCountryName(server.countryCode), name: server.name, customName: null, location: server.location, @@ -495,75 +508,6 @@ export class VpnServerListComponent implements OnDestroy { } } - /** - * TODO: should be removed in the final version. - */ - private loadTestData() { - setTimeout(() => { - this.allServers = []; - - const server1: VpnServer = { - countryCode: 'au', - name: 'Server name', - location: 'Melbourne - Australia', - pk: '024ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7', - congestion: 20, - congestionRating: Ratings.Gold, - latency: 123, - latencyRating: Ratings.Gold, - hops: 3, - note: 'Note', - }; - this.allServers.push({...server1, - customName: null, - personalNote: null, - originalDiscoveryData: server1, - }); - - const server2: VpnServer = { - countryCode: 'br', - name: 'Test server 14', - location: 'Rio de Janeiro - Brazil', - pk: '034ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7', - congestion: 20, - congestionRating: Ratings.Silver, - latency: 12345, - latencyRating: Ratings.Gold, - hops: 3, - note: 'Note' - }; - this.allServers.push({...server2, - customName: null, - personalNote: null, - originalDiscoveryData: server2 - }); - - const server3: VpnServer = { - countryCode: 'de', - name: 'Test server 20', - location: 'Berlin - Germany', - pk: '044ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7', - congestion: 20, - congestionRating: Ratings.Gold, - latency: 123, - latencyRating: Ratings.Bronze, - hops: 7, - note: 'Note' - }; - this.allServers.push({...server3, - customName: null, - personalNote: null, - originalDiscoveryData: server3, - }); - - this.vpnSavedDataService.updateFromDiscovery([server1, server2, server3]); - - this.loading = false; - - this.processAllServers(); - }, 100); - } - /** * Makes preparations for the page to work well with the obtained server list. */ @@ -626,12 +570,16 @@ export class VpnServerListComponent implements OnDestroy { sortableColumns.push(this.nameSortData); sortableColumns.push(this.locationSortData); sortableColumns.push(this.pkSortData); + sortableColumns.push(this.noteSortData); + /* + // TODO: for currently commented columns, must be deleted or reactivated depending on + // what happens to the columns. sortableColumns.push(this.congestionSortData); sortableColumns.push(this.congestionRatingSortData); sortableColumns.push(this.latencySortData); sortableColumns.push(this.latencyRatingSortData); sortableColumns.push(this.hopsSortData); - sortableColumns.push(this.noteSortData); + */ defaultColumn = 0; tieBreakerColumn = 1; @@ -700,7 +648,9 @@ export class VpnServerListComponent implements OnDestroy { maxlength: 100, } ]; - + /* + // TODO: for currently commented columns, must be deleted or reactivated depending on + // what happens to the columns. if (this.currentList === Lists.Public) { this.filterProperties.push({ filterName: 'vpn.server-list.filter-dialog.congestion-rating', @@ -750,6 +700,7 @@ export class VpnServerListComponent implements OnDestroy { ], }); } + */ } /** @@ -787,26 +738,35 @@ export class VpnServerListComponent implements OnDestroy { return countriesList[countryCode.toUpperCase()] ? countriesList[countryCode.toUpperCase()] : countryCode; } + + // TODO: the functions below are for currently commented columns, must be deleted or reactivated + // depending on what happens to the columns. + /** * Gets the name of the translatable var that must be used for showing a latency value. This * allows to add the correct measure suffix. */ + /* getLatencyValueString(latency: number): string { return VpnHelpers.getLatencyValueString(latency); } + */ /** * Gets the string value to show in the UI a latency value with an adecuate number of decimals. * This function converts the value from ms to segs, if appropriate, so the value must be shown * using the var returned by getLatencyValueString. */ + /* getPrintableLatency(latency: number): string { return VpnHelpers.getPrintableLatency(latency); } + */ /** * Gets the class that must be used for showing the color of a congestion value. */ + /* getCongestionTextColorClass(congestion: number): string { if (congestion < 60) { return 'green-value'; @@ -816,10 +776,12 @@ export class VpnServerListComponent implements OnDestroy { return 'red-value'; } + */ /** * Gets the class that must be used for showing the color of a latency value. */ + /* getLatencyTextColorClass(latency: number): string { if (latency < 200) { return 'green-value'; @@ -829,10 +791,12 @@ export class VpnServerListComponent implements OnDestroy { return 'red-value'; } + */ /** * Gets the class that must be used for showing the color of a hops value. */ + /* getHopsTextColorClass(hops: number): string { if (hops < 5) { return 'green-value'; @@ -842,10 +806,12 @@ export class VpnServerListComponent implements OnDestroy { return 'red-value'; } + */ /** * Returns the name of the image that must be shown for a rating value. */ + /* getRatingIcon(rating: Ratings): string { if (rating === Ratings.Gold) { return 'gold-rating'; @@ -855,10 +821,12 @@ export class VpnServerListComponent implements OnDestroy { return 'bronze-rating'; } + */ /** * Returns the translatable var for describing a rating value. */ + /* getRatingText(rating: Ratings): string { if (rating === Ratings.Gold) { return 'vpn.server-list.gold-rating-info'; @@ -868,10 +836,5 @@ export class VpnServerListComponent implements OnDestroy { return 'vpn.server-list.bronze-rating-info'; } - - private closeCheckVpnSubscription() { - if (this.checkVpnSubscription) { - this.checkVpnSubscription.unsubscribe(); - } - } + */ } diff --git a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-settings/vpn-settings.component.html b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-settings/vpn-settings.component.html index c9c4c64f6a..10af497ff0 100644 --- a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-settings/vpn-settings.component.html +++ b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-settings/vpn-settings.component.html @@ -83,6 +83,18 @@ {{ getUnitsOptionText(dataUnitsOption) | translate }} + + + +
+ {{ 'vpn.settings-page.minimum-hops' | translate }} + help +
+ + + {{ backendData.vpnClientAppData.minHops }} + +
diff --git a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-settings/vpn-settings.component.ts b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-settings/vpn-settings.component.ts index 8c351df982..feaf6ad44f 100644 --- a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-settings/vpn-settings.component.ts +++ b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-settings/vpn-settings.component.ts @@ -12,6 +12,7 @@ import { DataUnits, VpnSavedDataService } from 'src/app/services/vpn-saved-data. import GeneralUtils from 'src/app/utils/generalUtils'; import { SelectableOption, SelectOptionComponent } from 'src/app/components/layout/select-option/select-option.component'; import { TopBarComponent } from 'src/app/components/layout/top-bar/top-bar.component'; +import { RouterConfigComponent, RouterConfigParams } from 'src/app/components/pages/node/node-info/node-info-content/router-config/router-config.component'; /** * Options that VpnSettingsComponent might be changing asynchronously. @@ -230,4 +231,12 @@ export class VpnSettingsComponent implements OnDestroy { } }); } + + /** + * Opens the modal window for changing the hops configuration. + */ + changeHops() { + const params: RouterConfigParams = {nodePk: this.currentLocalPk, minHops: this.backendData.vpnClientAppData.minHops}; + RouterConfigComponent.openDialog(this.dialog, params).afterClosed().subscribe(); + } } diff --git a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.html b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.html index 5614ab684c..bda351269c 100644 --- a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.html +++ b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.html @@ -86,25 +86,39 @@
-
+
info_outline {{ getNoteVar() | translate:{custom: currentRemoteServer.personalNote, original: currentRemoteServer.note} }}
+ +
+
+ cancel + {{ 'vpn.status-page.last-error' | translate }} + {{ backendState.vpnClientAppData.lasErrorMsg }} +
+
+ + + timer - 01:12:21 + 00:00:00
-
Your connection is currently:
-
{{ currentStateText | translate }}
+ --> +
{{ 'vpn.connection-info.state-title' | translate }}
+
+
{{ currentStateText | translate }}
+
+
{{ (currentStateText + '-info') | translate }}
diff --git a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.scss b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.scss index fc743c3e8d..ae2f8c7776 100644 --- a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.scss +++ b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.scss @@ -51,6 +51,24 @@ text-transform: uppercase; } + .state-line { + height: 1px; + width: 100%; + margin-bottom: 5px; + } + + .green-line { + background-color: $green; + } + + .yellow-line { + background-color: $yellow; + } + + .red-line { + background-color: $red; + } + .state-explanation { font-size: $font-size-mini; } @@ -416,20 +434,28 @@ } } - .current-server-note { + .lower-text { display: inline-block; max-width: $max-server-box-size; - margin-top: 15px; - font-size: $font-size-mini; - color: $grey; + margin-top: 10px; mat-icon { position: relative; - top: 1px; + top: 2px; display: inline; user-select: none; } } + + .current-server-note { + font-size: $font-size-smaller; + color: $grey; + } + + .last-error { + font-size: $font-size-smaller; + color: $red-clear; + } } .right-area { diff --git a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.ts b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.ts index b32ab0c549..128ac43cf3 100644 --- a/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.ts +++ b/static/skywire-manager-src/src/app/components/vpn/pages/vpn-status/vpn-status.component.ts @@ -62,6 +62,8 @@ export class VpnStatusComponent implements OnInit, OnDestroy { lastAppState: AppState = null; // If the UI must be shown busy. showBusy = false; + // If the user requested the VPN to be stopped and the code is still waiting for it to happen. + private stopRequested = false; // If the user has not blocked the option for showing the IP info. ipInfoAllowed: boolean; // Public IP of the machine running the app. @@ -134,16 +136,22 @@ export class VpnStatusComponent implements OnInit, OnDestroy { // Start getting and updating the state of the backend. this.dataSubscription = this.vpnClientService.backendState.subscribe(data => { if (data && data.serviceState !== VpnServiceStates.PerformingInitialCheck) { + const firstEventExecution = !!!this.backendState; this.backendState = data; - // If the state was changed, update the IP. - if (this.lastAppState !== data.vpnClientAppData.appState) { - if (data.vpnClientAppData.appState === AppState.Running || data.vpnClientAppData.appState === AppState.Stopped) { - this.getIp(true); + if (!firstEventExecution) { + // If the state was changed, update the IP. + if (this.lastAppState !== data.vpnClientAppData.appState) { + if (data.vpnClientAppData.appState === AppState.Running || data.vpnClientAppData.appState === AppState.Stopped) { + this.getIp(true); + } } + } else { + // Get the ip data for the first time. + this.getIp(true); } - this.showStarted = data.vpnClientAppData.running; + this.showStarted = data.vpnClientAppData.running || data.vpnClientAppData.appState !== AppState.Stopped; if (this.showStartedLastValue !== this.showStarted) { // If the running state changed, restart the values for the data graphs. @@ -164,7 +172,12 @@ export class VpnStatusComponent implements OnInit, OnDestroy { this.lastAppState = data.vpnClientAppData.appState; this.showStartedLastValue = this.showStarted; - this.showBusy = data.busy; + if (!this.stopRequested) { + this.showBusy = data.busy; + } else if (!this.showStarted) { + this.stopRequested = false; + this.showBusy = data.busy; + } // Update the values for the data graphs. if (data.vpnClientAppData.connectionData) { @@ -193,9 +206,6 @@ export class VpnStatusComponent implements OnInit, OnDestroy { this.currentRemoteServer = server; }); }); - - // Get the current IP. - this.getIp(true); } ngOnDestroy() { @@ -256,6 +266,7 @@ export class VpnStatusComponent implements OnInit, OnDestroy { * Makes the actual request for stopping the VPN. */ private finishStoppingVpn() { + this.stopRequested = true; this.showBusy = true; this.vpnClientService.stop(); } @@ -330,6 +341,23 @@ export class VpnStatusComponent implements OnInit, OnDestroy { } } + /** + * Class that should be used for the colored state bar. + */ + get currentStateLineClass(): string { + if (this.backendState.vpnClientAppData.appState === AppState.Stopped) { + return 'red-line'; + } else if (this.backendState.vpnClientAppData.appState === AppState.Connecting) { + return 'yellow-line'; + } else if (this.backendState.vpnClientAppData.appState === AppState.Running) { + return 'green-line'; + } else if (this.backendState.vpnClientAppData.appState === AppState.ShuttingDown) { + return 'yellow-line'; + } else { + return 'yellow-line'; + } + } + private closeOperationSubscription() { if (this.operationSubscription) { this.operationSubscription.unsubscribe(); @@ -388,7 +416,7 @@ export class VpnStatusComponent implements OnInit, OnDestroy { * @param ignoreTimeCheck If true, the operation will be performed even if the function * was called shortly before. */ - private getIp(ignoreTimeCheck = false) { + public getIp(ignoreTimeCheck = false) { // Cancel the operation if the used blocked the IP checking functionality. if (!this.ipInfoAllowed) { return; @@ -435,8 +463,8 @@ export class VpnStatusComponent implements OnInit, OnDestroy { this.problemGettingIp = false; this.currentIp = response; - // Update the country, if the IP changed. - if (this.previousIp !== this.currentIp || this.problemGettingIpCountry) { + // Update the country, if no country has been loaded or the IP changed. + if (!this.ipCountry || this.previousIp !== this.currentIp || this.problemGettingIpCountry) { this.getIpCountry(); } else { this.loadingIpCountry = false; diff --git a/static/skywire-manager-src/src/app/components/vpn/vpn-helpers.ts b/static/skywire-manager-src/src/app/components/vpn/vpn-helpers.ts index b5d84663fa..7a710198c6 100644 --- a/static/skywire-manager-src/src/app/components/vpn/vpn-helpers.ts +++ b/static/skywire-manager-src/src/app/components/vpn/vpn-helpers.ts @@ -250,6 +250,9 @@ export class VpnHelpers { if (server.usedWithPassword) { options.push({ icon: 'lock_open', label: 'vpn.server-options.connect-without-password' }); optionCodes.push(201); + + options.push({ icon: 'lock_outlined', label: 'vpn.server-options.connect-using-another-password' }); + optionCodes.push(202); } else { // Allow to use a password only if the server was added manually. if (server.enteredManually) { diff --git a/static/skywire-manager-src/src/app/services/api.service.ts b/static/skywire-manager-src/src/app/services/api.service.ts index a1e9dcbdce..a8f15654bd 100644 --- a/static/skywire-manager-src/src/app/services/api.service.ts +++ b/static/skywire-manager-src/src/app/services/api.service.ts @@ -21,6 +21,7 @@ export class RequestOptions { responseType = ResponseTypes.Json; requestType = RequestTypes.Json; ignoreAuth = false; + vpnKeyForAuth: string; public constructor(init?: Partial) { Object.assign(this, init); @@ -167,11 +168,13 @@ export class ApiService { // user is redirected to the login page. if (!options.ignoreAuth) { if (error.status === 401) { - this.ngZone.run(() => this.router.navigate(['login'], { replaceUrl: true })); + const destination = !options.vpnKeyForAuth ? ['login'] : ['vpnlogin', options.vpnKeyForAuth]; + this.ngZone.run(() => this.router.navigate(destination, { replaceUrl: true })); } if (error.error && typeof error.error === 'string' && error.error.includes('change password')) { - this.ngZone.run(() => this.router.navigate(['login'], { replaceUrl: true })); + const destination = !options.vpnKeyForAuth ? ['login'] : ['vpnlogin', options.vpnKeyForAuth]; + this.ngZone.run(() => this.router.navigate(destination, { replaceUrl: true })); } } diff --git a/static/skywire-manager-src/src/app/services/vpn-client-discovery.service.ts b/static/skywire-manager-src/src/app/services/vpn-client-discovery.service.ts index f7c2f0f0b1..e317a2f8e2 100644 --- a/static/skywire-manager-src/src/app/services/vpn-client-discovery.service.ts +++ b/static/skywire-manager-src/src/app/services/vpn-client-discovery.service.ts @@ -5,12 +5,17 @@ import { retryWhen, delay, map } from 'rxjs/operators'; /** * Ratings some properties of a server can have. + * + * TODO: used only for columns that are currently deactivated in the server list. Must + * be deleted or reactivated depending on what happens to the columns. */ +/* export enum Ratings { Gold = 0, Silver = 1, Bronze = 2, } +*/ /** * Data of a server obtained from the discovery service. @@ -19,7 +24,7 @@ export class VpnServer { /** * 2 letter code of the country the server is in. */ - countryCode: string; + countryCode = 'ZZ'; /** * Sever name. */ @@ -32,26 +37,32 @@ export class VpnServer { * Public key. */ pk: string; + + + // TODO: used only for columns that are currently deactivated in the server list. Must + // be deleted or reactivated depending on what happens to the columns. /** * Current congestion of the server. */ - congestion: number; + // congestion: number; /** * Rating of the congestion the server normally has. */ - congestionRating: Ratings; + // congestionRating: Ratings; /** * Latency of the server. */ - latency: number; + // latency: number; /** * Rating of the latency the server normally has. */ - latencyRating: Ratings; + // latencyRating: Ratings; /** * Hops needed for reaching the server. */ - hops: number; + // hops: number; + + /** * Note the server has in the discovery service. */ @@ -71,7 +82,7 @@ export class VpnClientDiscoveryService { /** * URL of the discovery service. */ - private readonly discoveryServiceUrl = 'https://service.discovery.skycoin.com/api/services?type=vpn'; + private readonly discoveryServiceUrl = 'https://sd.skycoin.com/api/services?type=vpn'; /** * Servers obtained from the discovery service. @@ -119,13 +130,18 @@ export class VpnClientDiscoveryService { } } - // Data that must be obtained after the changes in the service. currentEntry.name = addressParts[0]; + /* + // TODO: used only for columns that are currently deactivated in the server list. Must + // be deleted or reactivated depending on what happens to the columns. currentEntry.congestion = 20; currentEntry.congestionRating = Ratings.Gold; currentEntry.latency = 123; currentEntry.latencyRating = Ratings.Gold; currentEntry.hops = 3; + */ + + // TODO: still not added to the discovery service. currentEntry.note = ''; response.push(currentEntry); diff --git a/static/skywire-manager-src/src/app/services/vpn-client.service.ts b/static/skywire-manager-src/src/app/services/vpn-client.service.ts index 677c4ff508..7f789b1ff7 100644 --- a/static/skywire-manager-src/src/app/services/vpn-client.service.ts +++ b/static/skywire-manager-src/src/app/services/vpn-client.service.ts @@ -2,9 +2,9 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { Observable, Subscription, of, BehaviorSubject, concat, throwError } from 'rxjs'; import { mergeMap, delay, retryWhen, take, catchError, map } from 'rxjs/operators'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { ApiService } from './api.service'; +import { ApiService, RequestOptions } from './api.service'; import { AppsService } from './apps.service'; import { VpnServer } from './vpn-client-discovery.service'; import { ManualVpnServerData } from '../components/vpn/pages/vpn-server-list/add-vpn-server/add-vpn-server.component'; @@ -14,7 +14,6 @@ import { environment } from 'src/environments/environment'; import { SnackbarService } from './snackbar.service'; import { processServiceError } from '../utils/errors'; import { OperationError } from '../utils/operation-error'; -import { TransportService } from './transport.service'; /** * States in which the VPN client app of the local visor can be. @@ -73,6 +72,14 @@ export class VpnClientAppData { * Data transmission stats, if the app is running. */ connectionData: VpnClientConnectionsData; + /** + * Min hops the reoutes must have. + */ + minHops: number; + /** + * Error msg returned by the vpn-client app, for which the last excecution was stopped. + */ + lasErrorMsg: string; } /** @@ -161,6 +168,8 @@ export class VpnClientService { private requestedServer: LocalServerData = null; // Password provided with requestedServer. private requestedPassword: string = null; + // If the continuous automatic updates were stopped due to a problem. + private updatesStopped = false; // Data transmission history values. private downloadSpeedHistory: number[]; @@ -179,7 +188,6 @@ export class VpnClientService { private vpnSavedDataService: VpnSavedDataService, private http: HttpClient, private snackbarService: SnackbarService, - private transportService: TransportService, ) { // Set the initial state. PerformingInitialCheck will be replaced when getting the state // for the first time. The busy state too, to start being able to perform other operations. @@ -207,6 +215,9 @@ export class VpnClientService { // PK is provided, go to an error page. if (nodeKey !== this.nodeKey) { this.router.navigate(['vpn', 'unavailable'], { queryParams: {problem: 'pkChange'} }); + } else if (this.updatesStopped) { + this.updatesStopped = false; + this.updateData(); } } } @@ -233,7 +244,7 @@ export class VpnClientService { start(): boolean { // Continue only if the service is not busy and the VPN is stopped. if (!this.working && this.lastServiceState < 20) { - this.checkBeforeChangingAppState(true); + this.changeAppState(true); return true; } @@ -248,7 +259,7 @@ export class VpnClientService { stop(): boolean { // Continue only if the service is not busy and the VPN is running. if (!this.working && this.lastServiceState >= 20 && this.lastServiceState < 200) { - this.checkBeforeChangingAppState(false); + this.changeAppState(false); return true; } @@ -450,11 +461,11 @@ export class VpnClientService { } /** - * Checks and configures the local visor to make it posible to start or stop the VPN and then - * starts or stops it. - * @param startApp If the VPN must be started or stopped. + * Starts or stops the VPN client app in the local visor, which starts or stops the VPN + * protection. + * @param startApp If the app must be started or stopped. */ - private checkBeforeChangingAppState(startApp: boolean) { + private changeAppState(startApp: boolean) { // Cancel if the service is busy. if (this.working) { return; @@ -465,65 +476,6 @@ export class VpnClientService { this.working = true; this.sendUpdate(); - // If the VPN is going to be stopped, just continue with the process, as no config is needed. - if (!startApp) { - this.changeAppState(startApp); - - return; - } - - if (this.dataSubscription) { - this.dataSubscription.unsubscribe(); - } - - // Get the current general state of the local visor. - this.dataSubscription = this.apiService.get(`visors/${this.nodeKey}`).pipe( - mergeMap(nodeInfo => { - // Check if the local visor already has a transport for connecting with the server. - let transportFound = false; - if (nodeInfo.transports && nodeInfo.transports.length > 0) { - (nodeInfo.transports as any[]).forEach(transport => { - if (transport.remote_pk === this.vpnSavedDataService.currentServer.pk) { - transportFound = true; - } - }); - } - - // If the transport was found, do nothing. - if (transportFound) { - return of(null); - } - - // If the transport was not found, create one. - return this.transportService.create( - this.nodeKey, - this.vpnSavedDataService.currentServer.pk, - 'dmsg', - ); - }), retryWhen(errors => - concat(errors.pipe(delay(this.standardWaitTime), take(3)), errors.pipe(mergeMap(err => throwError(err)))) - ), - ).subscribe(() => { - // Continue with the process. - this.changeAppState(startApp); - }, (err: OperationError) => { - // Inform about the error. - err = processServiceError(err); - this.snackbarService.showError('vpn.status-page.problem-connecting-error', null, false, err.originalServerErrorMsg); - - // Make the service work normally again. - this.working = false; - this.sendUpdate(); - this.updateData(); - }); - } - - /** - * Starts or stops the VPN client app in the local visor, which starts or stops the VPN - * protection. Must be called only by checkBeforeChangingAppState. - * @param startApp If the app must be started or stopped. - */ - private changeAppState(startApp: boolean) { const data = { status: 1 }; if (startApp) { @@ -618,14 +570,32 @@ export class VpnClientService { this.continuousUpdateSubscription.unsubscribe(); } + let retries = 0; + this.continuousUpdateSubscription = of(0).pipe( delay(delayMs), mergeMap(() => this.getVpnClientState()), - retryWhen(errors => concat( - // During the initial check, retry only a few times. - errors.pipe(delay(this.standardWaitTime), take(this.lastServiceState === VpnServiceStates.PerformingInitialCheck ? 5 : 1000000000)), - throwError('') - )), + retryWhen(err => { + return err.pipe(mergeMap((error: OperationError) => { + error = processServiceError(error); + // If the problem was because the user is not authorized, don't retry. + if ( + error.originalError && + (error.originalError as HttpErrorResponse).status && + (error.originalError as HttpErrorResponse).status === 401 + ) { + return throwError(error); + } + + // Retry a few times if this is the first connection, or indefinitely if it is not. + if (this.lastServiceState !== VpnServiceStates.PerformingInitialCheck || retries < 4) { + retries += 1; + return of(error).pipe(delay(this.standardWaitTime)); + } else { + return throwError(error); + } + })); + }), ).subscribe(appData => { if (appData) { // Remove the busy state of the initial check. @@ -649,14 +619,27 @@ export class VpnClientService { // Go to the error page, as it was not possible to connect with the local visor. this.router.navigate(['vpn', 'unavailable']); this.nodeKey = null; + this.updatesStopped = true; } // Program the next update. this.continuallyUpdateData(this.standardWaitTime); - }, () => { - // Go to the error page, as it was not possible to connect with the local visor. - this.router.navigate(['vpn', 'unavailable']); - this.nodeKey = null; + }, error => { + error = processServiceError(error); + if ( + error.originalError && + (error.originalError as HttpErrorResponse).status && + (error.originalError as HttpErrorResponse).status === 401 + ) { + // If the problem was because the user is not authorized, do nothing. The connection + // code should have redirected the user to the login page. + } else { + // Go to the error page, as it was not possible to connect with the local visor. + this.router.navigate(['vpn', 'unavailable']); + this.nodeKey = null; + } + + this.updatesStopped = true; }); } @@ -675,13 +658,16 @@ export class VpnClientService { private getVpnClientState(): Observable { let vpnClientData: VpnClientAppData; + const options = new RequestOptions(); + options.vpnKeyForAuth = this.nodeKey; + // Get the basic info about the local visor. - return this.apiService.get(`visors/${this.nodeKey}`).pipe(mergeMap(nodeInfo => { + return this.apiService.get(`visors/${this.nodeKey}/summary`, options).pipe(mergeMap(nodeInfo => { let appData: any; // Get the data of the VPN client app. - if (nodeInfo && nodeInfo.apps && (nodeInfo.apps as any[]).length > 0) { - (nodeInfo.apps as any[]).forEach(value => { + if (nodeInfo && nodeInfo.overview && nodeInfo.overview.apps && (nodeInfo.overview.apps as any[]).length > 0) { + (nodeInfo.overview.apps as any[]).forEach(value => { if (value.name === this.vpnClientAppName) { appData = value; } @@ -691,17 +677,21 @@ export class VpnClientService { // Get the required data from the app properties. if (appData) { vpnClientData = new VpnClientAppData(); - vpnClientData.running = appData.status !== 0; + vpnClientData.running = appData.status === 1; vpnClientData.appState = AppState.Stopped; - if (appData.detailed_status === AppState.Connecting) { - vpnClientData.appState = AppState.Connecting; - } else if (appData.detailed_status === AppState.Running) { - vpnClientData.appState = AppState.Running; - } else if (appData.detailed_status === AppState.ShuttingDown) { - vpnClientData.appState = AppState.ShuttingDown; - } else if (appData.detailed_status === AppState.Reconnecting) { - vpnClientData.appState = AppState.Reconnecting; + if (vpnClientData.running) { + if (appData.detailed_status === AppState.Connecting) { + vpnClientData.appState = AppState.Connecting; + } else if (appData.detailed_status === AppState.Running) { + vpnClientData.appState = AppState.Running; + } else if (appData.detailed_status === AppState.ShuttingDown) { + vpnClientData.appState = AppState.ShuttingDown; + } else if (appData.detailed_status === AppState.Reconnecting) { + vpnClientData.appState = AppState.Reconnecting; + } + } else if (appData.status === 2) { + vpnClientData.lasErrorMsg = appData.detailed_status; } vpnClientData.killswitch = false; @@ -712,16 +702,22 @@ export class VpnClientService { vpnClientData.serverPk = appData.args[i + 1]; } - if (appData.args[i] === '-killswitch' && i + 1 < appData.args.length) { - vpnClientData.killswitch = (appData.args[i + 1] as string).toLowerCase() === 'true'; + if (appData.args[i].toLowerCase().includes('-killswitch')) { + vpnClientData.killswitch = (appData.args[i] as string).toLowerCase().includes('true'); } } } } + // Get the min hops value. + vpnClientData.minHops = nodeInfo.min_hops ? nodeInfo.min_hops : 0; + // Get the data transmission data, is the app is running. if (vpnClientData && vpnClientData.running) { - return this.apiService.get(`visors/${this.nodeKey}/apps/${this.vpnClientAppName}/connections`); + const o = new RequestOptions(); + o.vpnKeyForAuth = this.nodeKey; + + return this.apiService.get(`visors/${this.nodeKey}/apps/${this.vpnClientAppName}/connections`, o); } return of(null); diff --git a/static/skywire-manager-src/src/assets/i18n/en.json b/static/skywire-manager-src/src/assets/i18n/en.json index 46bf47175b..0b4c584d80 100644 --- a/static/skywire-manager-src/src/assets/i18n/en.json +++ b/static/skywire-manager-src/src/assets/i18n/en.json @@ -668,6 +668,7 @@ }, "connection-info" : { + "state-title": "Your connection is currently:", "state-connecting": "Connecting", "state-connecting-info": "The VPN protection is being activated.", "state-connected": "Connected", @@ -688,6 +689,7 @@ "start-title": "Start VPN", "no-server": "No server selected!", "disconnect": "Disconnect", + "last-error": "Last error:", "disconnect-confirmation": "Are you sure you want to stop the VPN protection?", "entered-manually": "Entered manually", "upload-info": "Uploaded data stats.", @@ -722,6 +724,7 @@ "connect-without-password": "Connect without password", "connect-without-password-confirmation": "The connection will be made without the password. Are you sure you want to continue?", "connect-using-password": "Connect using a password", + "connect-using-another-password": "Connect using another password", "edit-name": "Custom name", "edit-label": "Custom note", "make-favorite": "Make favorite", @@ -848,6 +851,8 @@ "get-ip-info": "When active, the application will use external services to obtain information about the current IP.", "data-units": "Data units", "data-units-info": "Allows to select the units that will be used to display the data transmission statistics.", + "minimum-hops": "Minimum hops", + "minimum-hops-info": "Allows to set the minimum number of hops the connections must pass through other Skywire visors before reaching the final destination.", "setting-on": "On", "setting-off": "Off", "working-warning": "The system is busy. Please wait for the previous operation to finish.", diff --git a/static/skywire-manager-src/src/assets/i18n/es.json b/static/skywire-manager-src/src/assets/i18n/es.json index 488e816960..9b968c3f4b 100644 --- a/static/skywire-manager-src/src/assets/i18n/es.json +++ b/static/skywire-manager-src/src/assets/i18n/es.json @@ -668,6 +668,7 @@ }, "connection-info" : { + "state-title": "El estado de tu conexión es actualmente:", "state-connecting": "Conectando", "state-connecting-info": "Se está activando la protección VPN.", "state-connected": "Conectado", @@ -688,6 +689,7 @@ "start-title": "Iniciar VPN", "no-server": "¡Ningún servidor seleccionado!", "disconnect": "Desconectar", + "last-error": "Último error:", "disconnect-confirmation": "¿Realmente desea detener la protección VPN?", "entered-manually": "Ingresado manualmente", "upload-info": "Estadísticas de datos subidos.", @@ -722,6 +724,7 @@ "connect-without-password": "Conectarse sin contraseña", "connect-without-password-confirmation": "La conexión se realizará sin la contraseña. ¿Seguro que desea continuar?", "connect-using-password": "Conectarse usando una contraseña", + "connect-using-another-password": "Conectarse usando otra contraseña", "edit-name": "Nombre personalizado", "edit-label": "Nota personalizada", "make-favorite": "Hacer favorito", @@ -848,6 +851,8 @@ "get-ip-info": "Cuando está activa, la aplicación utilizará servicios externos para obtener información sobre la IP actual.", "data-units": "Unidades de datos", "data-units-info": "Permite seleccionar las unidades que se utilizarán para mostrar las estadísticas de transmisión de datos.", + "minimum-hops": "Saltos mínimos", + "minimum-hops-info": "Permite configurar la cantidad mínima de saltos que la conexión deberá realizar a través de otros visores de Skywire antes de alcanzar el destino final.", "setting-on": "Encendido", "setting-off": "Apagado", "working-warning": "El sistema está ocupado. Por favor, espere a que finalice la operación anterior.", diff --git a/static/skywire-manager-src/src/assets/i18n/es_base.json b/static/skywire-manager-src/src/assets/i18n/es_base.json index 1931abc858..a98a9fee81 100644 --- a/static/skywire-manager-src/src/assets/i18n/es_base.json +++ b/static/skywire-manager-src/src/assets/i18n/es_base.json @@ -668,6 +668,7 @@ }, "connection-info" : { + "state-title": "Your connection is currently:", "state-connecting": "Connecting", "state-connecting-info": "The VPN protection is being activated.", "state-connected": "Connected", @@ -688,6 +689,7 @@ "start-title": "Start VPN", "no-server": "No server selected!", "disconnect": "Disconnect", + "last-error": "Last error:", "disconnect-confirmation": "Are you sure you want to stop the VPN protection?", "entered-manually": "Entered manually", "upload-info": "Uploaded data stats.", @@ -722,6 +724,7 @@ "connect-without-password": "Connect without password", "connect-without-password-confirmation": "The connection will be made without the password. Are you sure you want to continue?", "connect-using-password": "Connect using a password", + "connect-using-another-password": "Connect using another password", "edit-name": "Custom name", "edit-label": "Custom note", "make-favorite": "Make favorite", @@ -848,6 +851,8 @@ "get-ip-info": "When active, the application will use external services to obtain information about the current IP.", "data-units": "Data units", "data-units-info": "Allows to select the units that will be used to display the data transmission statistics.", + "minimum-hops": "Minimum hops", + "minimum-hops-info": "Allows to set the minimum number of hops the connections must pass through other Skywire visors before reaching the final destination.", "setting-on": "On", "setting-off": "Off", "working-warning": "The system is busy. Please wait for the previous operation to finish.",