mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
feat: enable strictNullChecks
feat: enable `noImplicitAny` chore: remove sync data access fix loading package data for affected dependencies chore: properly get alt marketplace data update patchdb client to allow for emit on undefined values
This commit is contained in:
@@ -23,40 +23,47 @@ export class LogsPage {
|
||||
scrollToBottomButton = false
|
||||
isOnBottom = true
|
||||
|
||||
constructor (
|
||||
private readonly api: ApiService,
|
||||
) { }
|
||||
constructor(private readonly api: ApiService) {}
|
||||
|
||||
ngOnInit () {
|
||||
ngOnInit() {
|
||||
this.getLogs()
|
||||
}
|
||||
|
||||
async getLogs () {
|
||||
async getLogs() {
|
||||
try {
|
||||
// get logs
|
||||
const logs = await this.fetch()
|
||||
if (!logs.length) return
|
||||
|
||||
if (!logs?.length) return
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const beforeContainerHeight = container.scrollHeight
|
||||
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
|
||||
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
|
||||
const beforeContainerHeight = container?.scrollHeight || 0
|
||||
const newLogs = document.getElementById('template')?.cloneNode(true)
|
||||
|
||||
container.prepend(newLogs)
|
||||
const afterContainerHeight = container.scrollHeight
|
||||
if (!(newLogs instanceof HTMLElement)) return
|
||||
|
||||
newLogs.innerHTML =
|
||||
logs
|
||||
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
|
||||
.join('\n') + (logs.length ? '\n' : '')
|
||||
container?.prepend(newLogs)
|
||||
|
||||
const afterContainerHeight = container?.scrollHeight || 0
|
||||
|
||||
// scroll down
|
||||
scrollBy(0, afterContainerHeight - beforeContainerHeight)
|
||||
this.content.scrollToPoint(0, afterContainerHeight - beforeContainerHeight)
|
||||
this.content.scrollToPoint(
|
||||
0,
|
||||
afterContainerHeight - beforeContainerHeight,
|
||||
)
|
||||
|
||||
if (logs.length < this.limit) {
|
||||
this.needInfinite = false
|
||||
}
|
||||
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async fetch (isBefore: boolean = true) {
|
||||
async fetch(isBefore: boolean = true) {
|
||||
try {
|
||||
const cursor = isBefore ? this.startCursor : this.endCursor
|
||||
|
||||
@@ -81,33 +88,40 @@ export class LogsPage {
|
||||
}
|
||||
}
|
||||
|
||||
async loadMore () {
|
||||
async loadMore() {
|
||||
try {
|
||||
this.loadingMore = true
|
||||
const logs = await this.fetch(false)
|
||||
if (!logs.length) return this.loadingMore = false
|
||||
|
||||
if (!logs?.length) return (this.loadingMore = false)
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
|
||||
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
|
||||
container.append(newLogs)
|
||||
const newLogs = document.getElementById('template')?.cloneNode(true)
|
||||
|
||||
if (!(newLogs instanceof HTMLElement)) return
|
||||
|
||||
newLogs.innerHTML =
|
||||
logs
|
||||
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
|
||||
.join('\n') + (logs.length ? '\n' : '')
|
||||
container?.append(newLogs)
|
||||
this.loadingMore = false
|
||||
this.scrollEvent()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
scrollEvent () {
|
||||
scrollEvent() {
|
||||
const buttonDiv = document.getElementById('button-div')
|
||||
this.isOnBottom = buttonDiv.getBoundingClientRect().top < window.innerHeight
|
||||
this.isOnBottom =
|
||||
!!buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
scrollToBottom() {
|
||||
this.content.scrollToBottom(500)
|
||||
}
|
||||
|
||||
async loadData (e: any): Promise<void> {
|
||||
async loadData(e: any): Promise<void> {
|
||||
await this.getLogs()
|
||||
e.target.complete()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,17 +25,14 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async restart(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
return null
|
||||
}
|
||||
|
||||
async forgetDrive(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
return null
|
||||
}
|
||||
|
||||
async repairDisk(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
return null
|
||||
}
|
||||
|
||||
async getLogs(params: GetLogsReq): Promise<GetLogsRes> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { HttpError, RpcError } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
@@ -12,6 +12,8 @@ export class HttpService {
|
||||
const res = await this.httpRequest<RPCResponse<T>>(options)
|
||||
if (isRpcError(res)) throw new RpcError(res.error)
|
||||
if (isRpcSuccess(res)) return res.result
|
||||
|
||||
throw new Error('Unknown RPC response')
|
||||
}
|
||||
|
||||
async httpRequest<T>(body: RPCOptions): Promise<T> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '../../services/marketplace.service'
|
||||
|
||||
@Component({
|
||||
@@ -9,7 +10,7 @@ import { AbstractMarketplaceService } from '../../services/marketplace.service'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReleaseNotesComponent {
|
||||
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
private readonly pkgId = getPkgId(this.route)
|
||||
|
||||
private selected: string | null = null
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export class AdditionalComponent {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Versions',
|
||||
inputs: this.pkg.versions
|
||||
.sort((a, b) => -1 * this.emver.compare(a, b))
|
||||
.sort((a, b) => -1 * (this.emver.compare(a, b) || 0))
|
||||
.map(v => ({
|
||||
name: v, // for CSS
|
||||
type: 'radio',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NgModule, Pipe, PipeTransform } from '@angular/core'
|
||||
import Fuse from 'fuse.js/dist/fuse.min.js'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
import { MarketplacePkg } from '../types/marketplace-pkg'
|
||||
import { MarketplaceManifest } from '../types/marketplace-manifest'
|
||||
|
||||
@@ -15,5 +15,8 @@ export abstract class AbstractMarketplaceService {
|
||||
|
||||
abstract getPackageMarkdown(type: string, pkgId: string): Observable<string>
|
||||
|
||||
abstract getPackage(id: string, version: string): Observable<MarketplacePkg>
|
||||
abstract getPackage(
|
||||
id: string,
|
||||
version: string,
|
||||
): Observable<MarketplacePkg | null>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonInput, ModalController } from '@ionic/angular'
|
||||
import { DiskInfo, CifsBackupTarget, DiskBackupTarget } from 'src/app/services/api/api.service'
|
||||
import {
|
||||
DiskInfo,
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.service'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
|
||||
@Component({
|
||||
@@ -21,26 +25,27 @@ export class PasswordPage {
|
||||
passwordVer = ''
|
||||
unmasked2 = false
|
||||
|
||||
constructor (
|
||||
private modalController: ModalController,
|
||||
) { }
|
||||
constructor(private modalController: ModalController) {}
|
||||
|
||||
ngAfterViewInit () {
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.elem.setFocus(), 400)
|
||||
}
|
||||
|
||||
async verifyPw () {
|
||||
if (!this.target || !this.target['embassy-os']) this.pwError = 'No recovery target' // unreachable
|
||||
async verifyPw() {
|
||||
if (!this.target || !this.target['embassy-os'])
|
||||
this.pwError = 'No recovery target' // unreachable
|
||||
|
||||
try {
|
||||
argon2.verify(this.target['embassy-os']['password-hash'], this.password)
|
||||
const passwordHash = this.target['embassy-os']?.['password-hash'] || ''
|
||||
|
||||
argon2.verify(passwordHash, this.password)
|
||||
this.modalController.dismiss({ password: this.password }, 'success')
|
||||
} catch (e) {
|
||||
this.pwError = 'Incorrect password provided'
|
||||
}
|
||||
}
|
||||
|
||||
async submitPw () {
|
||||
async submitPw() {
|
||||
this.validate()
|
||||
if (this.password !== this.passwordVer) {
|
||||
this.verError = '*passwords do not match'
|
||||
@@ -50,8 +55,8 @@ export class PasswordPage {
|
||||
this.modalController.dismiss({ password: this.password }, 'success')
|
||||
}
|
||||
|
||||
validate () {
|
||||
if (!!this.target) return this.pwError = ''
|
||||
validate() {
|
||||
if (!!this.target) return (this.pwError = '')
|
||||
|
||||
if (this.passwordVer) {
|
||||
this.checkVer()
|
||||
@@ -64,11 +69,12 @@ export class PasswordPage {
|
||||
}
|
||||
}
|
||||
|
||||
checkVer () {
|
||||
this.verError = this.password !== this.passwordVer ? 'Passwords do not match' : ''
|
||||
checkVer() {
|
||||
this.verError =
|
||||
this.password !== this.passwordVer ? 'Passwords do not match' : ''
|
||||
}
|
||||
|
||||
cancel () {
|
||||
cancel() {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,19 +16,19 @@ export class ProdKeyModal {
|
||||
productKey = ''
|
||||
unmasked = false
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
private readonly modalController: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly httpService: HttpService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
ngAfterViewInit () {
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.elem.setFocus(), 400)
|
||||
}
|
||||
|
||||
async verifyProductKey () {
|
||||
if (!this.productKey) return
|
||||
async verifyProductKey() {
|
||||
if (!this.productKey || !this.target.logicalname) return
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Verifying Product Key',
|
||||
@@ -48,7 +48,7 @@ export class ProdKeyModal {
|
||||
}
|
||||
}
|
||||
|
||||
cancel () {
|
||||
cancel() {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ export class RecoverPage {
|
||||
'embassy-os': p['embassy-os'],
|
||||
}
|
||||
this.mappedDrives.push({
|
||||
hasValidBackup: p['embassy-os']?.full,
|
||||
is02x: drive['embassy-os']?.version.startsWith('0.2'),
|
||||
hasValidBackup: !!p['embassy-os']?.full,
|
||||
is02x: !!drive['embassy-os']?.version.startsWith('0.2'),
|
||||
drive,
|
||||
})
|
||||
})
|
||||
@@ -111,6 +111,7 @@ export class RecoverPage {
|
||||
{
|
||||
text: 'Use Drive',
|
||||
handler: async () => {
|
||||
if (importableDrive.guid)
|
||||
await this.importDrive(importableDrive.guid)
|
||||
},
|
||||
},
|
||||
@@ -148,11 +149,14 @@ export class RecoverPage {
|
||||
}
|
||||
|
||||
async select(target: DiskBackupTarget) {
|
||||
const is02x = target['embassy-os'].version.startsWith('0.2')
|
||||
const is02x = target['embassy-os']?.version.startsWith('0.2')
|
||||
const { logicalname } = target
|
||||
|
||||
if (!logicalname) return
|
||||
|
||||
if (this.stateService.hasProductKey) {
|
||||
if (is02x) {
|
||||
this.selectRecoverySource(target.logicalname)
|
||||
this.selectRecoverySource(logicalname)
|
||||
} else {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
@@ -160,8 +164,8 @@ export class RecoverPage {
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data && res.data.password) {
|
||||
this.selectRecoverySource(target.logicalname, res.data.password)
|
||||
if (res.data?.password) {
|
||||
this.selectRecoverySource(logicalname, res.data.password)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
@@ -188,8 +192,8 @@ export class RecoverPage {
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data && res.data.productKey) {
|
||||
this.selectRecoverySource(target.logicalname)
|
||||
if (res.data?.productKey) {
|
||||
this.selectRecoverySource(logicalname)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
|
||||
@@ -24,7 +24,7 @@ export class SuccessPage {
|
||||
await this.stateService.completeEmbassy()
|
||||
document
|
||||
.getElementById('install-cert')
|
||||
.setAttribute(
|
||||
?.setAttribute(
|
||||
'href',
|
||||
'data:application/x-x509-ca-cert;base64,' +
|
||||
encodeURIComponent(this.stateService.cert),
|
||||
@@ -56,20 +56,24 @@ export class SuccessPage {
|
||||
}
|
||||
|
||||
installCert() {
|
||||
document.getElementById('install-cert').click()
|
||||
document.getElementById('install-cert')?.click()
|
||||
}
|
||||
|
||||
download() {
|
||||
document.getElementById('tor-addr').innerHTML = this.stateService.torAddress
|
||||
document.getElementById('lan-addr').innerHTML = this.stateService.lanAddress
|
||||
const torAddress = document.getElementById('tor-addr')
|
||||
const lanAddress = document.getElementById('lan-addr')
|
||||
|
||||
if (torAddress) torAddress.innerHTML = this.stateService.torAddress
|
||||
if (lanAddress) lanAddress.innerHTML = this.stateService.lanAddress
|
||||
|
||||
document
|
||||
.getElementById('cert')
|
||||
.setAttribute(
|
||||
?.setAttribute(
|
||||
'href',
|
||||
'data:application/x-x509-ca-cert;base64,' +
|
||||
encodeURIComponent(this.stateService.cert),
|
||||
)
|
||||
let html = document.getElementById('downloadable').innerHTML
|
||||
let html = document.getElementById('downloadable')?.innerHTML || ''
|
||||
const filename = 'embassy-info.html'
|
||||
|
||||
const elem = document.createElement('a')
|
||||
|
||||
@@ -15,7 +15,7 @@ import { HttpError, RpcError } from '@start9labs/shared'
|
||||
})
|
||||
export class HttpService {
|
||||
fullUrl: string
|
||||
productKey: string
|
||||
productKey?: string
|
||||
|
||||
constructor(private readonly http: HttpClient) {
|
||||
const port = window.location.port
|
||||
@@ -43,6 +43,8 @@ export class HttpService {
|
||||
}
|
||||
|
||||
if (isRpcSuccess(res)) return res.result
|
||||
|
||||
throw new Error('Unknown RPC response')
|
||||
}
|
||||
|
||||
async encryptedHttpRequest<T>(httpOpts: {
|
||||
@@ -53,7 +55,7 @@ export class HttpService {
|
||||
const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url
|
||||
|
||||
const encryptedBody = await AES_CTR.encryptPbkdf2(
|
||||
this.productKey,
|
||||
this.productKey || '',
|
||||
encodeUtf8(JSON.stringify(httpOpts.body)),
|
||||
)
|
||||
const options = {
|
||||
@@ -74,7 +76,7 @@ export class HttpService {
|
||||
.toPromise()
|
||||
.then(res =>
|
||||
AES_CTR.decryptPbkdf2(
|
||||
this.productKey,
|
||||
this.productKey || '',
|
||||
(res as any).body as ArrayBuffer,
|
||||
),
|
||||
)
|
||||
@@ -206,7 +208,7 @@ type AES_CTR = {
|
||||
secretKey: string,
|
||||
messageBuffer: Uint8Array,
|
||||
) => Promise<Uint8Array>
|
||||
decryptPbkdf2: (secretKey, arr: ArrayBuffer) => Promise<string>
|
||||
decryptPbkdf2: (secretKey: string, arr: ArrayBuffer) => Promise<string>
|
||||
}
|
||||
|
||||
export const AES_CTR: AES_CTR = {
|
||||
@@ -243,8 +245,10 @@ export const AES_CTR: AES_CTR = {
|
||||
|
||||
export const encode16 = (buffer: Uint8Array) =>
|
||||
buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')
|
||||
export const decode16 = hexString =>
|
||||
new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)))
|
||||
export const decode16 = (hexString: string) =>
|
||||
new Uint8Array(
|
||||
hexString.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || [],
|
||||
)
|
||||
|
||||
export function encodeUtf8(str: string): Uint8Array {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
@@ -43,7 +43,7 @@ export class LiveApiService extends ApiService {
|
||||
)
|
||||
}
|
||||
|
||||
async set02XDrive(logicalname) {
|
||||
async set02XDrive(logicalname: string) {
|
||||
return this.http.rpcRequest<void>(
|
||||
{
|
||||
method: 'setup.recovery.v2.set',
|
||||
@@ -124,7 +124,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
function isCifsSource(
|
||||
source: CifsRecoverySource | DiskRecoverySource | undefined,
|
||||
source: CifsRecoverySource | DiskRecoverySource | null,
|
||||
): source is CifsRecoverySource {
|
||||
return !!(source as CifsRecoverySource)?.hostname
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class StateService {
|
||||
embassyLoaded = false
|
||||
|
||||
recoverySource: CifsRecoverySource | DiskRecoverySource
|
||||
recoveryPassword: string
|
||||
recoveryPassword?: string
|
||||
|
||||
dataTransferProgress: {
|
||||
bytesTransferred: number
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^13.2.0",
|
||||
"@angular/core": "^13.2.0",
|
||||
"@angular/router": "^13.2.0",
|
||||
"@ionic/angular": "^6.0.3",
|
||||
"@start9labs/emver": "^0.1.5"
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ export class RpcError<T> {
|
||||
return `${this.error.message}\n\n${this.error.data}`
|
||||
}
|
||||
|
||||
return this.error.data.details
|
||||
return this.error.data?.details
|
||||
? `${this.error.message}\n\n${this.error.data.details}`
|
||||
: this.error.message
|
||||
}
|
||||
@@ -20,6 +20,6 @@ export class RpcError<T> {
|
||||
private getRevision(): T | null {
|
||||
return typeof this.error.data === 'string'
|
||||
? null
|
||||
: this.error.data.revision || null
|
||||
: this.error.data?.revision || null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@ import { Emver } from '../../services/emver.service'
|
||||
export class EmverSatisfiesPipe implements PipeTransform {
|
||||
constructor(private readonly emver: Emver) {}
|
||||
|
||||
transform(versionUnderTest: string, range: string): boolean {
|
||||
return this.emver.satisfies(versionUnderTest, range)
|
||||
transform(versionUnderTest?: string, range?: string): boolean {
|
||||
return (
|
||||
!!versionUnderTest &&
|
||||
!!range &&
|
||||
this.emver.satisfies(versionUnderTest, range)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,18 +19,17 @@ export class ConvertBytesPipe implements PipeTransform {
|
||||
name: 'durationToSeconds',
|
||||
})
|
||||
export class DurationToSecondsPipe implements PipeTransform {
|
||||
transform(duration: string | null): number {
|
||||
transform(duration?: string | null): number {
|
||||
if (!duration) return 0
|
||||
const splitUnit = duration.match(/^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/)
|
||||
const unit = splitUnit[3]
|
||||
const num = splitUnit[1]
|
||||
const [, num, , unit] =
|
||||
duration.match(/^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/) || []
|
||||
return Number(num) * unitsToSeconds[unit]
|
||||
}
|
||||
}
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
|
||||
const unitsToSeconds = {
|
||||
const unitsToSeconds: Record<string, number> = {
|
||||
ns: 1e-9,
|
||||
µs: 1e-6,
|
||||
ms: 0.001,
|
||||
|
||||
@@ -34,5 +34,6 @@ export * from './types/rpc-error-details'
|
||||
export * from './types/url'
|
||||
export * from './types/workspace-config'
|
||||
|
||||
export * from './util/get-pkg-id'
|
||||
export * from './util/misc.util'
|
||||
export * from './util/unused'
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as emver from '@start9labs/emver'
|
||||
export class Emver {
|
||||
constructor() {}
|
||||
|
||||
compare(lhs: string, rhs: string): number {
|
||||
compare(lhs: string, rhs: string): number | null {
|
||||
if (!lhs || !rhs) return null
|
||||
return emver.compare(lhs, rhs)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IonicSafeString, ToastController } from '@ionic/angular'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ErrorToastService {
|
||||
private toast: HTMLIonToastElement
|
||||
private toast?: HTMLIonToastElement
|
||||
|
||||
constructor(private readonly toastCtrl: ToastController) {}
|
||||
|
||||
|
||||
11
frontend/projects/shared/src/util/get-pkg-id.ts
Normal file
11
frontend/projects/shared/src/util/get-pkg-id.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
|
||||
export function getPkgId({ snapshot }: ActivatedRoute): string {
|
||||
const pkgId = snapshot.paramMap.get('pkgId')
|
||||
|
||||
if (!pkgId) {
|
||||
throw new Error('pkgId is missing from route params')
|
||||
}
|
||||
|
||||
return pkgId
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function debounce(delay: number = 300): MethodDecorator {
|
||||
|
||||
const original = descriptor.value
|
||||
|
||||
descriptor.value = function (...args) {
|
||||
descriptor.value = function (this: any, ...args: any[]) {
|
||||
clearTimeout(this[timeoutKey])
|
||||
this[timeoutKey] = setTimeout(() => original.apply(this, args), delay)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function traceThrowDesc<T>(description: string, t: T | undefined): T {
|
||||
export function inMs(
|
||||
count: number,
|
||||
unit: 'days' | 'hours' | 'minutes' | 'seconds',
|
||||
) {
|
||||
): number {
|
||||
switch (unit) {
|
||||
case 'seconds':
|
||||
return count * 1000
|
||||
@@ -63,31 +63,6 @@ export function toObject<T>(t: T[], map: (t0: T) => string): Record<string, T> {
|
||||
}, {} as Record<string, T>)
|
||||
}
|
||||
|
||||
export function deepCloneUnknown<T>(value: T): T {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return deepCloneArray(value)
|
||||
}
|
||||
return deepCloneObject(value)
|
||||
}
|
||||
|
||||
export function deepCloneObject<T>(source: T) {
|
||||
const result = {}
|
||||
Object.keys(source).forEach(key => {
|
||||
const value = source[key]
|
||||
result[key] = deepCloneUnknown(value)
|
||||
}, {})
|
||||
return result as T
|
||||
}
|
||||
|
||||
export function deepCloneArray(collection: any) {
|
||||
return collection.map(value => {
|
||||
return deepCloneUnknown(value)
|
||||
})
|
||||
}
|
||||
|
||||
export function partitionArray<T>(
|
||||
ts: T[],
|
||||
condition: (t: T) => boolean,
|
||||
@@ -110,21 +85,3 @@ export function update<T>(
|
||||
): Record<string, T> {
|
||||
return { ...t, ...u }
|
||||
}
|
||||
|
||||
export function uniqueBy<T>(
|
||||
ts: T[],
|
||||
uniqueBy: (t: T) => string,
|
||||
prioritize: (t1: T, t2: T) => T,
|
||||
) {
|
||||
return Object.values(
|
||||
ts.reduce((acc, next) => {
|
||||
const previousValue = acc[uniqueBy(next)]
|
||||
if (previousValue) {
|
||||
acc[uniqueBy(next)] = prioritize(acc[uniqueBy(next)], previousValue)
|
||||
} else {
|
||||
acc[uniqueBy(next)] = previousValue
|
||||
}
|
||||
return acc
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class FooterComponent {
|
||||
getProgress({
|
||||
downloaded,
|
||||
size,
|
||||
}: ServerInfo['status-info']['update-progress']): number {
|
||||
}: NonNullable<ServerInfo['status-info']['update-progress']>): number {
|
||||
return Math.round((100 * (downloaded || 1)) / (size || 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@ function getMessage(failure: ConnectionFailure): OfflineMessage {
|
||||
message: 'Embassy not found on Local Area Network.',
|
||||
link: 'https://start9.com/latest/support/common-issues',
|
||||
}
|
||||
default:
|
||||
return { message: '' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { mapTo, share, switchMap } from 'rxjs/operators'
|
||||
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
// Start and stop PatchDb upon verification
|
||||
@Injectable({
|
||||
@@ -27,7 +26,6 @@ export class PatchMonitorService extends Observable<boolean> {
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly storage: Storage,
|
||||
|
||||
@@ -51,7 +51,7 @@ export class UnreadToastService extends Observable<unknown> {
|
||||
await this.unreadToast?.dismiss()
|
||||
|
||||
this.unreadToast = await this.toastCtrl.create(TOAST)
|
||||
this.unreadToast.buttons.push({
|
||||
this.unreadToast.buttons?.push({
|
||||
side: 'end',
|
||||
text: 'View',
|
||||
handler: () => {
|
||||
|
||||
@@ -49,7 +49,7 @@ export class UpdateToastService extends Observable<unknown> {
|
||||
await this.updateToast?.dismiss()
|
||||
|
||||
this.updateToast = await this.toastCtrl.create(TOAST)
|
||||
this.updateToast.buttons.push({
|
||||
this.updateToast.buttons?.push({
|
||||
side: 'end',
|
||||
text: 'Restart',
|
||||
handler: () => {
|
||||
|
||||
@@ -26,13 +26,15 @@
|
||||
{{ page.title }}
|
||||
</ion-label>
|
||||
<ion-icon
|
||||
*ngIf="page.url === '/embassy' && eosService.updateAvailable$ | async"
|
||||
*ngIf="page.url === '/embassy' && (eosService.updateAvailable$ | async)"
|
||||
color="success"
|
||||
size="small"
|
||||
name="rocket-outline"
|
||||
></ion-icon>
|
||||
<ion-badge
|
||||
*ngIf="page.url === '/notifications' && notification$ | async as count"
|
||||
*ngIf="
|
||||
page.url === '/notifications' && (notification$ | async) as count
|
||||
"
|
||||
color="danger"
|
||||
class="badge"
|
||||
>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<div [hidden]="!control.dirty && !control.touched" class="validation-error">
|
||||
<!-- primitive -->
|
||||
<p *ngIf="control.hasError('required')">
|
||||
{{ spec.name }} is required
|
||||
</p>
|
||||
<p *ngIf="control.hasError('required')">{{ spec.name }} is required</p>
|
||||
|
||||
<!-- string -->
|
||||
<p *ngIf="control.hasError('pattern')">
|
||||
{{ spec['pattern-description'] }}
|
||||
{{ $any(spec)['pattern-description'] }}
|
||||
</p>
|
||||
|
||||
<!-- number -->
|
||||
@@ -15,7 +13,7 @@
|
||||
{{ spec.name }} must be an integer
|
||||
</p>
|
||||
<p *ngIf="control.hasError('numberNotInRange')">
|
||||
{{ control.errors['numberNotInRange'].value }}
|
||||
{{ control.errors?.['numberNotInRange']?.value }}
|
||||
</p>
|
||||
<p *ngIf="control.hasError('notNumber')">
|
||||
{{ spec.name }} must be a number
|
||||
@@ -25,13 +23,13 @@
|
||||
<!-- list -->
|
||||
<ng-container *ngIf="spec.type === 'list'">
|
||||
<p *ngIf="control.hasError('listNotInRange')">
|
||||
{{ control.errors['listNotInRange'].value }}
|
||||
{{ control.errors?.['listNotInRange']?.value }}
|
||||
</p>
|
||||
<p *ngIf="control.hasError('listNotUnique')">
|
||||
{{ control.errors['listNotUnique'].value }}
|
||||
{{ control.errors?.['listNotUnique']?.value }}
|
||||
</p>
|
||||
<p *ngIf="control.hasError('listItemIssue')">
|
||||
{{ control.errors['listItemIssue'].value }}
|
||||
{{ control.errors?.['listItemIssue']?.value }}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
new: current && current[entry.key] === undefined,
|
||||
new: !!current && current[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
></form-label>
|
||||
@@ -129,7 +129,7 @@
|
||||
<!-- enum -->
|
||||
<!-- class enter-click disables the enter click on the modal behind the select -->
|
||||
<ion-select
|
||||
*ngIf="spec.type === 'enum'"
|
||||
*ngIf="spec.type === 'enum' && formGroup.get(entry.key) as control"
|
||||
[interfaceOptions]="{
|
||||
message: getWarningText(spec.warning),
|
||||
cssClass: 'enter-click'
|
||||
@@ -137,7 +137,7 @@
|
||||
slot="end"
|
||||
placeholder="Select"
|
||||
[formControlName]="entry.key"
|
||||
[selectedText]="spec['value-names'][formGroup.get(entry.key).value]"
|
||||
[selectedText]="spec['value-names'][control.value]"
|
||||
>
|
||||
<ion-select-option
|
||||
*ngFor="let option of spec.values"
|
||||
@@ -157,7 +157,7 @@
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
new: current && current[entry.key] === undefined,
|
||||
new: !!current && current[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
></form-label>
|
||||
@@ -208,7 +208,7 @@
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
new: current && current[entry.key] === undefined,
|
||||
new: !!current && current[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
></form-label>
|
||||
@@ -281,16 +281,12 @@
|
||||
: $any(spec.spec).spec
|
||||
"
|
||||
[formGroup]="abstractControl"
|
||||
[current]="
|
||||
current && current[entry.key]
|
||||
? current[entry.key][i]
|
||||
: undefined
|
||||
"
|
||||
[current]="current?.[entry.key]?.[i]"
|
||||
[unionSpec]="
|
||||
spec.subtype === 'union' ? $any(spec.spec) : undefined
|
||||
"
|
||||
(onInputChange)="
|
||||
updateLabel(entry.key, i, spec.spec['display-as'])
|
||||
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
|
||||
"
|
||||
(onExpand)="resize(entry.key, i)"
|
||||
></form-object>
|
||||
@@ -350,7 +346,7 @@
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
new: current && current[entry.key] === undefined,
|
||||
new: !!current && current[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
></form-label>
|
||||
@@ -372,7 +368,7 @@
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<form-error
|
||||
*ngIf="formGroup.get(entry.key).errors"
|
||||
*ngIf="formGroup.get(entry.key)?.errors"
|
||||
[control]="$any(formGroup.get(entry.key))"
|
||||
[spec]="spec"
|
||||
>
|
||||
|
||||
@@ -34,8 +34,8 @@ const Mustache = require('mustache')
|
||||
export class FormObjectComponent {
|
||||
@Input() objectSpec: ConfigSpec
|
||||
@Input() formGroup: FormGroup
|
||||
@Input() unionSpec: ValueSpecUnion
|
||||
@Input() current: { [key: string]: any }
|
||||
@Input() unionSpec?: ValueSpecUnion
|
||||
@Input() current?: { [key: string]: any }
|
||||
@Input() showEdited: boolean = false
|
||||
@Output() onInputChange = new EventEmitter<void>()
|
||||
@Output() onExpand = new EventEmitter<void>()
|
||||
@@ -61,7 +61,7 @@ export class FormObjectComponent {
|
||||
|
||||
if (spec.type === 'list' && ['object', 'union'].includes(spec.subtype)) {
|
||||
this.objectListDisplay[key] = []
|
||||
this.formGroup.get(key).value.forEach((obj, index) => {
|
||||
this.formGroup.get(key)?.value.forEach((obj: any, index: number) => {
|
||||
const displayAs = (spec.spec as ListValueSpecOf<'object'>)[
|
||||
'display-as'
|
||||
]
|
||||
@@ -87,7 +87,7 @@ export class FormObjectComponent {
|
||||
}
|
||||
|
||||
updateUnion(e: any): void {
|
||||
const primary = this.unionSpec.tag.id
|
||||
const primary = this.unionSpec?.tag.id
|
||||
|
||||
Object.keys(this.formGroup.controls).forEach(control => {
|
||||
if (control === primary) return
|
||||
@@ -104,7 +104,7 @@ export class FormObjectComponent {
|
||||
this.formGroup.addControl(control, unionGroup.controls[control])
|
||||
})
|
||||
|
||||
Object.entries(this.unionSpec.variants[e.detail.value]).forEach(
|
||||
Object.entries(this.unionSpec?.variants[e.detail.value] || {}).forEach(
|
||||
([key, value]) => {
|
||||
if (['object', 'union'].includes(value.type)) {
|
||||
this.objectDisplay[key] = {
|
||||
@@ -138,6 +138,9 @@ export class FormObjectComponent {
|
||||
if (markDirty) arr.markAsDirty()
|
||||
const listSpec = this.objectSpec[key] as ValueSpecList
|
||||
const newItem = this.formService.getListItem(listSpec, val)
|
||||
|
||||
if (!newItem) return
|
||||
|
||||
newItem.markAllAsTouched()
|
||||
arr.insert(0, newItem)
|
||||
if (['object', 'union'].includes(listSpec.subtype)) {
|
||||
@@ -177,13 +180,14 @@ export class FormObjectComponent {
|
||||
|
||||
updateLabel(key: string, i: number, displayAs: string) {
|
||||
this.objectListDisplay[key][i].displayAs = displayAs
|
||||
? Mustache.render(displayAs, this.formGroup.get(key).value[i])
|
||||
? Mustache.render(displayAs, this.formGroup.get(key)?.value[i])
|
||||
: ''
|
||||
}
|
||||
|
||||
getWarningText(text: string): IonicSafeString {
|
||||
if (text)
|
||||
return new IonicSafeString(`<ion-text color="warning">${text}</ion-text>`)
|
||||
getWarningText(text: string = ''): IonicSafeString | string {
|
||||
return text
|
||||
? new IonicSafeString(`<ion-text color="warning">${text}</ion-text>`)
|
||||
: ''
|
||||
}
|
||||
|
||||
handleInputChange() {
|
||||
@@ -192,8 +196,8 @@ export class FormObjectComponent {
|
||||
|
||||
handleBooleanChange(key: string, spec: ValueSpecBoolean) {
|
||||
if (spec.warning) {
|
||||
const current = this.formGroup.get(key).value
|
||||
const cancelFn = () => this.formGroup.get(key).setValue(!current)
|
||||
const current = this.formGroup.get(key)?.value
|
||||
const cancelFn = () => this.formGroup.get(key)?.setValue(!current)
|
||||
this.presentAlertChangeWarning(key, spec, undefined, cancelFn)
|
||||
}
|
||||
}
|
||||
@@ -307,7 +311,7 @@ export class FormObjectComponent {
|
||||
}
|
||||
|
||||
private updateEnumList(key: string, current: string[], updated: string[]) {
|
||||
this.formGroup.get(key).markAsDirty()
|
||||
this.formGroup.get(key)?.markAsDirty()
|
||||
|
||||
for (let i = current.length - 1; i >= 0; i--) {
|
||||
if (!updated.includes(current[i])) {
|
||||
@@ -322,9 +326,9 @@ export class FormObjectComponent {
|
||||
})
|
||||
}
|
||||
|
||||
private getDocSize(key: string, index = 0) {
|
||||
private getDocSize(key: string, index = 0): string {
|
||||
const element = document.getElementById(this.getElementId(key, index))
|
||||
return `${element.scrollHeight}px`
|
||||
return `${element?.scrollHeight}px`
|
||||
}
|
||||
|
||||
getElementId(key: string, index = 0): string {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{{ dependentViolation }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="patch.data['package-data']" class="items">
|
||||
<div *ngIf="pkgs$ | async as pkgs" class="items">
|
||||
<div class="affected">
|
||||
<ion-text color="warning">Affected Services</ion-text>
|
||||
</div>
|
||||
@@ -26,13 +26,10 @@
|
||||
*ngFor="let dep of dependentBreakages | keyvalue"
|
||||
>
|
||||
<ion-thumbnail class="thumbnail" slot="start">
|
||||
<img
|
||||
alt=""
|
||||
[src]="patch.data['package-data'][dep.key]['static-files'].icon"
|
||||
/>
|
||||
<img alt="" [src]="pkgs[dep.key]['static-files'].icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h5>{{ patch.data['package-data'][dep.key].manifest.title }}</h5>
|
||||
<h5>{{ pkgs[dep.key].manifest.title }}</h5>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,8 @@ export class DependentsComponent {
|
||||
loading$ = new BehaviorSubject(false)
|
||||
cancel$ = new Subject<void>()
|
||||
|
||||
readonly pkgs$ = this.patch.watch$('package-data')
|
||||
|
||||
constructor(public readonly patch: PatchDbService) {}
|
||||
|
||||
load() {
|
||||
@@ -45,6 +47,7 @@ export class DependentsComponent {
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
console.log('DEP BREAKS, ', this.dependentBreakages)
|
||||
if (
|
||||
this.dependentBreakages &&
|
||||
!isEmptyObject(this.dependentBreakages)
|
||||
|
||||
@@ -33,7 +33,7 @@ export class WizardBaker {
|
||||
const action = 'update'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
const slideDefinitions: Array<SlideDefinition | undefined> = [
|
||||
installAlert
|
||||
? {
|
||||
slide: {
|
||||
@@ -170,7 +170,7 @@ export class WizardBaker {
|
||||
const action = 'downgrade'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
const slideDefinitions: Array<SlideDefinition | undefined> = [
|
||||
installAlert
|
||||
? {
|
||||
slide: {
|
||||
|
||||
@@ -66,19 +66,20 @@ export class LogsPage {
|
||||
try {
|
||||
// get logs
|
||||
const logs = await this.fetch()
|
||||
if (!logs.length) return
|
||||
if (!logs?.length) return
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const beforeContainerHeight = container.scrollHeight
|
||||
const newLogs = document
|
||||
.getElementById('template')
|
||||
.cloneNode(true) as HTMLElement
|
||||
const beforeContainerHeight = container?.scrollHeight || 0
|
||||
const newLogs = document.getElementById('template')?.cloneNode(true)
|
||||
|
||||
if (!(newLogs instanceof HTMLElement)) return
|
||||
|
||||
newLogs.innerHTML =
|
||||
logs
|
||||
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
|
||||
.join('\n') + (logs.length ? '\n' : '')
|
||||
container.prepend(newLogs)
|
||||
const afterContainerHeight = container.scrollHeight
|
||||
container?.prepend(newLogs)
|
||||
const afterContainerHeight = container?.scrollHeight || 0
|
||||
|
||||
// scroll down
|
||||
scrollBy(0, afterContainerHeight - beforeContainerHeight)
|
||||
@@ -97,17 +98,18 @@ export class LogsPage {
|
||||
try {
|
||||
this.loadingMore = true
|
||||
const logs = await this.fetch(false)
|
||||
if (!logs.length) return (this.loadingMore = false)
|
||||
if (!logs?.length) return (this.loadingMore = false)
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const newLogs = document
|
||||
.getElementById('template')
|
||||
.cloneNode(true) as HTMLElement
|
||||
const newLogs = document.getElementById('template')?.cloneNode(true)
|
||||
|
||||
if (!(newLogs instanceof HTMLElement)) return
|
||||
|
||||
newLogs.innerHTML =
|
||||
logs
|
||||
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
|
||||
.join('\n') + (logs.length ? '\n' : '')
|
||||
container.append(newLogs)
|
||||
container?.append(newLogs)
|
||||
this.loadingMore = false
|
||||
this.scrollEvent()
|
||||
} catch (e) {}
|
||||
@@ -116,7 +118,7 @@ export class LogsPage {
|
||||
scrollEvent() {
|
||||
const buttonDiv = document.getElementById('button-div')
|
||||
this.isOnBottom =
|
||||
buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight
|
||||
!!buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
|
||||
@@ -20,5 +20,5 @@ export class StatusComponent {
|
||||
@Input() weight?: string = 'normal'
|
||||
@Input() disconnected?: boolean = false
|
||||
@Input() installProgress?: number
|
||||
@Input() sigtermTimeout?: string
|
||||
@Input() sigtermTimeout?: string | null = null
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<ng-template #noError>
|
||||
<ion-item
|
||||
*ngIf="hasConfig && !pkg.installed.status.configured && !configForm.dirty"
|
||||
*ngIf="hasConfig && !pkg.installed?.status?.configured && !configForm.dirty"
|
||||
>
|
||||
<ion-label>
|
||||
<ion-text color="success"
|
||||
|
||||
@@ -62,11 +62,12 @@ export class AppConfigPage {
|
||||
|
||||
if (!this.hasConfig) return
|
||||
|
||||
try {
|
||||
let oldConfig: object
|
||||
let newConfig: object
|
||||
let newConfig: object | undefined
|
||||
let spec: ConfigSpec
|
||||
let patch: Operation[]
|
||||
let patch: Operation[] | undefined
|
||||
|
||||
try {
|
||||
if (this.dependentInfo) {
|
||||
this.loadingText = `Setting properties to accommodate ${this.dependentInfo.title}`
|
||||
const {
|
||||
@@ -133,7 +134,7 @@ export class AppConfigPage {
|
||||
if (this.configForm.invalid) {
|
||||
document
|
||||
.getElementsByClassName('validation-error')[0]
|
||||
?.parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
?.parentElement?.parentElement?.scrollIntoView({ behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -153,7 +154,7 @@ export class AppConfigPage {
|
||||
config,
|
||||
})
|
||||
|
||||
if (!isEmptyObject(breakages['length'])) {
|
||||
if (!isEmptyObject(breakages['length']) && this.pkg) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.configure({
|
||||
@@ -247,11 +248,11 @@ export class AppConfigPage {
|
||||
return isNaN(num) ? node : num
|
||||
})
|
||||
|
||||
if (op.op !== 'remove') this.configForm.get(arrPath).markAsDirty()
|
||||
if (op.op !== 'remove') this.configForm.get(arrPath)?.markAsDirty()
|
||||
|
||||
if (typeof arrPath[arrPath.length - 1] === 'number') {
|
||||
const prevPath = arrPath.slice(0, arrPath.length - 1)
|
||||
this.configForm.get(prevPath).markAsDirty()
|
||||
this.configForm.get(prevPath)?.markAsDirty()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class GenericFormPage {
|
||||
this.formGroup.markAllAsTouched()
|
||||
document
|
||||
.getElementsByClassName('validation-error')[0]
|
||||
?.parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
?.parentElement?.parentElement?.scrollIntoView({ behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export class GenericInputComponent {
|
||||
...this.options,
|
||||
}
|
||||
|
||||
this.value = this.options.initialValue
|
||||
this.value = this.options.initialValue || ''
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
|
||||
@@ -12,21 +12,21 @@ export class SnakePage {
|
||||
speed = 45
|
||||
width = 40
|
||||
height = 26
|
||||
grid
|
||||
grid = NaN
|
||||
|
||||
startingLength = 4
|
||||
|
||||
score = 0
|
||||
highScore = 0
|
||||
|
||||
xDown: number
|
||||
yDown: number
|
||||
xDown?: number
|
||||
yDown?: number
|
||||
canvas: HTMLCanvasElement
|
||||
image: HTMLImageElement
|
||||
context
|
||||
context: CanvasRenderingContext2D
|
||||
|
||||
snake
|
||||
bitcoin
|
||||
snake: any
|
||||
bitcoin: { x: number; y: number } = { x: NaN, y: NaN }
|
||||
|
||||
moveQueue: String[] = []
|
||||
|
||||
@@ -37,7 +37,8 @@ export class SnakePage {
|
||||
|
||||
ngOnInit() {
|
||||
if (this.patch.getData().ui.gaming?.snake?.['high-score']) {
|
||||
this.highScore = this.patch.getData().ui.gaming?.snake?.['high-score']
|
||||
this.highScore =
|
||||
this.patch.getData().ui.gaming?.snake?.['high-score'] || 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +61,8 @@ export class SnakePage {
|
||||
this.handleTouchMove(e)
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
sizeChange(event) {
|
||||
@HostListener('window:resize')
|
||||
sizeChange() {
|
||||
this.init()
|
||||
}
|
||||
|
||||
@@ -78,7 +79,7 @@ export class SnakePage {
|
||||
init() {
|
||||
this.canvas = document.getElementById('game') as HTMLCanvasElement
|
||||
this.canvas.style.border = '1px solid #e0e0e0'
|
||||
this.context = this.canvas.getContext('2d')
|
||||
this.context = this.canvas.getContext('2d')!
|
||||
const container = document.getElementsByClassName('canvas-center')[0]
|
||||
this.grid = Math.min(
|
||||
Math.floor(container.clientWidth / this.width),
|
||||
@@ -109,13 +110,13 @@ export class SnakePage {
|
||||
return evt.touches
|
||||
}
|
||||
|
||||
handleTouchStart(evt) {
|
||||
handleTouchStart(evt: TouchEvent) {
|
||||
const firstTouch = this.getTouches(evt)[0]
|
||||
this.xDown = firstTouch.clientX
|
||||
this.yDown = firstTouch.clientY
|
||||
}
|
||||
|
||||
handleTouchMove(evt) {
|
||||
handleTouchMove(evt: TouchEvent) {
|
||||
if (!this.xDown || !this.yDown) {
|
||||
return
|
||||
}
|
||||
@@ -141,8 +142,8 @@ export class SnakePage {
|
||||
}
|
||||
}
|
||||
/* reset values */
|
||||
this.xDown = null
|
||||
this.yDown = null
|
||||
this.xDown = undefined
|
||||
this.yDown = undefined
|
||||
}
|
||||
|
||||
// game loop
|
||||
@@ -257,7 +258,7 @@ export class SnakePage {
|
||||
this.score = 0
|
||||
}
|
||||
|
||||
getRandomInt(min, max) {
|
||||
getRandomInt(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min)) + min
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { isEmptyObject, ErrorToastService } from '@start9labs/shared'
|
||||
import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared'
|
||||
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
||||
|
||||
@Component({
|
||||
@@ -28,7 +28,7 @@ import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.
|
||||
})
|
||||
export class AppActionsPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
pkgId: string
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
pkg: PackageDataEntry
|
||||
subs: Subscription[]
|
||||
|
||||
@@ -45,7 +45,6 @@ export class AppActionsPage {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.subs = [
|
||||
this.patch.watch$('package-data', this.pkgId).subscribe(pkg => {
|
||||
this.pkg = pkg
|
||||
@@ -62,13 +61,14 @@ export class AppActionsPage {
|
||||
}
|
||||
|
||||
async handleAction(action: { key: string; value: Action }) {
|
||||
const status = this.pkg.installed.status
|
||||
const status = this.pkg.installed?.status
|
||||
if (
|
||||
status &&
|
||||
(action.value['allowed-statuses'] as PackageMainStatus[]).includes(
|
||||
status.main.status,
|
||||
)
|
||||
) {
|
||||
if (!isEmptyObject(action.value['input-spec'])) {
|
||||
if (!isEmptyObject(action.value['input-spec'] || {})) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericFormPage,
|
||||
componentProps: {
|
||||
@@ -112,7 +112,7 @@ export class AppActionsPage {
|
||||
const statuses = [...action.value['allowed-statuses']]
|
||||
const last = statuses.pop()
|
||||
let statusesStr = statuses.join(', ')
|
||||
let error = null
|
||||
let error = ''
|
||||
if (statuses.length) {
|
||||
if (statuses.length > 1) {
|
||||
// oxford comma
|
||||
@@ -144,7 +144,7 @@ export class AppActionsPage {
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
uninstallAlert: alerts.uninstall,
|
||||
uninstallAlert: alerts.uninstall || undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -177,6 +177,7 @@ export class AppActionsPage {
|
||||
})
|
||||
|
||||
setTimeout(() => successModal.present(), 400)
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
return false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { IonContent, ToastController } from '@ionic/angular'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { getUiInterfaceKey } from 'src/app/services/config.service'
|
||||
import {
|
||||
InstalledPackageDataEntry,
|
||||
@@ -23,7 +24,7 @@ export class AppInterfacesPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
ui: LocalInterface | null
|
||||
other: LocalInterface[] = []
|
||||
pkgId: string
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
@@ -31,11 +32,12 @@ export class AppInterfacesPage {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
const pkg = this.patch.getData()['package-data'][this.pkgId]
|
||||
const interfaces = pkg.manifest.interfaces
|
||||
const uiKey = getUiInterfaceKey(interfaces)
|
||||
|
||||
if (!pkg?.installed) return
|
||||
|
||||
const addressesMap = pkg.installed['interface-addresses']
|
||||
|
||||
if (uiKey) {
|
||||
@@ -45,10 +47,10 @@ export class AppInterfacesPage {
|
||||
addresses: {
|
||||
'lan-address': uiAddresses['lan-address']
|
||||
? 'https://' + uiAddresses['lan-address']
|
||||
: null,
|
||||
: '',
|
||||
'tor-address': uiAddresses['tor-address']
|
||||
? 'http://' + uiAddresses['tor-address']
|
||||
: null,
|
||||
: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -62,10 +64,10 @@ export class AppInterfacesPage {
|
||||
addresses: {
|
||||
'lan-address': addresses['lan-address']
|
||||
? 'https://' + addresses['lan-address']
|
||||
: null,
|
||||
: '',
|
||||
'tor-address': addresses['tor-address']
|
||||
? 'http://' + addresses['tor-address']
|
||||
: null,
|
||||
: '',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -21,7 +21,9 @@ export class AppListPkgComponent {
|
||||
constructor(private readonly launcherService: UiLauncherService) {}
|
||||
|
||||
get status(): PackageMainStatus {
|
||||
return this.pkg.entry.installed?.status.main.status
|
||||
return (
|
||||
this.pkg.entry.installed?.status.main.status || PackageMainStatus.Stopped
|
||||
)
|
||||
}
|
||||
|
||||
get manifest(): Manifest {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<!-- header -->
|
||||
<ion-item-divider>
|
||||
{{ reordering ? "Reorder" : "Installed Services" }}
|
||||
<ion-button *ngIf="pkgs.length > 1" slot="end" fill="clear" (click)="toggle()">
|
||||
{{ reordering ? 'Reorder' : 'Installed Services' }}
|
||||
<ion-button
|
||||
*ngIf="pkgs.length > 1"
|
||||
slot="end"
|
||||
fill="clear"
|
||||
(click)="toggle()"
|
||||
>
|
||||
<ion-icon
|
||||
slot="start"
|
||||
[name]="reordering ? 'checkmark' : 'swap-vertical'"
|
||||
></ion-icon>
|
||||
{{ reordering ? "Done" : "Reorder" }}
|
||||
{{ reordering ? 'Done' : 'Reorder' }}
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
|
||||
@@ -14,11 +19,15 @@
|
||||
<ion-list *ngIf="reordering; else grid">
|
||||
<ion-reorder-group disabled="false" (ionItemReorder)="reorder($any($event))">
|
||||
<ion-reorder *ngFor="let item of pkgs">
|
||||
<ion-item color="light" *ngIf="item | packageInfo | async as pkg" class="item">
|
||||
<ion-item
|
||||
color="light"
|
||||
*ngIf="item | packageInfo | async as pkg"
|
||||
class="item"
|
||||
>
|
||||
<app-list-icon
|
||||
slot="start"
|
||||
[pkg]="pkg"
|
||||
[connectionFailure]="connectionFailure$ | async"
|
||||
[connectionFailure]="!!(connectionFailure$ | async)"
|
||||
></app-list-icon>
|
||||
<ion-thumbnail slot="start">
|
||||
<img alt="" [src]="pkg.entry['static-files'].icon" />
|
||||
@@ -27,7 +36,7 @@
|
||||
<h2>{{ pkg.entry.manifest.title }}</h2>
|
||||
<p>{{ pkg.entry.manifest.version | displayEmver }}</p>
|
||||
<status
|
||||
[disconnected]="connectionFailure$ | async"
|
||||
[disconnected]="!!(connectionFailure$ | async)"
|
||||
[rendering]="pkg.primaryRendering"
|
||||
[installProgress]="pkg.installProgress?.totalProgress"
|
||||
weight="bold"
|
||||
@@ -47,8 +56,9 @@
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let pkg of pkgs" sizeXs="12" sizeXl="6">
|
||||
<app-list-pkg
|
||||
[pkg]="pkg | packageInfo | async"
|
||||
[connectionFailure]="connectionFailure$ | async"
|
||||
*ngIf="pkg | packageInfo | async as info"
|
||||
[pkg]="info"
|
||||
[connectionFailure]="!!(connectionFailure$ | async)"
|
||||
></app-list-pkg>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
@@ -8,22 +9,22 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
styleUrls: ['./app-logs.page.scss'],
|
||||
})
|
||||
export class AppLogsPage {
|
||||
pkgId: string
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
loading = true
|
||||
needInfinite = true
|
||||
before: string
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly embassyApi: ApiService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
}
|
||||
|
||||
fetchFetchLogs () {
|
||||
return async (params: { before_flag?: boolean, limit?: number, cursor?: string }) => {
|
||||
fetchFetchLogs() {
|
||||
return async (params: {
|
||||
before_flag?: boolean
|
||||
limit?: number
|
||||
cursor?: string
|
||||
}) => {
|
||||
return this.embassyApi.getPackageLogs({
|
||||
id: this.pkgId,
|
||||
before_flag: params.before_flag,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subscription } from 'rxjs'
|
||||
import { Metric } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MainStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
||||
import { pauseFor, ErrorToastService, getPkgId } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'app-metrics',
|
||||
@@ -14,7 +14,7 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
||||
})
|
||||
export class AppMetricsPage {
|
||||
loading = true
|
||||
pkgId: string
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
mainStatus: MainStatus
|
||||
going = false
|
||||
metrics: Metric
|
||||
@@ -29,7 +29,6 @@ export class AppMetricsPage {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.startDaemon()
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { PackageProperties } from 'src/app/util/properties.util'
|
||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { ErrorToastService, getPkgId } from '@start9labs/shared'
|
||||
import { getValueByPointer } from 'fast-json-patch'
|
||||
|
||||
@Component({
|
||||
@@ -25,7 +25,7 @@ import { getValueByPointer } from 'fast-json-patch'
|
||||
})
|
||||
export class AppPropertiesPage {
|
||||
loading = true
|
||||
pkgId: string
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
pointer: string
|
||||
properties: PackageProperties
|
||||
node: PackageProperties
|
||||
@@ -55,8 +55,6 @@ export class AppPropertiesPage {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
|
||||
await this.getProperties()
|
||||
|
||||
this.subs = [
|
||||
@@ -100,7 +98,7 @@ export class AppPropertiesPage {
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: property.key,
|
||||
message: property.value.description,
|
||||
message: property.value.description || undefined,
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- ** status ** -->
|
||||
<app-show-status
|
||||
[pkg]="pkg"
|
||||
[connectionFailure]="connectionFailure$ | async"
|
||||
[connectionFailure]="!!(connectionFailure$ | async)"
|
||||
[dependencies]="dependencies"
|
||||
[status]="status"
|
||||
></app-show-status>
|
||||
@@ -16,7 +16,7 @@
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
[pkg]="pkg"
|
||||
[connectionFailure]="connectionFailure$ | async"
|
||||
[connectionFailure]="!!(connectionFailure$ | async)"
|
||||
></app-show-health-checks>
|
||||
<!-- ** dependencies ** -->
|
||||
<app-show-dependencies
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from 'src/app/services/connection.service'
|
||||
import { map, startWith } from 'rxjs/operators'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
|
||||
const STATES = [
|
||||
PackageState.Installing,
|
||||
@@ -26,7 +27,7 @@ const STATES = [
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowPage {
|
||||
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
private readonly pkgId = getPkgId(this.route)
|
||||
|
||||
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
|
||||
map(pkg => {
|
||||
|
||||
@@ -31,6 +31,6 @@ export class AppShowProgressComponent {
|
||||
}
|
||||
|
||||
getColor(action: keyof InstallProgress): string {
|
||||
return this.pkg['install-progress'][action] ? 'success' : 'secondary'
|
||||
return this.pkg['install-progress']?.[action] ? 'success' : 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</ion-button>
|
||||
|
||||
<ion-button
|
||||
*ngIf="!pkgStatus.configured"
|
||||
*ngIf="!pkgStatus?.configured"
|
||||
class="action-button"
|
||||
slot="start"
|
||||
color="warning"
|
||||
@@ -48,7 +48,7 @@
|
||||
</ion-button>
|
||||
|
||||
<ion-button
|
||||
*ngIf="interfaces | hasUi"
|
||||
*ngIf="pkgStatus && (interfaces | hasUi)"
|
||||
class="action-button"
|
||||
slot="start"
|
||||
color="primary"
|
||||
|
||||
@@ -61,8 +61,8 @@ export class AppShowStatusComponent {
|
||||
return this.pkg.manifest.interfaces
|
||||
}
|
||||
|
||||
get pkgStatus(): Status {
|
||||
return this.pkg.installed.status
|
||||
get pkgStatus(): Status | null {
|
||||
return this.pkg.installed?.status || null
|
||||
}
|
||||
|
||||
get isInstalled(): boolean {
|
||||
@@ -75,7 +75,8 @@ export class AppShowStatusComponent {
|
||||
|
||||
get isStopped(): boolean {
|
||||
return (
|
||||
this.status.primary === PrimaryStatus.Stopped && this.pkgStatus.configured
|
||||
this.status.primary === PrimaryStatus.Stopped &&
|
||||
!!this.pkgStatus?.configured
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ export class AppShowStatusComponent {
|
||||
async stop(): Promise<void> {
|
||||
const { id, title, version } = this.pkg.manifest
|
||||
const hasDependents = !!Object.keys(
|
||||
this.pkg.installed['current-dependents'],
|
||||
this.pkg.installed?.['current-dependents'] || {},
|
||||
).filter(depId => depId !== id).length
|
||||
|
||||
if (!hasDependents) {
|
||||
|
||||
@@ -134,7 +134,7 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
private async donate({ manifest }: PackageDataEntry): Promise<void> {
|
||||
const url = manifest['donation-url']
|
||||
if (url) {
|
||||
this.document.defaultView.open(url, '_blank', 'noreferrer')
|
||||
this.document.defaultView?.open(url, '_blank', 'noreferrer')
|
||||
} else {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Not Accepting Donations',
|
||||
|
||||
@@ -62,7 +62,7 @@ export class ToDependenciesPipe implements PipeTransform {
|
||||
private setDepValues(
|
||||
pkg: PackageDataEntry,
|
||||
id: string,
|
||||
errors: { [id: string]: DependencyError },
|
||||
errors: { [id: string]: DependencyError | null },
|
||||
): DependencyInfo {
|
||||
let errorText = ''
|
||||
let actionText = 'View'
|
||||
@@ -105,13 +105,13 @@ export class ToDependenciesPipe implements PipeTransform {
|
||||
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
|
||||
}
|
||||
|
||||
const depInfo = pkg.installed['dependency-info'][id]
|
||||
const depInfo = pkg.installed?.['dependency-info'][id]
|
||||
|
||||
return {
|
||||
id,
|
||||
version: pkg.manifest.dependencies[id].version,
|
||||
title: depInfo.manifest?.title || id,
|
||||
icon: depInfo.icon,
|
||||
title: depInfo?.manifest?.title || id,
|
||||
icon: depInfo?.icon || '',
|
||||
errorText,
|
||||
actionText,
|
||||
action,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { debounce, exists, ErrorToastService } from '@start9labs/shared'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { filter, take } from 'rxjs/operators'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { getProjectId } from 'src/app/util/get-project-id'
|
||||
import { GenericFormPage } from '../../../modals/generic-form/generic-form.page'
|
||||
import { debounce, ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'dev-config',
|
||||
@@ -14,7 +15,7 @@ import { debounce, ErrorToastService } from '@start9labs/shared'
|
||||
styleUrls: ['dev-config.page.scss'],
|
||||
})
|
||||
export class DevConfigPage {
|
||||
projectId: string
|
||||
readonly projectId = getProjectId(this.route)
|
||||
editorOptions = { theme: 'vs-dark', language: 'yaml' }
|
||||
code: string = ''
|
||||
saving: boolean = false
|
||||
@@ -28,11 +29,9 @@ export class DevConfigPage {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.projectId = this.route.snapshot.paramMap.get('projectId')
|
||||
|
||||
this.patchDb
|
||||
.watch$('ui', 'dev', this.projectId, 'config')
|
||||
.pipe(take(1))
|
||||
.pipe(filter(exists), take(1))
|
||||
.subscribe(config => {
|
||||
this.code = config
|
||||
})
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { filter, take } from 'rxjs/operators'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
debounce,
|
||||
exists,
|
||||
ErrorToastService,
|
||||
MarkdownComponent,
|
||||
} from '@start9labs/shared'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { getProjectId } from 'src/app/util/get-project-id'
|
||||
|
||||
@Component({
|
||||
selector: 'dev-instructions',
|
||||
@@ -16,7 +18,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
styleUrls: ['dev-instructions.page.scss'],
|
||||
})
|
||||
export class DevInstructionsPage {
|
||||
projectId: string
|
||||
readonly projectId = getProjectId(this.route)
|
||||
editorOptions = { theme: 'vs-dark', language: 'markdown' }
|
||||
code: string = ''
|
||||
saving: boolean = false
|
||||
@@ -30,11 +32,9 @@ export class DevInstructionsPage {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.projectId = this.route.snapshot.paramMap.get('projectId')
|
||||
|
||||
this.patchDb
|
||||
.watch$('ui', 'dev', this.projectId, 'instructions')
|
||||
.pipe(take(1))
|
||||
.pipe(filter(exists), take(1))
|
||||
.subscribe(config => {
|
||||
this.code = config
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { getProjectId } from 'src/app/util/get-project-id'
|
||||
|
||||
@Component({
|
||||
selector: 'dev-manifest',
|
||||
@@ -10,7 +11,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
styleUrls: ['dev-manifest.page.scss'],
|
||||
})
|
||||
export class DevManifestPage {
|
||||
projectId: string
|
||||
readonly projectId = getProjectId(this.route)
|
||||
editorOptions = { theme: 'vs-dark', language: 'yaml', readOnly: true }
|
||||
manifest: string = ''
|
||||
|
||||
@@ -20,8 +21,6 @@ export class DevManifestPage {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.projectId = this.route.snapshot.paramMap.get('projectId')
|
||||
|
||||
this.patchDb
|
||||
.watch$('ui', 'dev', this.projectId)
|
||||
.pipe(take(1))
|
||||
|
||||
@@ -231,9 +231,7 @@ const SAMPLE_CONFIG: ConfigSpec = {
|
||||
masked: false,
|
||||
copyable: false,
|
||||
// optional
|
||||
warning: null,
|
||||
description: 'Example description for required string input.',
|
||||
default: null,
|
||||
placeholder: 'Enter string value',
|
||||
pattern: '^[a-zA-Z0-9! _]+$',
|
||||
'pattern-description': 'Must be alphanumeric (may contain underscore).',
|
||||
@@ -248,14 +246,12 @@ const SAMPLE_CONFIG: ConfigSpec = {
|
||||
warning: 'Example warning to display when changing this number value.',
|
||||
units: 'ms',
|
||||
description: 'Example description for optional number input.',
|
||||
default: null,
|
||||
placeholder: 'Enter number value',
|
||||
},
|
||||
'sample-boolean': {
|
||||
type: 'boolean',
|
||||
name: 'Example Boolean Toggle',
|
||||
// optional
|
||||
warning: null,
|
||||
description: 'Example description for boolean toggle',
|
||||
default: true,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="/developer"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ patchDb.data.ui.dev[projectId].name}}</ion-title>
|
||||
<ion-title>{{ name }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button routerLink="manifest">View Manifest</ion-button>
|
||||
</ion-buttons>
|
||||
|
||||
@@ -5,10 +5,10 @@ import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { BasicInfo, getBasicInfoSpec } from './form-info'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { ErrorToastService, DestroyService } from '@start9labs/shared'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
import { DevProjectData } from 'src/app/services/patch-db/data-model'
|
||||
import { DestroyService } from '../../../../../../shared/src/services/destroy.service'
|
||||
import { getProjectId } from 'src/app/util/get-project-id'
|
||||
import * as yaml from 'js-yaml'
|
||||
|
||||
@Component({
|
||||
@@ -18,7 +18,7 @@ import * as yaml from 'js-yaml'
|
||||
providers: [DestroyService],
|
||||
})
|
||||
export class DeveloperMenuPage {
|
||||
projectId: string
|
||||
readonly projectId = getProjectId(this.route)
|
||||
projectData: DevProjectData
|
||||
|
||||
constructor(
|
||||
@@ -28,12 +28,14 @@ export class DeveloperMenuPage {
|
||||
private readonly api: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly destroy$: DestroyService,
|
||||
public readonly patchDb: PatchDbService,
|
||||
private readonly patchDb: PatchDbService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.projectId = this.route.snapshot.paramMap.get('projectId')
|
||||
get name(): string {
|
||||
return this.patchDb.data.ui?.dev?.[this.projectId]?.name || ''
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.patchDb
|
||||
.watch$('ui', 'dev', this.projectId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -51,14 +53,14 @@ export class DeveloperMenuPage {
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: basicInfo => {
|
||||
handler: (basicInfo: any) => {
|
||||
basicInfo.description = {
|
||||
short: basicInfo.short,
|
||||
long: basicInfo.long,
|
||||
}
|
||||
delete basicInfo.short
|
||||
delete basicInfo.long
|
||||
this.saveBasicInfo(basicInfo as BasicInfo)
|
||||
this.saveBasicInfo(basicInfo)
|
||||
},
|
||||
isSubmit: true,
|
||||
},
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<ion-content class="ion-padding">
|
||||
<marketplace-list-content
|
||||
*ngIf="loaded else loading"
|
||||
[localPkgs]="localPkgs$ | async"
|
||||
[localPkgs]="(localPkgs$ | async) || {}"
|
||||
[pkgs]="pkgs$ | async"
|
||||
[categories]="categories$ | async"
|
||||
[name]="name$ | async"
|
||||
[name]="(name$ | async) || ''"
|
||||
></marketplace-list-content>
|
||||
|
||||
<ng-template #loading>
|
||||
|
||||
@@ -25,7 +25,7 @@ export class MarketplaceShowControlsComponent {
|
||||
pkg: MarketplacePkg
|
||||
|
||||
@Input()
|
||||
localPkg: PackageDataEntry
|
||||
localPkg: PackageDataEntry | null = null
|
||||
|
||||
readonly PackageState = PackageState
|
||||
|
||||
@@ -77,7 +77,7 @@ export class MarketplaceShowControlsComponent {
|
||||
title,
|
||||
version,
|
||||
serviceRequirements: dependencies,
|
||||
installAlert: alerts.install,
|
||||
installAlert: alerts.install || undefined,
|
||||
}
|
||||
|
||||
const { cancelled } = await wizardModal(
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<ng-container *ngIf="!(pkg | empty)">
|
||||
<marketplace-package [pkg]="pkg">
|
||||
<marketplace-status
|
||||
*ngIf="localPkg$ | async as localPkg"
|
||||
class="status"
|
||||
[version]="pkg.manifest.version"
|
||||
[localPkg]="localPkg$ | async"
|
||||
[localPkg]="localPkg"
|
||||
></marketplace-status>
|
||||
<marketplace-show-controls
|
||||
position="controls"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { ErrorToastService, getPkgId } from '@start9labs/shared'
|
||||
import {
|
||||
MarketplacePkg,
|
||||
AbstractMarketplaceService,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs'
|
||||
import { catchError, filter, shareReplay, switchMap, tap } from 'rxjs/operators'
|
||||
import { catchError, filter, shareReplay, switchMap } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show',
|
||||
@@ -17,7 +17,7 @@ import { catchError, filter, shareReplay, switchMap, tap } from 'rxjs/operators'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowPage {
|
||||
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
private readonly pkgId = getPkgId(this.route)
|
||||
|
||||
readonly loadVersion$ = new BehaviorSubject<string>('*')
|
||||
|
||||
@@ -28,12 +28,16 @@ export class MarketplaceShowPage {
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
readonly pkg$: Observable<MarketplacePkg> = this.loadVersion$.pipe(
|
||||
readonly pkg$: Observable<MarketplacePkg | null> = this.loadVersion$.pipe(
|
||||
switchMap(version =>
|
||||
this.marketplaceService.getPackage(this.pkgId, version),
|
||||
),
|
||||
// TODO: Better fallback
|
||||
catchError(e => this.errToast.present(e) && of({} as MarketplacePkg)),
|
||||
catchError(e => {
|
||||
this.errToast.present(e)
|
||||
|
||||
return of({} as MarketplacePkg)
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -6,8 +6,8 @@ import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
|
||||
name: 'installProgress',
|
||||
})
|
||||
export class InstallProgressPipe implements PipeTransform {
|
||||
transform(loadData: InstallProgress): string {
|
||||
const { totalProgress } = packageLoadingProgress(loadData)
|
||||
transform(loadData?: InstallProgress): string {
|
||||
const totalProgress = packageLoadingProgress(loadData)?.totalProgress || 0
|
||||
|
||||
return totalProgress < 99 ? totalProgress + '%' : 'finalizing'
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
templateUrl: './release-notes.page.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReleaseNotesPage {
|
||||
readonly href = `/marketplace/${this.route.snapshot.paramMap.get('pkgId')}`
|
||||
readonly href = `/marketplace/${getPkgId(this.route)}`
|
||||
|
||||
constructor(private readonly route: ActivatedRoute) {}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
export class NotificationsPage {
|
||||
loading = true
|
||||
notifications: ServerNotifications = []
|
||||
beforeCursor: number
|
||||
beforeCursor?: number
|
||||
needInfinite = false
|
||||
fromToast = false
|
||||
readonly perPage = 40
|
||||
@@ -51,19 +51,23 @@ export class NotificationsPage {
|
||||
}
|
||||
|
||||
async getNotifications(): Promise<ServerNotifications> {
|
||||
let notifications: ServerNotifications = []
|
||||
try {
|
||||
notifications = await this.embassyApi.getNotifications({
|
||||
const notifications = await this.embassyApi.getNotifications({
|
||||
before: this.beforeCursor,
|
||||
limit: this.perPage,
|
||||
})
|
||||
|
||||
if (!notifications) return []
|
||||
|
||||
this.beforeCursor = notifications[notifications.length - 1]?.id
|
||||
this.needInfinite = notifications.length >= this.perPage
|
||||
|
||||
return notifications
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
return notifications
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async delete(id: number, index: number): Promise<void> {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
>instructions</a
|
||||
>.
|
||||
</h2>
|
||||
<ng-container *ngIf="downloadIsDisabled && server$ | async as server">
|
||||
<ng-container *ngIf="downloadIsDisabled && (server$ | async) as server">
|
||||
<br />
|
||||
<ion-text color="warning">
|
||||
For security reasons, you must setup LAN over a
|
||||
|
||||
@@ -8,8 +8,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
styleUrls: ['./lan.page.scss'],
|
||||
})
|
||||
export class LANPage {
|
||||
downloadIsDisabled: boolean
|
||||
|
||||
readonly downloadIsDisabled = !this.config.isTor()
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
|
||||
constructor(
|
||||
@@ -17,11 +16,7 @@ export class LANPage {
|
||||
private readonly patch: PatchDbService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.downloadIsDisabled = !this.config.isTor()
|
||||
}
|
||||
|
||||
installCert(): void {
|
||||
document.getElementById('install-cert').click()
|
||||
document.getElementById('install-cert')?.click()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,19 @@ import { v4 } from 'uuid'
|
||||
import { UIMarketplaceData } from '../../../services/patch-db/data-model'
|
||||
import { ConfigService } from '../../../services/config.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { finalize, first } from 'rxjs/operators'
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
finalize,
|
||||
first,
|
||||
map,
|
||||
startWith,
|
||||
} from 'rxjs/operators'
|
||||
|
||||
type Marketplaces = {
|
||||
id: string | undefined
|
||||
name: string
|
||||
url: string
|
||||
}[]
|
||||
|
||||
@Component({
|
||||
selector: 'marketplaces',
|
||||
@@ -24,7 +36,7 @@ import { finalize, first } from 'rxjs/operators'
|
||||
})
|
||||
export class MarketplacesPage {
|
||||
selectedId: string | undefined
|
||||
marketplaces: { id: string | undefined; name: string; url: string }[] = []
|
||||
marketplaces: Marketplaces = []
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
@@ -39,8 +51,14 @@ export class MarketplacesPage {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.patch.watch$('ui', 'marketplace').subscribe(mp => {
|
||||
const marketplaces = [
|
||||
this.patch
|
||||
.watch$('ui')
|
||||
.pipe(
|
||||
map(ui => ui.marketplace),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
.subscribe(mp => {
|
||||
let marketplaces: Marketplaces = [
|
||||
{
|
||||
id: undefined,
|
||||
name: this.config.marketplace.name,
|
||||
@@ -48,7 +66,7 @@ export class MarketplacesPage {
|
||||
},
|
||||
]
|
||||
if (mp) {
|
||||
this.selectedId = mp['selected-id']
|
||||
this.selectedId = mp['selected-id'] || undefined
|
||||
const alts = Object.entries(mp['known-hosts']).map(([k, v]) => {
|
||||
return {
|
||||
id: k,
|
||||
@@ -56,7 +74,7 @@ export class MarketplacesPage {
|
||||
url: v.url,
|
||||
}
|
||||
})
|
||||
marketplaces.push.apply(marketplaces, alts)
|
||||
marketplaces = marketplaces.concat(alts)
|
||||
}
|
||||
this.marketplaces = marketplaces
|
||||
})
|
||||
@@ -91,9 +109,10 @@ export class MarketplacesPage {
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentAction(id: string) {
|
||||
async presentAction(id: string = '') {
|
||||
// no need to view actions if is selected marketplace
|
||||
if (id === this.patch.getData().ui.marketplace?.['selected-id']) return
|
||||
if (!id || id === this.patch.getData().ui.marketplace?.['selected-id'])
|
||||
return
|
||||
|
||||
const buttons: ActionSheetButton[] = [
|
||||
{
|
||||
@@ -200,7 +219,10 @@ export class MarketplacesPage {
|
||||
? (JSON.parse(
|
||||
JSON.stringify(this.patch.getData().ui.marketplace),
|
||||
) as UIMarketplaceData)
|
||||
: { 'selected-id': undefined, 'known-hosts': {} }
|
||||
: {
|
||||
'selected-id': undefined,
|
||||
'known-hosts': {} as Record<string, unknown>,
|
||||
}
|
||||
|
||||
// no-op on duplicates
|
||||
const currentUrls = this.marketplaces.map(mp => mp.url)
|
||||
@@ -242,7 +264,10 @@ export class MarketplacesPage {
|
||||
? (JSON.parse(
|
||||
JSON.stringify(this.patch.getData().ui.marketplace),
|
||||
) as UIMarketplaceData)
|
||||
: { 'selected-id': undefined, 'known-hosts': {} }
|
||||
: {
|
||||
'selected-id': undefined,
|
||||
'known-hosts': {} as Record<string, unknown>,
|
||||
}
|
||||
|
||||
// no-op on duplicates
|
||||
const currentUrls = this.marketplaces.map(mp => mp.url)
|
||||
|
||||
@@ -43,7 +43,8 @@ export class RestorePage {
|
||||
useMask: true,
|
||||
buttonText: 'Next',
|
||||
submitFn: async (password: string) => {
|
||||
argon2.verify(target.entry['embassy-os']['password-hash'], password)
|
||||
const passwordHash = target.entry['embassy-os']?.['password-hash'] || ''
|
||||
argon2.verify(passwordHash, password)
|
||||
await this.restoreFromBackup(target, password)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class ServerBackupPage {
|
||||
} else {
|
||||
if (this.backingUp) {
|
||||
this.backingUp = false
|
||||
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||
this.pkgs.forEach(pkg => pkg.sub?.unsubscribe())
|
||||
this.navCtrl.navigateRoot('/embassy')
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export class ServerBackupPage {
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||
this.pkgs.forEach(pkg => pkg.sub?.unsubscribe())
|
||||
}
|
||||
|
||||
async presentModalPassword(
|
||||
@@ -98,7 +98,10 @@ export class ServerBackupPage {
|
||||
// existing backup
|
||||
} else {
|
||||
try {
|
||||
argon2.verify(target.entry['embassy-os']['password-hash'], password)
|
||||
const passwordHash =
|
||||
target.entry['embassy-os']?.['password-hash'] || ''
|
||||
|
||||
argon2.verify(passwordHash, password)
|
||||
} catch {
|
||||
setTimeout(
|
||||
() => this.presentModalOldPassword(target, password),
|
||||
@@ -133,7 +136,9 @@ export class ServerBackupPage {
|
||||
useMask: true,
|
||||
buttonText: 'Create Backup',
|
||||
submitFn: async (oldPassword: string) => {
|
||||
argon2.verify(target.entry['embassy-os']['password-hash'], oldPassword)
|
||||
const passwordHash = target.entry['embassy-os']?.['password-hash'] || ''
|
||||
|
||||
argon2.verify(passwordHash, oldPassword)
|
||||
await this.createBackup(target.id, password, oldPassword)
|
||||
},
|
||||
}
|
||||
@@ -182,15 +187,11 @@ export class ServerBackupPage {
|
||||
pkg.installed?.status.main.status === PackageMainStatus.BackingUp,
|
||||
)
|
||||
|
||||
this.pkgs = pkgArr.map((pkg, i) => {
|
||||
const pkgInfo = {
|
||||
this.pkgs = pkgArr.map((pkg, i) => ({
|
||||
entry: pkg,
|
||||
active: i === activeIndex,
|
||||
complete: i < activeIndex,
|
||||
sub: null,
|
||||
}
|
||||
return pkgInfo
|
||||
})
|
||||
}))
|
||||
|
||||
// subscribe to pkg
|
||||
this.pkgs.forEach(pkg => {
|
||||
@@ -220,5 +221,5 @@ interface PkgInfo {
|
||||
entry: PackageDataEntry
|
||||
active: boolean
|
||||
complete: boolean
|
||||
sub: Subscription
|
||||
sub?: Subscription
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ export class ServerLogsPage {
|
||||
needInfinite = true
|
||||
before: string
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
private readonly embassyApi: ApiService,
|
||||
) { }
|
||||
|
||||
fetchFetchLogs () {
|
||||
fetchFetchLogs() {
|
||||
return async (params: { before_flag?: boolean, limit?: number, cursor?: string }) => {
|
||||
return this.embassyApi.getServerLogs({
|
||||
before_flag: params.before_flag,
|
||||
@@ -25,4 +25,22 @@ export class ServerLogsPage {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async copy(): Promise<void> {
|
||||
const logs = document
|
||||
.getElementById('template')
|
||||
?.cloneNode(true) as HTMLElement
|
||||
const formatted = '```' + logs.innerHTML + '```'
|
||||
const success = await copyToClipboard(formatted)
|
||||
const message = success
|
||||
? 'Copied to clipboard!'
|
||||
: 'Failed to copy to clipboard.'
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ServerMetricsPage {
|
||||
})
|
||||
const height = headersCount * 54 + rowsCount * 50 + 24 // extra 24 for room at the bottom
|
||||
const elem = document.getElementById('metricSection')
|
||||
elem.style.height = `${height}px`
|
||||
if (elem) elem.style.height = `${height}px`
|
||||
this.startDaemon()
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title *ngIf="patch.loaded else loading">
|
||||
{{ (ui$ | async).name || "Embassy-" + (server$ | async).id }}
|
||||
{{ (ui$ | async)?.name || "Embassy-" + (server$ | async)?.id }}
|
||||
</ion-title>
|
||||
<ng-template #loading>
|
||||
<ion-title>Loading<span class="loading-dots"></span></ion-title>
|
||||
|
||||
@@ -42,7 +42,7 @@ export class WifiPage {
|
||||
await this.getWifi()
|
||||
}
|
||||
|
||||
async getWifi(timeout?: number): Promise<void> {
|
||||
async getWifi(timeout: number = 0): Promise<void> {
|
||||
this.loading = true
|
||||
try {
|
||||
this.wifi = await this.api.getWifi({}, timeout)
|
||||
|
||||
@@ -18,7 +18,7 @@ export class Range {
|
||||
|
||||
checkIncludes(n: number) {
|
||||
if (
|
||||
this.hasMin() !== undefined &&
|
||||
this.hasMin() &&
|
||||
(this.min > n || (!this.minInclusive && this.min == n))
|
||||
) {
|
||||
throw new Error(this.minMessage())
|
||||
@@ -31,11 +31,11 @@ export class Range {
|
||||
}
|
||||
}
|
||||
|
||||
hasMin(): boolean {
|
||||
hasMin(): this is Range & { min: number } {
|
||||
return this.min !== undefined
|
||||
}
|
||||
|
||||
hasMax(): boolean {
|
||||
hasMax(): this is Range & { max: number } {
|
||||
return this.max !== undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -177,8 +177,6 @@ export module Mock {
|
||||
nullable: true,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
pattern: null,
|
||||
'pattern-description': null,
|
||||
warning: 'You may loose all your money by providing your name.',
|
||||
},
|
||||
notifications: {
|
||||
@@ -213,7 +211,6 @@ export module Mock {
|
||||
name: 'Top Speed',
|
||||
description: 'The fastest you can possibly run.',
|
||||
nullable: false,
|
||||
default: null,
|
||||
range: '[-1000, 1000]',
|
||||
integral: false,
|
||||
units: 'm/s',
|
||||
@@ -248,7 +245,6 @@ export module Mock {
|
||||
name: {
|
||||
type: 'string',
|
||||
name: 'Name',
|
||||
description: null,
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
@@ -258,7 +254,6 @@ export module Mock {
|
||||
email: {
|
||||
type: 'string',
|
||||
name: 'Email',
|
||||
description: null,
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: true,
|
||||
@@ -1187,7 +1182,6 @@ export module Mock {
|
||||
type: 'string',
|
||||
description: 'User first name',
|
||||
nullable: true,
|
||||
default: null,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
},
|
||||
@@ -1210,7 +1204,6 @@ export module Mock {
|
||||
type: 'number',
|
||||
description: 'The age of the user',
|
||||
nullable: true,
|
||||
default: null,
|
||||
integral: false,
|
||||
warning: 'User must be at least 18.',
|
||||
range: '[18,*)',
|
||||
|
||||
@@ -265,7 +265,7 @@ export module RR {
|
||||
}
|
||||
|
||||
export type WithExpire<T> = { 'expire-id'?: string } & T
|
||||
export type WithRevision<T> = { response: T; revision?: Revision }
|
||||
export type WithRevision<T> = { response: T | null; revision?: Revision }
|
||||
|
||||
export interface MarketplaceEOS {
|
||||
version: string
|
||||
|
||||
@@ -99,7 +99,7 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async killSessions(params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
return { response: null }
|
||||
}
|
||||
|
||||
// server
|
||||
@@ -749,13 +749,14 @@ export class MockApiService extends ApiService {
|
||||
{ progress: 'downloaded', completion: 'download-complete' },
|
||||
{ progress: 'validated', completion: 'validation-complete' },
|
||||
{ progress: 'unpacked', completion: 'unpack-complete' },
|
||||
]
|
||||
] as const
|
||||
|
||||
for (let phase of phases) {
|
||||
let i = progress[phase.progress]
|
||||
while (i < progress.size) {
|
||||
const size = progress?.size || 0
|
||||
while (i < size) {
|
||||
await pauseFor(250)
|
||||
i = Math.min(i + 5, progress.size)
|
||||
i = Math.min(i + 5, size)
|
||||
progress[phase.progress] = i
|
||||
|
||||
if (i === progress.size) {
|
||||
@@ -858,7 +859,7 @@ export class MockApiService extends ApiService {
|
||||
|
||||
private async withRevision<T>(
|
||||
patch: Operation<unknown>[],
|
||||
response: T = null,
|
||||
response: T | null = null,
|
||||
): Promise<WithRevision<T>> {
|
||||
if (!this.sequence) {
|
||||
const { sequence } = await this.bootstrapper.init()
|
||||
|
||||
@@ -11,12 +11,9 @@ import {
|
||||
export const mockPatchData: DataModel = {
|
||||
ui: {
|
||||
name: `Matt's Embassy`,
|
||||
'auto-check-updates': undefined,
|
||||
'auto-check-updates': false,
|
||||
'pkg-order': [],
|
||||
'ack-welcome': '1.0.0',
|
||||
marketplace: undefined,
|
||||
dev: undefined,
|
||||
gaming: undefined,
|
||||
},
|
||||
'server-info': {
|
||||
id: 'abcdefgh',
|
||||
@@ -212,8 +209,6 @@ export const mockPatchData: DataModel = {
|
||||
nullable: true,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
pattern: null,
|
||||
'pattern-description': null,
|
||||
warning: 'You may loose all your money by providing your name.',
|
||||
},
|
||||
notifications: {
|
||||
@@ -248,7 +243,6 @@ export const mockPatchData: DataModel = {
|
||||
name: 'Top Speed',
|
||||
description: 'The fastest you can possibly run.',
|
||||
nullable: false,
|
||||
default: null,
|
||||
range: '[-1000, 1000]',
|
||||
integral: false,
|
||||
units: 'm/s',
|
||||
@@ -283,7 +277,6 @@ export const mockPatchData: DataModel = {
|
||||
name: {
|
||||
type: 'string',
|
||||
name: 'Name',
|
||||
description: null,
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
@@ -293,7 +286,6 @@ export const mockPatchData: DataModel = {
|
||||
email: {
|
||||
type: 'string',
|
||||
name: 'Email',
|
||||
description: null,
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: true,
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ConfigService {
|
||||
api = api
|
||||
marketplace = marketplace
|
||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||
isConsulate = window['platform'] === 'ios'
|
||||
isConsulate = (window as any)['platform'] === 'ios'
|
||||
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
||||
|
||||
isTor(): boolean {
|
||||
@@ -76,14 +76,20 @@ export function hasLanUi(interfaces: Record<string, InterfaceDef>): boolean {
|
||||
return !!int?.['lan-config']
|
||||
}
|
||||
|
||||
export function torUiAddress(pkg: PackageDataEntry): string {
|
||||
const key = getUiInterfaceKey(pkg.manifest.interfaces)
|
||||
return pkg.installed['interface-addresses'][key]['tor-address']
|
||||
export function torUiAddress({
|
||||
manifest,
|
||||
installed,
|
||||
}: PackageDataEntry): string {
|
||||
const key = getUiInterfaceKey(manifest.interfaces)
|
||||
return installed ? installed['interface-addresses'][key]['tor-address'] : ''
|
||||
}
|
||||
|
||||
export function lanUiAddress(pkg: PackageDataEntry): string {
|
||||
const key = getUiInterfaceKey(pkg.manifest.interfaces)
|
||||
return pkg.installed['interface-addresses'][key]['lan-address']
|
||||
export function lanUiAddress({
|
||||
manifest,
|
||||
installed,
|
||||
}: PackageDataEntry): string {
|
||||
const key = getUiInterfaceKey(manifest.interfaces)
|
||||
return installed ? installed['interface-addresses'][key]['lan-address'] : ''
|
||||
}
|
||||
|
||||
export function hasUi(interfaces: Record<string, InterfaceDef>): boolean {
|
||||
@@ -103,11 +109,11 @@ export function removePort(str: string): string {
|
||||
export function getUiInterfaceKey(
|
||||
interfaces: Record<string, InterfaceDef>,
|
||||
): string {
|
||||
return Object.keys(interfaces).find(key => interfaces[key].ui)
|
||||
return Object.keys(interfaces).find(key => interfaces[key].ui) || ''
|
||||
}
|
||||
|
||||
export function getUiInterfaceValue(
|
||||
interfaces: Record<string, InterfaceDef>,
|
||||
): InterfaceDef {
|
||||
return Object.values(interfaces).find(i => i.ui)
|
||||
): InterfaceDef | null {
|
||||
return Object.values(interfaces).find(i => i.ui) || null
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export class FormService {
|
||||
validators: ValidatorFn[] = [],
|
||||
current: { [key: string]: any } = {},
|
||||
): FormGroup {
|
||||
let group = {}
|
||||
let group: Record<string, FormGroup | FormArray | FormControl> = {}
|
||||
Object.entries(config).map(([key, spec]) => {
|
||||
if (spec.type === 'pointer') return
|
||||
group[key] = this.getFormEntry(spec, current ? current[key] : undefined)
|
||||
@@ -137,6 +137,8 @@ export class FormService {
|
||||
case 'enum':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value)
|
||||
default:
|
||||
return this.formBuilder.control(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,25 +300,20 @@ export function listUnique(spec: ValueSpecList): ValidatorFn {
|
||||
}
|
||||
|
||||
function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean {
|
||||
// TODO: fix types
|
||||
switch (spec.subtype) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'enum':
|
||||
return val1 == val2
|
||||
case 'object':
|
||||
return listObjEquals(
|
||||
spec.spec['unique-by'],
|
||||
spec.spec as ListValueSpecObject,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
const obj: ListValueSpecObject = spec.spec as any
|
||||
|
||||
return listObjEquals(obj['unique-by'], obj, val1, val2)
|
||||
case 'union':
|
||||
return unionEquals(
|
||||
spec.spec['unique-by'],
|
||||
spec.spec as ListValueSpecUnion,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
const union: ListValueSpecUnion = spec.spec as any
|
||||
|
||||
return unionEquals(union['unique-by'], union, val1, val2)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -330,9 +327,21 @@ function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean {
|
||||
case 'enum':
|
||||
return val1 == val2
|
||||
case 'object':
|
||||
return objEquals(spec['unique-by'], spec as ValueSpecObject, val1, val2)
|
||||
// TODO: 'unique-by' does not exist on ValueSpecObject, fix types
|
||||
return objEquals(
|
||||
(spec as any)['unique-by'],
|
||||
spec as ValueSpecObject,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
case 'union':
|
||||
return unionEquals(spec['unique-by'], spec as ValueSpecUnion, val1, val2)
|
||||
// TODO: 'unique-by' does not exist on ValueSpecUnion, fix types
|
||||
return unionEquals(
|
||||
(spec as any)['unique-by'],
|
||||
spec as ValueSpecUnion,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
case 'list':
|
||||
if (val1.length !== val2.length) {
|
||||
return false
|
||||
@@ -373,6 +382,7 @@ function listObjEquals(
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function objEquals(
|
||||
@@ -384,7 +394,8 @@ function objEquals(
|
||||
if (uniqueBy === null) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return itemEquals(spec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
// TODO: fix types
|
||||
return itemEquals((spec as any)[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
} else if ('any' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
if (objEquals(subSpec, spec, val1, val2)) {
|
||||
@@ -400,6 +411,7 @@ function objEquals(
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function unionEquals(
|
||||
@@ -433,12 +445,13 @@ function unionEquals(
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function uniqueByMessageWrapper(
|
||||
uniqueBy: UniqueBy,
|
||||
spec: ListValueSpecObject | ListValueSpecUnion,
|
||||
obj: object,
|
||||
obj: Record<string, string>,
|
||||
) {
|
||||
let configSpec: ConfigSpec
|
||||
if (isUnion(spec)) {
|
||||
@@ -460,9 +473,9 @@ function uniqueByMessage(
|
||||
outermost = true,
|
||||
): string {
|
||||
let joinFunc
|
||||
const subSpecs = []
|
||||
const subSpecs: string[] = []
|
||||
if (uniqueBy === null) {
|
||||
return null
|
||||
return ''
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return configSpec[uniqueBy] ? configSpec[uniqueBy].name : uniqueBy
|
||||
} else if ('any' in uniqueBy) {
|
||||
@@ -476,7 +489,7 @@ function uniqueByMessage(
|
||||
subSpecs.push(uniqueByMessage(subSpec, configSpec, false))
|
||||
}
|
||||
}
|
||||
const ret = subSpecs.filter(ss => ss).join(joinFunc)
|
||||
const ret = subSpecs.filter(Boolean).join(joinFunc)
|
||||
return outermost || subSpecs.filter(ss => ss).length === 1
|
||||
? ret
|
||||
: '(' + ret + ')'
|
||||
@@ -486,7 +499,7 @@ function isObjectOrUnion(
|
||||
spec: ListValueSpecOf<any>,
|
||||
): spec is ListValueSpecObject | ListValueSpecUnion {
|
||||
// only lists of objects and unions have unique-by
|
||||
return spec['unique-by'] !== undefined
|
||||
return 'unique-by' in spec
|
||||
}
|
||||
|
||||
function isUnion(spec: any): spec is ListValueSpecUnion {
|
||||
@@ -499,18 +512,20 @@ export function convertValuesRecursive(
|
||||
group: FormGroup,
|
||||
) {
|
||||
Object.entries(configSpec).forEach(([key, valueSpec]) => {
|
||||
if (valueSpec.type === 'number') {
|
||||
const control = group.get(key)
|
||||
|
||||
if (!control) return
|
||||
|
||||
if (valueSpec.type === 'number') {
|
||||
control.setValue(control.value ? Number(control.value) : null)
|
||||
} else if (valueSpec.type === 'string') {
|
||||
const control = group.get(key)
|
||||
if (!control.value) control.setValue(null)
|
||||
} else if (valueSpec.type === 'object') {
|
||||
convertValuesRecursive(valueSpec.spec, group.get(key) as FormGroup)
|
||||
} else if (valueSpec.type === 'union') {
|
||||
const control = group.get(key) as FormGroup
|
||||
const spec = valueSpec.variants[control.controls[valueSpec.tag.id].value]
|
||||
convertValuesRecursive(spec, control)
|
||||
const formGr = group.get(key) as FormGroup
|
||||
const spec = valueSpec.variants[formGr.controls[valueSpec.tag.id].value]
|
||||
convertValuesRecursive(spec, formGr)
|
||||
} else if (valueSpec.type === 'list') {
|
||||
const formArr = group.get(key) as FormArray
|
||||
const { controls } = formArr
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
HttpClient,
|
||||
HttpErrorResponse,
|
||||
HttpHeaders,
|
||||
HttpParams,
|
||||
} from '@angular/common/http'
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
|
||||
import { Observable, from, interval, race } from 'rxjs'
|
||||
import { map, take } from 'rxjs/operators'
|
||||
import { ConfigService } from './config.service'
|
||||
@@ -27,6 +22,7 @@ export class HttpService {
|
||||
this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}`
|
||||
}
|
||||
|
||||
// @ts-ignore TODO: fix typing
|
||||
async rpcRequest<T>(rpcOpts: RPCOptions): Promise<T> {
|
||||
const { url, version } = this.config.api
|
||||
rpcOpts.params = rpcOpts.params || {}
|
||||
@@ -53,12 +49,15 @@ export class HttpService {
|
||||
|
||||
const urlIsRelative = httpOpts.url.startsWith('/')
|
||||
const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url
|
||||
const { params } = httpOpts
|
||||
|
||||
Object.keys(httpOpts.params || {}).forEach(key => {
|
||||
if (httpOpts.params[key] === undefined) {
|
||||
delete httpOpts.params[key]
|
||||
if (hasParams(params)) {
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const options = {
|
||||
responseType: httpOpts.responseType || 'json',
|
||||
@@ -181,6 +180,12 @@ export interface HttpOptions {
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
function hasParams(
|
||||
params?: HttpOptions['params'],
|
||||
): params is Record<string, string | string[]> {
|
||||
return !!params
|
||||
}
|
||||
|
||||
function withTimeout<U>(req: Observable<U>, timeout: number): Observable<U> {
|
||||
return race(
|
||||
from(req.toPromise()), // this guarantees it only emits on completion, intermediary emissions are suppressed.
|
||||
|
||||
@@ -5,16 +5,20 @@ import {
|
||||
MarketplacePkg,
|
||||
AbstractMarketplaceService,
|
||||
Marketplace,
|
||||
MarketplaceData,
|
||||
} from '@start9labs/marketplace'
|
||||
import { defer, from, Observable, of } from 'rxjs'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ServerInfo } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
ServerInfo,
|
||||
UIMarketplaceData,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import {
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
shareReplay,
|
||||
startWith,
|
||||
@@ -27,33 +31,54 @@ import {
|
||||
export class MarketplaceService extends AbstractMarketplaceService {
|
||||
private readonly notes = new Map<string, Record<string, string>>()
|
||||
|
||||
private readonly init$: Observable<Marketplace> = this.patch
|
||||
.watch$('ui', 'marketplace')
|
||||
.pipe(
|
||||
map(marketplace =>
|
||||
marketplace?.['selected-id']
|
||||
? marketplace['known-hosts'][marketplace['selected-id']]
|
||||
: this.config.marketplace,
|
||||
),
|
||||
shareReplay(),
|
||||
private readonly altMarketplaceData$: Observable<
|
||||
UIMarketplaceData | undefined
|
||||
> = this.patch.watch$('ui').pipe(
|
||||
map(ui => ui.marketplace),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
private readonly data$: Observable<MarketplaceData> = this.init$.pipe(
|
||||
private readonly marketplace$ = this.altMarketplaceData$.pipe(
|
||||
map(data => this.toMarketplace(data)),
|
||||
)
|
||||
|
||||
private readonly serverInfo$: Observable<ServerInfo> = this.patch
|
||||
.watch$('server-info')
|
||||
.pipe(take(1), shareReplay({ bufferSize: 1, refCount: true }))
|
||||
|
||||
private readonly categories$: Observable<string[]> = this.marketplace$.pipe(
|
||||
switchMap(({ url }) =>
|
||||
from(this.getMarketplaceData({ 'server-id': this.serverInfo.id }, url)),
|
||||
this.serverInfo$.pipe(
|
||||
switchMap(({ id }) =>
|
||||
from(this.getMarketplaceData({ 'server-id': id }, url)),
|
||||
),
|
||||
shareReplay(),
|
||||
),
|
||||
),
|
||||
map(({ categories }) => categories),
|
||||
)
|
||||
|
||||
private readonly pkg$: Observable<MarketplacePkg[]> = this.init$.pipe(
|
||||
take(1),
|
||||
switchMap(({ url, name }) =>
|
||||
from(this.getMarketplacePkgs({ page: 1, 'per-page': 100 }, url)).pipe(
|
||||
tap(() => this.onPackages(name)),
|
||||
private readonly pkg$: Observable<MarketplacePkg[]> =
|
||||
this.altMarketplaceData$.pipe(
|
||||
switchMap(data =>
|
||||
this.serverInfo$.pipe(
|
||||
switchMap(info =>
|
||||
from(
|
||||
this.getMarketplacePkgs(
|
||||
{ page: 1, 'per-page': 100 },
|
||||
this.toMarketplace(data).url,
|
||||
info['eos-version-compat'],
|
||||
),
|
||||
).pipe(tap(() => this.onPackages(data))),
|
||||
),
|
||||
),
|
||||
shareReplay(),
|
||||
catchError(e => this.errToast.present(e) && of([])),
|
||||
),
|
||||
catchError(e => {
|
||||
this.errToast.present(e)
|
||||
|
||||
return of([])
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
constructor(
|
||||
@@ -68,22 +93,29 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
}
|
||||
|
||||
getMarketplace(): Observable<Marketplace> {
|
||||
return this.init$
|
||||
return this.marketplace$
|
||||
}
|
||||
|
||||
getCategories(): Observable<string[]> {
|
||||
return this.data$.pipe(map(({ categories }) => categories))
|
||||
return this.categories$
|
||||
}
|
||||
|
||||
getPackages(): Observable<MarketplacePkg[]> {
|
||||
return this.pkg$
|
||||
}
|
||||
|
||||
getPackage(id: string, version: string): Observable<MarketplacePkg> {
|
||||
getPackage(id: string, version: string): Observable<MarketplacePkg | null> {
|
||||
const params = { ids: [{ id, version }] }
|
||||
const fallback$ = this.init$.pipe(
|
||||
take(1),
|
||||
switchMap(({ url }) => from(this.getMarketplacePkgs(params, url))),
|
||||
const fallback$ = this.marketplace$.pipe(
|
||||
switchMap(({ url }) =>
|
||||
this.serverInfo$.pipe(
|
||||
switchMap(info =>
|
||||
from(
|
||||
this.getMarketplacePkgs(params, url, info['eos-version-compat']),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
map(pkgs => this.findPackage(pkgs, id, version)),
|
||||
startWith(null),
|
||||
)
|
||||
@@ -91,23 +123,29 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
return this.getPackages().pipe(
|
||||
map(pkgs => this.findPackage(pkgs, id, version)),
|
||||
switchMap(pkg => (pkg ? of(pkg) : fallback$)),
|
||||
tap(pkg => {
|
||||
filter((pkg): pkg is MarketplacePkg | null => {
|
||||
if (pkg === undefined) {
|
||||
throw new Error(`No results for ${id}${version ? ' ' + version : ''}`)
|
||||
}
|
||||
|
||||
return true
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
getReleaseNotes(id: string): Observable<Record<string, string>> {
|
||||
if (this.notes.has(id)) {
|
||||
return of(this.notes.get(id))
|
||||
return of(this.notes.get(id) || {})
|
||||
}
|
||||
|
||||
return this.init$.pipe(
|
||||
return this.marketplace$.pipe(
|
||||
switchMap(({ url }) => this.loadReleaseNotes(id, url)),
|
||||
tap(response => this.notes.set(id, response)),
|
||||
catchError(e => this.errToast.present(e) && of({})),
|
||||
catchError(e => {
|
||||
this.errToast.present(e)
|
||||
|
||||
return of({})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -173,22 +211,19 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
async getMarketplacePkgs(
|
||||
params: Omit<RR.GetMarketplacePackagesReq, 'eos-version-compat'>,
|
||||
url: string,
|
||||
eosVersionCompat: string,
|
||||
): Promise<RR.GetMarketplacePackagesRes> {
|
||||
if (params.query) delete params.category
|
||||
if (params.ids) params.ids = JSON.stringify(params.ids)
|
||||
|
||||
const qp: RR.GetMarketplacePackagesReq = {
|
||||
...params,
|
||||
'eos-version-compat': this.serverInfo['eos-version-compat'],
|
||||
'eos-version-compat': eosVersionCompat,
|
||||
}
|
||||
|
||||
return this.api.marketplaceProxy('/package/v0/index', qp, url)
|
||||
}
|
||||
|
||||
private get serverInfo(): ServerInfo {
|
||||
return this.patch.getData()['server-info']
|
||||
}
|
||||
|
||||
private loadReleaseNotes(
|
||||
id: string,
|
||||
url: string,
|
||||
@@ -202,15 +237,15 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
)
|
||||
}
|
||||
|
||||
private onPackages(name: string) {
|
||||
const { marketplace } = this.patch.getData().ui
|
||||
private onPackages(data?: UIMarketplaceData) {
|
||||
const { name } = this.toMarketplace(data)
|
||||
|
||||
if (!marketplace?.['selected-id']) {
|
||||
if (!data?.['selected-id']) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedId = marketplace['selected-id']
|
||||
const knownHosts = marketplace['known-hosts']
|
||||
const selectedId = data['selected-id']
|
||||
const knownHosts = data['known-hosts']
|
||||
|
||||
if (knownHosts[selectedId].name !== name) {
|
||||
this.api.setDbValue({
|
||||
@@ -220,6 +255,12 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
}
|
||||
}
|
||||
|
||||
private toMarketplace(marketplace?: UIMarketplaceData): Marketplace {
|
||||
return marketplace?.['selected-id']
|
||||
? marketplace['known-hosts'][marketplace['selected-id']]
|
||||
: this.config.marketplace
|
||||
}
|
||||
|
||||
private findPackage(
|
||||
pkgs: readonly MarketplacePkg[],
|
||||
id: string,
|
||||
|
||||
@@ -17,15 +17,13 @@ export interface UIData {
|
||||
'auto-check-updates': boolean
|
||||
'pkg-order': string[]
|
||||
'ack-welcome': string // EOS version
|
||||
marketplace: UIMarketplaceData
|
||||
dev: DevData
|
||||
gaming:
|
||||
| {
|
||||
marketplace?: UIMarketplaceData
|
||||
dev?: DevData
|
||||
gaming?: {
|
||||
snake: {
|
||||
'high-score': number
|
||||
}
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
|
||||
export interface UIMarketplaceData {
|
||||
@@ -113,7 +111,7 @@ export interface CurrentDependencyInfo {
|
||||
'health-checks': string[] // array of health check IDs
|
||||
}
|
||||
|
||||
export interface Manifest extends MarketplaceManifest<DependencyConfig> {
|
||||
export interface Manifest extends MarketplaceManifest<DependencyConfig | null> {
|
||||
main: ActionImpl
|
||||
'health-checks': Record<
|
||||
string,
|
||||
@@ -124,7 +122,7 @@ export interface Manifest extends MarketplaceManifest<DependencyConfig> {
|
||||
'min-os-version': string
|
||||
interfaces: Record<string, InterfaceDef>
|
||||
backup: BackupActions
|
||||
migrations: Migrations
|
||||
migrations: Migrations | null
|
||||
actions: Record<string, Action>
|
||||
permissions: any // @TODO 0.3.1
|
||||
}
|
||||
@@ -155,8 +153,8 @@ export enum DockerIoFormat {
|
||||
}
|
||||
|
||||
export interface ConfigActions {
|
||||
get: ActionImpl
|
||||
set: ActionImpl
|
||||
get: ActionImpl | null
|
||||
set: ActionImpl | null
|
||||
}
|
||||
|
||||
export type Volume = VolumeData
|
||||
@@ -229,7 +227,7 @@ export interface Action {
|
||||
warning: string | null
|
||||
implementation: ActionImpl
|
||||
'allowed-statuses': (PackageMainStatus.Stopped | PackageMainStatus.Running)[]
|
||||
'input-spec': ConfigSpec
|
||||
'input-spec': ConfigSpec | null
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
|
||||
@@ -36,8 +36,8 @@ export function realSourceFactory(
|
||||
{ defaultView }: Document,
|
||||
): Source<DataModel>[] {
|
||||
const { patchDb } = config
|
||||
const { host } = defaultView.location
|
||||
const protocol = defaultView.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = defaultView?.location.host
|
||||
const protocol = defaultView?.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
|
||||
return [
|
||||
new WebsocketSource<DataModel>(`${protocol}://${host}/ws/db`),
|
||||
|
||||
@@ -17,7 +17,7 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
|
||||
let dependency: DependencyStatus | null = null
|
||||
let health: HealthStatus | null = null
|
||||
|
||||
if (pkg.state === PackageState.Installed) {
|
||||
if (pkg.state === PackageState.Installed && pkg.installed) {
|
||||
primary = getPrimaryStatus(pkg.installed.status)
|
||||
dependency = getDependencyStatus(pkg)
|
||||
health = getHealthStatus(pkg.installed.status)
|
||||
@@ -36,9 +36,10 @@ function getPrimaryStatus(status: Status): PrimaryStatus {
|
||||
}
|
||||
}
|
||||
|
||||
function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus {
|
||||
function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null {
|
||||
const installed = pkg.installed
|
||||
if (isEmptyObject(installed['current-dependencies'])) return null
|
||||
if (!installed || isEmptyObject(installed['current-dependencies']))
|
||||
return null
|
||||
|
||||
const depErrors = installed.status['dependency-errors']
|
||||
const depIds = Object.keys(depErrors).filter(key => !!depErrors[key])
|
||||
@@ -46,9 +47,9 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus {
|
||||
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
|
||||
}
|
||||
|
||||
function getHealthStatus(status: Status): HealthStatus {
|
||||
function getHealthStatus(status: Status): HealthStatus | null {
|
||||
if (status.main.status !== PackageMainStatus.Running || !status.main.health) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
const values = Object.values(status.main.health)
|
||||
|
||||
@@ -16,7 +16,10 @@ export class ServerConfigService {
|
||||
private readonly embassyApi: ApiService,
|
||||
) {}
|
||||
|
||||
async presentAlert(key: string, current?: any): Promise<HTMLIonAlertElement> {
|
||||
async presentAlert(
|
||||
key: string,
|
||||
current?: any,
|
||||
): Promise<HTMLIonAlertElement | null> {
|
||||
const spec = serverConfig[key]
|
||||
|
||||
let inputs: AlertInput[]
|
||||
@@ -66,7 +69,7 @@ export class ServerConfigService {
|
||||
]
|
||||
break
|
||||
default:
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
|
||||
@@ -13,7 +13,7 @@ export class UiLauncherService {
|
||||
) {}
|
||||
|
||||
launch(pkg: PackageDataEntry): void {
|
||||
this.document.defaultView.open(
|
||||
this.document.defaultView?.open(
|
||||
this.config.launchableURL(pkg),
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
|
||||
11
frontend/projects/ui/src/app/util/get-project-id.ts
Normal file
11
frontend/projects/ui/src/app/util/get-project-id.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
|
||||
export function getProjectId({ snapshot }: ActivatedRoute): string {
|
||||
const projectId = snapshot.paramMap.get('projectId')
|
||||
|
||||
if (!projectId) {
|
||||
throw new Error('projectId is missing from route params')
|
||||
}
|
||||
|
||||
return projectId
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import { InstallProgress } from 'src/app/types/install-progress'
|
||||
import { ProgressData } from 'src/app/types/progress-data'
|
||||
|
||||
export function packageLoadingProgress(
|
||||
loadData: InstallProgress,
|
||||
loadData?: InstallProgress,
|
||||
): ProgressData | null {
|
||||
if (isEmptyObject(loadData)) {
|
||||
if (!loadData || isEmptyObject(loadData)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export function packageLoadingProgress(
|
||||
} = loadData
|
||||
|
||||
// only permit 100% when "complete" == true
|
||||
size = size || 0
|
||||
downloaded = downloadComplete ? size : Math.max(downloaded - 1, 0)
|
||||
validated = validationComplete ? size : Math.max(validated - 1, 0)
|
||||
unpacked = unpackComplete ? size : Math.max(unpacked - 1, 0)
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
|
||||
export function parseDataModel(data: DataModel): ParsedData {
|
||||
const all = JSON.parse(JSON.stringify(data['package-data'])) as {
|
||||
[id: string]: PackageDataEntry
|
||||
}
|
||||
const all: Record<string, PackageDataEntry> = JSON.parse(
|
||||
JSON.stringify(data['package-data']),
|
||||
)
|
||||
|
||||
const order = [...(data.ui['pkg-order'] || [])]
|
||||
const pkgs = []
|
||||
const pkgs: PackageDataEntry[] = []
|
||||
const recoveredPkgs = Object.entries(data['recovered-packages'])
|
||||
.filter(([id, _]) => !all[id])
|
||||
.map(([id, val]) => ({
|
||||
|
||||
@@ -29,7 +29,7 @@ const matchPropertiesV1 = shape(
|
||||
qr: boolean,
|
||||
},
|
||||
['description', 'copyable', 'qr'],
|
||||
{ description: null as null, copyable: false, qr: false } as const,
|
||||
{ copyable: false, qr: false } as const,
|
||||
)
|
||||
type PropertiesV1 = typeof matchPropertiesV1._TYPE
|
||||
|
||||
@@ -49,7 +49,6 @@ const matchPackagePropertyString = shape(
|
||||
},
|
||||
['description', 'copyable', 'qr', 'masked'],
|
||||
{
|
||||
description: null as null,
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: false,
|
||||
@@ -100,16 +99,16 @@ export function parsePropertiesPermissive(
|
||||
name,
|
||||
value: {
|
||||
value: String(value),
|
||||
description: null,
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: false,
|
||||
},
|
||||
}))
|
||||
.reduce((acc, { name, value }) => {
|
||||
acc[name] = value
|
||||
// TODO: Fix type
|
||||
acc[name] = value as any
|
||||
return acc
|
||||
}, {})
|
||||
}, {} as PackageProperties)
|
||||
}
|
||||
switch (properties.version) {
|
||||
case 1:
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
"alwaysStrict": true,
|
||||
// "strictNullChecks": true,
|
||||
"strictNullChecks": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictFunctionTypes": true,
|
||||
// "strictPropertyInitialization": true,
|
||||
// "noImplicitAny": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
|
||||
|
||||
2
patch-db
2
patch-db
Submodule patch-db updated: 8e7c893d48...a3aa821847
Reference in New Issue
Block a user