alt marketplace feature

re-arrange

use url

api proxy function

matt comments addressed

delete cache on marketplace load failure
This commit is contained in:
Drew Ansbacher
2022-01-31 19:46:41 -07:00
committed by Aiden McClelland
parent 691d567d31
commit 2d4ecd3096
16 changed files with 1311 additions and 515 deletions

View File

@@ -1,5 +1,9 @@
import { Injectable } from '@angular/core'
import { MarketplaceData, MarketplaceEOS, MarketplacePkg } from 'src/app/services/api/api.types'
import {
MarketplaceData,
MarketplaceEOS,
MarketplacePkg,
} from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Emver } from 'src/app/services/emver.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@@ -12,38 +16,59 @@ export class MarketplaceService {
data: MarketplaceData
eos: MarketplaceEOS
pkgs: MarketplacePkg[] = []
releaseNotes: { [id: string]: {
[version: string]: string
} } = { }
releaseNotes: {
[id: string]: {
[version: string]: string
}
} = {}
constructor (
constructor(
private readonly api: ApiService,
private readonly emver: Emver,
private readonly patch: PatchDbService,
) { }
) {}
get eosUpdateAvailable () {
return this.emver.compare(this.eos.version, this.patch.data['server-info'].version) === 1
get eosUpdateAvailable() {
return (
this.emver.compare(
this.eos.version,
this.patch.data['server-info'].version,
) === 1
)
}
async load (): Promise<void> {
const [data, eos, pkgs] = await Promise.all([
this.api.getMarketplaceData({ }),
this.api.getEos({
'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'],
}),
this.getPkgs(1, 100),
])
this.data = data
this.eos = eos
this.pkgs = pkgs
async load(): Promise<void> {
try {
const [data, eos, pkgs] = await Promise.all([
this.api.getMarketplaceData({}),
this.api.getEos({
'eos-version-compat':
this.patch.getData()['server-info']['eos-version-compat'],
}),
this.getPkgs(1, 100),
])
this.data = data
this.eos = eos
this.pkgs = pkgs
} catch (e) {
this.data = undefined
this.eos = undefined
this.pkgs = []
throw e
}
}
async getUpdates (localPkgs: { [id: string]: PackageDataEntry }) : Promise<MarketplacePkg[]> {
const idAndCurrentVersions = Object.keys(localPkgs).map(key => ({ id: key, version: localPkgs[key].manifest.version }))
async getUpdates(localPkgs: {
[id: string]: PackageDataEntry
}): Promise<MarketplacePkg[]> {
const idAndCurrentVersions = Object.keys(localPkgs).map(key => ({
id: key,
version: localPkgs[key].manifest.version,
}))
const latestPkgs = await this.api.getMarketplacePkgs({
ids: idAndCurrentVersions,
'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'],
'eos-version-compat':
this.patch.getData()['server-info']['eos-version-compat'],
})
return latestPkgs.filter(latestPkg => {
@@ -53,10 +78,11 @@ export class MarketplaceService {
})
}
async getPkg (id: string, version = '*'): Promise<MarketplacePkg> {
async getPkg(id: string, version = '*'): Promise<MarketplacePkg> {
const pkgs = await this.api.getMarketplacePkgs({
ids: [{ id, version }],
'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'],
'eos-version-compat':
this.patch.getData()['server-info']['eos-version-compat'],
})
const pkg = pkgs.find(pkg => pkg.manifest.id == id)
@@ -67,19 +93,21 @@ export class MarketplaceService {
}
}
async getReleaseNotes (id: string): Promise<void> {
async getReleaseNotes(id: string): Promise<void> {
this.releaseNotes[id] = await this.api.getReleaseNotes({ id })
}
private async getPkgs (page: number, perPage: number) : Promise<MarketplacePkg[]> {
private async getPkgs(
page: number,
perPage: number,
): Promise<MarketplacePkg[]> {
const pkgs = await this.api.getMarketplacePkgs({
page: String(page),
'per-page': String(perPage),
'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'],
'eos-version-compat':
this.patch.getData()['server-info']['eos-version-compat'],
})
return pkgs
}
}

View File

@@ -0,0 +1,24 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { MarketplacesPage } from './marketplaces.page'
import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [
{
path: '',
component: MarketplacesPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
SharingModule,
],
declarations: [MarketplacesPage],
})
export class MarketplacesPageModule {}

View File

@@ -0,0 +1,52 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons>
<ion-title>Marketplace Settings</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-item-group>
<ion-item-divider>Saved Marketplaces</ion-item-divider>
<ion-item button detail="false" (click)="presentModalAdd()">
<ion-icon slot="start" name="add" size="large" color="dark"></ion-icon>
<ion-label>
<ion-text color="dark">
<b>Add alternative marketplace</b>
</ion-text>
</ion-label>
</ion-item>
<ion-item
[button]="mp.key !== patch.data.ui.marketplace['selected-id']"
detail="false"
*ngFor="let mp of patch.data.ui.marketplace.options | keyvalue"
(click)="presentAction(mp.key)"
>
<div
*ngIf="mp.key !== patch.data.ui.marketplace['selected-id']"
slot="start"
style="padding-right: 32px"
></div>
<ion-icon
*ngIf="mp.key === patch.data.ui.marketplace['selected-id']"
slot="start"
size="large"
name="checkmark"
color="success"
></ion-icon>
<ion-label>
<h2>{{ mp.value.name }}</h2>
<p>{{ mp.value.url }}</p>
</ion-label>
<ion-note
*ngIf="mp.key === patch.data.ui.marketplace['selected-id']"
slot="end"
>
<ion-text color="success">Selected</ion-text>
</ion-note>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -0,0 +1,7 @@
.skeleton-parts {
ion-button::part(native) {
padding-inline-start: 0;
padding-inline-end: 0;
};
padding-bottom: 6px;
}

View File

@@ -0,0 +1,277 @@
import { Component } from '@angular/core'
import {
ActionSheetController,
LoadingController,
ModalController,
} from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActionSheetButton } from '@ionic/core'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
import { v4 } from 'uuid'
import { MarketplaceService } from '../../marketplace-routes/marketplace.service'
import {
DataModel,
UIData,
UIMarketplaceData,
} from '../../../services/patch-db/data-model'
@Component({
selector: 'marketplaces',
templateUrl: 'marketplaces.page.html',
styleUrls: ['marketplaces.page.scss'],
})
export class MarketplacesPage {
constructor(
private readonly api: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly modalCtrl: ModalController,
private readonly errToast: ErrorToastService,
private readonly actionCtrl: ActionSheetController,
private readonly marketplaceService: MarketplaceService,
public readonly patch: PatchDbService,
) {}
async presentModalAdd() {
const marketplaceSpec = getMarketplaceValueSpec()
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: marketplaceSpec.name,
spec: marketplaceSpec.spec,
buttons: [
{
text: 'Save for Later',
handler: (value: { url: string }) => {
this.save(value.url)
},
},
{
text: 'Save and Connect',
handler: (value: { url: string }) => {
this.saveAndConnect(value.url)
},
isSubmit: true,
},
],
},
cssClass: 'alertlike-modal',
})
await modal.present()
}
async presentAction(id: string) {
// no need to view actions if is selected marketplace
if (id === this.patch.data.ui.marketplace['selected-id']) return
const buttons: ActionSheetButton[] = [
{
text: 'Forget',
icon: 'trash',
role: 'destructive',
handler: () => {
this.delete(id)
},
},
{
text: 'Connect to marketplace',
handler: () => {
this.connect(id)
},
},
]
const action = await this.actionCtrl.create({
header: id,
subHeader: 'Manage marketplaces',
mode: 'ios',
buttons,
})
await action.present()
}
private async connect(id: string): Promise<void> {
const marketplace = JSON.parse(
JSON.stringify(this.patch.data.ui.marketplace),
)
const newMarketplace = marketplace.options[id]
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Validating Marketplace...',
cssClass: 'loader',
})
await loader.present()
try {
await this.api.getMarketplaceData({}, newMarketplace.url)
} catch (e) {
this.errToast.present({
message: `Could not connect to ${newMarketplace.url}`,
} as any)
loader.dismiss()
return
}
loader.message = 'Changing Marketplace...'
try {
marketplace['selected-id'] = id
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
} catch (e) {
this.errToast.present(e)
loader.dismiss()
}
loader.message = 'Syncing store...'
try {
await this.marketplaceService.load()
} catch (e) {
this.errToast.present({
message: `Error syncing marketplace data`,
} as any)
} finally {
loader.dismiss()
}
}
private async delete(id: string): Promise<void> {
const marketplace = JSON.parse(
JSON.stringify(this.patch.data.ui.marketplace),
)
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Deleting...',
cssClass: 'loader',
})
await loader.present()
try {
delete marketplace.options[id]
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private async save(url: string): Promise<void> {
const marketplace = JSON.parse(
JSON.stringify(this.patch.data.ui.marketplace),
) as UIMarketplaceData
// no-op on duplicates
const currentUrls = Object.values(marketplace.options).map(
u => new URL(u.url).hostname,
)
if (currentUrls.includes(new URL(url).hostname)) return
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Validating Marketplace...',
cssClass: 'loader',
})
await loader.present()
try {
const id = v4()
const { name } = await this.api.getMarketplaceData({}, url)
marketplace.options[id] = { name, url }
} catch (e) {
this.errToast.present({ message: `Could not connect to ${url}` } as any)
loader.dismiss()
return
}
loader.message = 'Saving...'
try {
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
} catch (e) {
this.errToast.present({ message: `Error saving marketplace data` } as any)
} finally {
loader.dismiss()
}
}
private async saveAndConnect(url: string): Promise<void> {
const marketplace = JSON.parse(
JSON.stringify(this.patch.data.ui.marketplace),
) as UIMarketplaceData
// no-op on duplicates
const currentUrls = Object.values(marketplace.options).map(
u => new URL(u.url).hostname,
)
if (currentUrls.includes(new URL(url).hostname)) return
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Validating Marketplace...',
cssClass: 'loader',
})
await loader.present()
try {
const id = v4()
const { name } = await this.api.getMarketplaceData({}, url)
marketplace.options[id] = { name, url }
marketplace['selected-id'] = id
} catch (e) {
this.errToast.present({ message: `Could not connect to ${url}` } as any)
loader.dismiss()
return
}
loader.message = 'Saving...'
try {
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
} catch (e) {
this.errToast.present({ message: `Error saving marketplace data` } as any)
loader.dismiss()
return
}
loader.message = 'Syncing store...'
try {
await this.marketplaceService.load()
} catch (e) {
this.errToast.present({
message: `Error syncing marketplace data`,
} as any)
} finally {
loader.dismiss()
}
}
}
function getMarketplaceValueSpec(): ValueSpecObject {
return {
type: 'object',
name: 'Add Marketplace',
'unique-by': null,
spec: {
url: {
type: 'string',
name: 'URL',
description: 'The fully-qualified URL of the alternative marketplace.',
nullable: false,
masked: false,
copyable: false,
pattern: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}`,
'pattern-description': 'Must be a valid URL',
placeholder: 'e.g. https://example.org',
},
},
}
}

View File

@@ -4,11 +4,17 @@ import { Routes, RouterModule } from '@angular/router'
const routes: Routes = [
{
path: '',
loadChildren: () => import('./server-show/server-show.module').then(m => m.ServerShowPageModule),
loadChildren: () =>
import('./server-show/server-show.module').then(
m => m.ServerShowPageModule,
),
},
{
path: 'backup',
loadChildren: () => import('./server-backup/server-backup.module').then(m => m.ServerBackupPageModule),
loadChildren: () =>
import('./server-backup/server-backup.module').then(
m => m.ServerBackupPageModule,
),
},
{
path: 'lan',
@@ -16,35 +22,60 @@ const routes: Routes = [
},
{
path: 'logs',
loadChildren: () => import('./server-logs/server-logs.module').then(m => m.ServerLogsPageModule),
loadChildren: () =>
import('./server-logs/server-logs.module').then(
m => m.ServerLogsPageModule,
),
},
{
path: 'marketplaces',
loadChildren: () =>
import('./marketplaces/marketplaces.module').then(
m => m.MarketplacesPageModule,
),
},
{
path: 'metrics',
loadChildren: () => import('./server-metrics/server-metrics.module').then(m => m.ServerMetricsPageModule),
loadChildren: () =>
import('./server-metrics/server-metrics.module').then(
m => m.ServerMetricsPageModule,
),
},
{
path: 'preferences',
loadChildren: () => import('./preferences/preferences.module').then( m => m.PreferencesPageModule),
loadChildren: () =>
import('./preferences/preferences.module').then(
m => m.PreferencesPageModule,
),
},
{
path: 'restore',
loadChildren: () => import('./restore/restore.component.module').then( m => m.RestorePageModule),
loadChildren: () =>
import('./restore/restore.component.module').then(
m => m.RestorePageModule,
),
},
{
path: 'sessions',
loadChildren: () => import('./sessions/sessions.module').then( m => m.SessionsPageModule),
loadChildren: () =>
import('./sessions/sessions.module').then(m => m.SessionsPageModule),
},
{
path: 'specs',
loadChildren: () => import('./server-specs/server-specs.module').then(m => m.ServerSpecsPageModule),
loadChildren: () =>
import('./server-specs/server-specs.module').then(
m => m.ServerSpecsPageModule,
),
},
{
path: 'ssh',
loadChildren: () => import('./ssh-keys/ssh-keys.module').then( m => m.SSHKeysPageModule),
loadChildren: () =>
import('./ssh-keys/ssh-keys.module').then(m => m.SSHKeysPageModule),
},
{
path: 'wifi',
loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiPageModule),
loadChildren: () =>
import('./wifi/wifi.module').then(m => m.WifiPageModule),
},
]
@@ -52,4 +83,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ServerRoutingModule { }
export class ServerRoutingModule {}

View File

@@ -1,5 +1,10 @@
import { Component } from '@angular/core'
import { AlertController, LoadingController, NavController, IonicSafeString } from '@ionic/angular'
import {
AlertController,
LoadingController,
NavController,
IonicSafeString,
} from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActivatedRoute } from '@angular/router'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@@ -16,7 +21,7 @@ import { map } from 'rxjs/operators'
export class ServerShowPage {
ServerStatus = ServerStatus
constructor (
constructor(
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
@@ -24,12 +29,13 @@ export class ServerShowPage {
private readonly navCtrl: NavController,
private readonly route: ActivatedRoute,
public readonly patch: PatchDbService,
) { }
) {}
async presentAlertRestart () {
async presentAlertRestart() {
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: 'Are you sure you want to restart your Embassy? It can take several minutes to come back online.',
message:
'Are you sure you want to restart your Embassy? It can take several minutes to come back online.',
buttons: [
{
text: 'Cancel',
@@ -47,11 +53,12 @@ export class ServerShowPage {
await alert.present()
}
async presentAlertShutdown () {
async presentAlertShutdown() {
const sts = this.patch.data['server-info'].status
const alert = await this.alertCtrl.create({
header: 'Warning',
message: 'Are you sure you want to power down your Embassy? This can take several minutes, and your Embassy will not come back online automatically. To power on again, You will need to physically unplug your Embassy and plug it back in.',
message:
'Are you sure you want to power down your Embassy? This can take several minutes, and your Embassy will not come back online automatically. To power on again, You will need to physically unplug your Embassy and plug it back in.',
buttons: [
{
text: 'Cancel',
@@ -69,11 +76,13 @@ export class ServerShowPage {
await alert.present()
}
async presentAlertSystemRebuild () {
async presentAlertSystemRebuild() {
const minutes = Object.keys(this.patch.data['package-data']).length * 2
const alert = await this.alertCtrl.create({
header: 'System Rebuild',
message: new IonicSafeString(`<ion-text color="warning">Important:</ion-text> This will tear down all service containers and rebuild them from scratch. This may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your Embassy.`),
message: new IonicSafeString(
`<ion-text color="warning">Important:</ion-text> This will tear down all service containers and rebuild them from scratch. This may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your Embassy.`,
),
buttons: [
{
text: 'Cancel',
@@ -91,7 +100,7 @@ export class ServerShowPage {
await alert.present()
}
private async restart () {
private async restart() {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Restarting...',
@@ -100,7 +109,7 @@ export class ServerShowPage {
await loader.present()
try {
await this.embassyApi.restartServer({ })
await this.embassyApi.restartServer({})
} catch (e) {
this.errToast.present(e)
} finally {
@@ -108,7 +117,7 @@ export class ServerShowPage {
}
}
private async shutdown () {
private async shutdown() {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Shutting down...',
@@ -117,7 +126,7 @@ export class ServerShowPage {
await loader.present()
try {
await this.embassyApi.shutdownServer({ })
await this.embassyApi.shutdownServer({})
} catch (e) {
this.errToast.present(e)
} finally {
@@ -125,7 +134,7 @@ export class ServerShowPage {
}
}
private async systemRebuild () {
private async systemRebuild() {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Hard Restarting...',
@@ -134,7 +143,7 @@ export class ServerShowPage {
await loader.present()
try {
await this.embassyApi.systemRebuild({ })
await this.embassyApi.systemRebuild({})
} catch (e) {
this.errToast.present(e)
} finally {
@@ -143,12 +152,13 @@ export class ServerShowPage {
}
settings: ServerSettings = {
'Backups': [
Backups: [
{
title: 'Create Backup',
description: 'Back up your Embassy and all its services',
icon: 'save-outline',
action: () => this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
detail: true,
disabled: of(false),
},
@@ -156,17 +166,25 @@ export class ServerShowPage {
title: 'Restore From Backup',
description: 'Restore one or more services from a prior backup',
icon: 'color-wand-outline',
action: () => this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
detail: true,
disabled: this.patch.watch$('server-info', 'status').pipe(map(status => [ServerStatus.Updated, ServerStatus.BackingUp].includes(status))),
disabled: this.patch
.watch$('server-info', 'status')
.pipe(
map(status =>
[ServerStatus.Updated, ServerStatus.BackingUp].includes(status),
),
),
},
],
'Insights': [
Insights: [
{
title: 'About',
description: 'Basic information about your Embassy',
icon: 'information-circle-outline',
action: () => this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }),
detail: true,
disabled: of(false),
},
@@ -174,7 +192,8 @@ export class ServerShowPage {
title: 'Monitor',
description: 'CPU, disk, memory, and other useful metrics',
icon: 'pulse',
action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
detail: true,
disabled: of(false),
},
@@ -182,17 +201,21 @@ export class ServerShowPage {
title: 'Logs',
description: 'Raw, unfiltered device logs',
icon: 'newspaper-outline',
action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
detail: true,
disabled: of(false),
},
],
'Settings': [
Settings: [
{
title: 'Preferences',
description: 'Device name, background tasks',
icon: 'options-outline',
action: () => this.navCtrl.navigateForward(['preferences'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['preferences'], {
relativeTo: this.route,
}),
detail: true,
disabled: of(false),
},
@@ -200,7 +223,8 @@ export class ServerShowPage {
title: 'LAN',
description: 'Access your Embassy on the Local Area Network',
icon: 'home-outline',
action: () => this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }),
detail: true,
disabled: of(false),
},
@@ -208,16 +232,28 @@ export class ServerShowPage {
title: 'SSH',
description: 'Access your Embassy from the command line',
icon: 'terminal-outline',
action: () => this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }),
detail: true,
disabled: of(false),
},
{
title: 'WiFi',
description: 'Add or remove WiFi networks',
icon: 'wifi',
action: () => this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }),
detail: true,
disabled: of(false),
},
{
title: 'Marketplace Settings',
description: 'Add or remove marketplaces',
icon: 'storefront',
action: () =>
this.navCtrl.navigateForward(['marketplaces'], {
relativeTo: this.route,
}),
detail: true,
disabled: of(false),
},
@@ -225,12 +261,15 @@ export class ServerShowPage {
title: 'Active Sessions',
description: 'View and manage device access',
icon: 'desktop-outline',
action: () => this.navCtrl.navigateForward(['sessions'], { relativeTo: this.route }),
action: () =>
this.navCtrl.navigateForward(['sessions'], {
relativeTo: this.route,
}),
detail: true,
disabled: of(false),
},
],
'Power': [
Power: [
{
title: 'Restart',
description: '',
@@ -258,7 +297,7 @@ export class ServerShowPage {
],
}
asIsOrder () {
asIsOrder() {
return 0
}
}

View File

@@ -15,13 +15,19 @@
<ion-content class="ion-padding-top">
<ion-item-group>
<!-- always -->
<ion-item>
<ion-label>
<h2>
Adding WiFi credentials to your Embassy allows you to remove the Ethernet cable and move the device anywhere you want. Embassy will automatically connect to available networks.
<a href="https://docs.start9.com/user-manual/general/wifi.html" target="_blank" rel="noreferrer">View instructions</a>
Adding WiFi credentials to your Embassy allows you to remove the
Ethernet cable and move the device anywhere you want. Embassy will
automatically connect to available networks.
<a
href="https://docs.start9.com/user-manual/general/wifi.html"
target="_blank"
rel="noreferrer"
>View instructions</a
>
</h2>
</ion-label>
</ion-item>
@@ -29,9 +35,16 @@
<ion-item-divider>Country</ion-item-divider>
<!-- not loading -->
<ion-item button detail="false" (click)="presentAlertCountry()" [disabled]="loading">
<ion-item
button
detail="false"
(click)="presentAlertCountry()"
[disabled]="loading"
>
<ion-icon slot="start" name="earth-outline" size="large"></ion-icon>
<ion-label *ngIf="wifi.country">{{ wifi.country }} - {{ this.countries[wifi.country] }}</ion-label>
<ion-label *ngIf="wifi.country"
>{{ wifi.country }} - {{ this.countries[wifi.country] }}</ion-label
>
<ion-label *ngIf="!wifi.country">Select Country</ion-label>
</ion-item>
@@ -40,20 +53,26 @@
<ion-item-divider>Saved Networks</ion-item-divider>
<ion-item *ngFor="let entry of ['', '']" class="skeleton-parts">
<ion-button slot="start" fill="clear">
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
<ion-skeleton-text
animated
style="width: 30px; height: 30px; border-radius: 0"
></ion-skeleton-text>
</ion-button>
<ion-label>
<ion-skeleton-text animated style="width: 18%;"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 18%"></ion-skeleton-text>
</ion-label>
</ion-item>
<ion-item-divider>Available Networks</ion-item-divider>
<ion-item *ngFor="let entry of ['', '']" class="skeleton-parts">
<ion-button slot="start" fill="clear">
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
<ion-skeleton-text
animated
style="width: 30px; height: 30px; border-radius: 0"
></ion-skeleton-text>
</ion-button>
<ion-label>
<ion-skeleton-text animated style="width: 18%;"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 18%"></ion-skeleton-text>
</ion-label>
</ion-item>
</ng-container>
@@ -61,29 +80,88 @@
<!-- not loading -->
<ng-container *ngIf="!loading && wifi.country">
<ion-item-divider>Saved Networks</ion-item-divider>
<ion-item button detail="false" *ngFor="let ssid of wifi.ssids | keyvalue" (click)="presentAction(ssid.key)">
<div *ngIf="ssid.key !== wifi.connected" slot="start" style="padding-right: 32px;"></div>
<ion-icon *ngIf="ssid.key === wifi.connected" slot="start" size="large" name="checkmark" color="success"></ion-icon>
<ion-item
button
detail="false"
*ngFor="let ssid of wifi.ssids | keyvalue"
(click)="presentAction(ssid.key)"
>
<div
*ngIf="ssid.key !== wifi.connected"
slot="start"
style="padding-right: 32px"
></div>
<ion-icon
*ngIf="ssid.key === wifi.connected"
slot="start"
size="large"
name="checkmark"
color="success"
></ion-icon>
<ion-label>{{ ssid.key }}</ion-label>
<img *ngIf="ssid.value > 0 && ssid.value < 5" slot="end" src="assets/img/icons/wifi-1.png" style="max-width: 32px;" />
<img *ngIf="ssid.value >= 5 && ssid.value < 50" slot="end" src="assets/img/icons/wifi-1.png" style="max-width: 32px;" />
<img *ngIf="ssid.value >= 50 && ssid.value < 90" slot="end" src="assets/img/icons/wifi-2.png" style="max-width: 32px;" />
<img *ngIf="ssid.value >= 90" slot="end" src="assets/img/icons/wifi-3.png" style="max-width: 32px;" />
<img
*ngIf="ssid.value > 0 && ssid.value < 5"
slot="end"
src="assets/img/icons/wifi-1.png"
style="max-width: 32px"
/>
<img
*ngIf="ssid.value >= 5 && ssid.value < 50"
slot="end"
src="assets/img/icons/wifi-1.png"
style="max-width: 32px"
/>
<img
*ngIf="ssid.value >= 50 && ssid.value < 90"
slot="end"
src="assets/img/icons/wifi-2.png"
style="max-width: 32px"
/>
<img
*ngIf="ssid.value >= 90"
slot="end"
src="assets/img/icons/wifi-3.png"
style="max-width: 32px"
/>
</ion-item>
<ion-item-divider>Available Networks</ion-item-divider>
<ion-item button detail="false" *ngFor="let avWifi of wifi['available-wifi']" (click)="presentModalAdd(avWifi.ssid, !!avWifi.security.length)">
<ion-icon slot="start" name="add" size="large"></ion-icon>
<ion-item
button
detail="false"
*ngFor="let avWifi of wifi['available-wifi']"
(click)="presentModalAdd(avWifi.ssid, !!avWifi.security.length)"
>
<ion-label>{{ avWifi.ssid }}</ion-label>
<img *ngIf="avWifi.strength < 5" slot="end" src="assets/img/icons/wifi-1.png" style="max-width: 32px;" />
<img *ngIf="avWifi.strength >= 5 && avWifi.strength < 50" slot="end" src="assets/img/icons/wifi-1.png" style="max-width: 32px;" />
<img *ngIf="avWifi.strength >= 50 && avWifi.strength < 90" slot="end" src="assets/img/icons/wifi-2.png" style="max-width: 32px;" />
<img *ngIf="avWifi.strength >= 90" slot="end" src="assets/img/icons/wifi-3.png" style="max-width: 32px;" />
<img
*ngIf="avWifi.strength < 5"
slot="end"
src="assets/img/icons/wifi-1.png"
style="max-width: 32px"
/>
<img
*ngIf="avWifi.strength >= 5 && avWifi.strength < 50"
slot="end"
src="assets/img/icons/wifi-1.png"
style="max-width: 32px"
/>
<img
*ngIf="avWifi.strength >= 50 && avWifi.strength < 90"
slot="end"
src="assets/img/icons/wifi-2.png"
style="max-width: 32px"
/>
<img
*ngIf="avWifi.strength >= 90"
slot="end"
src="assets/img/icons/wifi-3.png"
style="max-width: 32px"
/>
</ion-item>
<ion-item button detail="false" (click)="presentModalAdd()">
<ion-icon slot="start" name="add" size="large"></ion-icon>
<ion-label>Other</ion-label>
<ion-label>Join other network</ion-label>
</ion-item>
</ng-container>
</ion-item-group>
</ion-content>
</ion-content>

View File

@@ -1,5 +1,11 @@
import { Component } from '@angular/core'
import { ActionSheetController, AlertController, LoadingController, ModalController, ToastController } from '@ionic/angular'
import {
ActionSheetController,
AlertController,
LoadingController,
ModalController,
ToastController,
} from '@ionic/angular'
import { AlertInput } from '@ionic/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActionSheetButton } from '@ionic/core'
@@ -17,10 +23,12 @@ import { ConfigService } from 'src/app/services/config.service'
})
export class WifiPage {
loading = true
wifi: RR.GetWifiRes = { } as any
countries = require('../../../util/countries.json') as { [key: string]: string }
wifi: RR.GetWifiRes = {} as any
countries = require('../../../util/countries.json') as {
[key: string]: string
}
constructor (
constructor(
private readonly api: ApiService,
private readonly toastCtrl: ToastController,
private readonly alertCtrl: AlertController,
@@ -29,9 +37,9 @@ export class WifiPage {
private readonly errToast: ErrorToastService,
private readonly actionCtrl: ActionSheetController,
private readonly config: ConfigService,
) { }
) {}
async ngOnInit () {
async ngOnInit() {
try {
await this.getWifi()
} catch (e) {
@@ -41,24 +49,24 @@ export class WifiPage {
}
}
async getWifi (timeout?: number): Promise<void> {
this.wifi = await this.api.getWifi({ }, timeout)
async getWifi(timeout?: number): Promise<void> {
this.wifi = await this.api.getWifi({}, timeout)
if (!this.wifi.country) {
await this.presentAlertCountry()
}
}
async presentAlertCountry (): Promise<void> {
async presentAlertCountry(): Promise<void> {
if (!this.config.isLan) {
const alert = await this.alertCtrl.create({
header: 'Cannot Complete Action',
message: 'You must be connected to your Emassy via LAN to change the country.',
message:
'You must be connected to your Emassy via LAN to change the country.',
buttons: [
{
text: 'OK',
role: 'cancel',
},
],
cssClass: 'wide-alert enter-click',
})
@@ -66,19 +74,22 @@ export class WifiPage {
return
}
const inputs: AlertInput[] = Object.entries(this.countries).map(([country, fullName]) => {
return {
name: fullName,
type: 'radio',
label: `${country} - ${fullName}`,
value: country,
checked: country === this.wifi.country,
}
})
const inputs: AlertInput[] = Object.entries(this.countries).map(
([country, fullName]) => {
return {
name: fullName,
type: 'radio',
label: `${country} - ${fullName}`,
value: country,
checked: country === this.wifi.country,
}
},
)
const alert = await this.alertCtrl.create({
header: 'Select Country',
message: 'Warning: Changing the country will delete all saved networks from the Embassy.',
subHeader:
'Warning: Changing the country will delete all saved networks from the Embassy.',
inputs,
buttons: [
{
@@ -92,12 +103,12 @@ export class WifiPage {
},
},
],
cssClass: 'wide-alert enter-click',
cssClass: 'wide-alert enter-click select-warning',
})
await alert.present()
}
async presentModalAdd (ssid?: string, needsPW: boolean = true) {
async presentModalAdd(ssid?: string, needsPW: boolean = true) {
const wifiSpec = getWifiValueSpec(ssid, needsPW)
const modal = await this.modalCtrl.create({
component: GenericFormPage,
@@ -107,13 +118,13 @@ export class WifiPage {
buttons: [
{
text: 'Save for Later',
handler: async (value: { ssid: string, password: string }) => {
handler: async (value: { ssid: string; password: string }) => {
await this.save(value.ssid, value.password)
},
},
{
text: 'Save and Connect',
handler: async (value: { ssid: string, password: string }) => {
handler: async (value: { ssid: string; password: string }) => {
await this.saveAndConnect(value.ssid, value.password)
},
isSubmit: true,
@@ -125,7 +136,7 @@ export class WifiPage {
await modal.present()
}
async presentAction (ssid: string) {
async presentAction(ssid: string) {
const buttons: ActionSheetButton[] = [
{
text: 'Forget',
@@ -138,15 +149,13 @@ export class WifiPage {
]
if (ssid !== this.wifi.connected) {
buttons.unshift(
{
text: 'Connect',
icon: 'wifi',
handler: () => {
this.connect(ssid)
},
buttons.unshift({
text: 'Connect',
icon: 'wifi',
handler: () => {
this.connect(ssid)
},
)
})
}
const action = await this.actionCtrl.create({
@@ -159,7 +168,7 @@ export class WifiPage {
await action.present()
}
private async setCountry (country: string): Promise<void> {
private async setCountry(country: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
cssClass: 'loader',
@@ -177,7 +186,10 @@ export class WifiPage {
}
}
private async confirmWifi (ssid: string, deleteOnFailure = false): Promise<void> {
private async confirmWifi(
ssid: string,
deleteOnFailure = false,
): Promise<void> {
const timeout = 4000
const maxAttempts = 5
let attempts = 0
@@ -210,10 +222,11 @@ export class WifiPage {
}
}
private async presentAlertSuccess (ssid: string): Promise<void> {
private async presentAlertSuccess(ssid: string): Promise<void> {
const alert = await this.alertCtrl.create({
header: `Connected to "${ssid}"`,
message: 'Note. It may take several minutes to an hour for your Embassy to reconnect over Tor.',
message:
'Note. It may take several minutes to an hour for your Embassy to reconnect over Tor.',
buttons: [
{
text: 'Ok',
@@ -226,7 +239,7 @@ export class WifiPage {
await alert.present()
}
private async presentToastFail (): Promise<void> {
private async presentToastFail(): Promise<void> {
const toast = await this.toastCtrl.create({
header: 'Failed to connect:',
message: `Check credentials and try again`,
@@ -247,7 +260,7 @@ export class WifiPage {
await toast.present()
}
private async connect (ssid: string): Promise<void> {
private async connect(ssid: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Connecting. This could take a while...',
@@ -265,7 +278,7 @@ export class WifiPage {
}
}
private async delete (ssid: string): Promise<void> {
private async delete(ssid: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Deleting...',
@@ -284,7 +297,7 @@ export class WifiPage {
}
}
private async save (ssid: string, password: string): Promise<void> {
private async save(ssid: string, password: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
@@ -307,7 +320,7 @@ export class WifiPage {
}
}
private async saveAndConnect (ssid: string, password: string): Promise<void> {
private async saveAndConnect(ssid: string, password: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Connecting. This could take a while...',
@@ -324,7 +337,6 @@ export class WifiPage {
})
await this.confirmWifi(ssid, true)
} catch (e) {
this.errToast.present(e)
} finally {
@@ -333,11 +345,15 @@ export class WifiPage {
}
}
function getWifiValueSpec (ssid?: string, needsPW: boolean = true): ValueSpecObject {
function getWifiValueSpec(
ssid?: string,
needsPW: boolean = true,
): ValueSpecObject {
return {
type: 'object',
name: 'WiFi Credentials',
description: 'Enter the network SSID and password. You can connect now or save the network for later.',
description:
'Enter the network SSID and password. You can connect now or save the network for later.',
'unique-by': null,
spec: {
ssid: {
@@ -358,4 +374,3 @@ function getWifiValueSpec (ssid?: string, needsPW: boolean = true): ValueSpecObj
},
}
}