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:
waterplea
2022-05-26 18:20:31 +03:00
committed by Lucy C
parent 948fb795f2
commit 0390954a85
99 changed files with 674 additions and 535 deletions

View File

@@ -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()
}
}

View File

@@ -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> {

View File

@@ -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> {

View File

@@ -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

View File

@@ -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',

View File

@@ -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'

View File

@@ -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>
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()

View File

@@ -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')

View File

@@ -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()

View File

@@ -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
}

View File

@@ -18,7 +18,7 @@ export class StateService {
embassyLoaded = false
recoverySource: CifsRecoverySource | DiskRecoverySource
recoveryPassword: string
recoveryPassword?: string
dataTransferProgress: {
bytesTransferred: number

View File

@@ -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"
},

View File

@@ -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
}
}

View File

@@ -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)
)
}
}

View File

@@ -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,

View File

@@ -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'

View File

@@ -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)
}

View File

@@ -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) {}

View 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
}

View File

@@ -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)
}

View File

@@ -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
}, {}),
)
}

View File

@@ -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))
}
}

View File

@@ -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: '' }
}
}

View File

@@ -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,

View File

@@ -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: () => {

View File

@@ -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: () => {

View File

@@ -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"
>

View File

@@ -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>

View File

@@ -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"
>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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)

View File

@@ -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: {

View File

@@ -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() {

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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()
}
})
}

View File

@@ -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
}

View File

@@ -29,7 +29,7 @@ export class GenericInputComponent {
...this.options,
}
this.value = this.options.initialValue
this.value = this.options.initialValue || ''
}
ngAfterViewInit() {

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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,
: '',
},
}
})

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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 => {

View File

@@ -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'
}
}

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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,

View File

@@ -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
})

View File

@@ -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
})

View File

@@ -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))

View File

@@ -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,
},

View File

@@ -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>

View File

@@ -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,
},

View File

@@ -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>

View File

@@ -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(

View File

@@ -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"

View File

@@ -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(

View File

@@ -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'
}

View File

@@ -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) {}
}

View File

@@ -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> {

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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)
},
}

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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,*)',

View File

@@ -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

View File

@@ -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()

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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`),

View File

@@ -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)

View File

@@ -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({

View File

@@ -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',

View 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
}

View File

@@ -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)

View File

@@ -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]) => ({

View File

@@ -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:

View File

@@ -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,