mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
feat: move all frontend projects under the same Angular workspace (#1141)
* feat: move all frontend projects under the same Angular workspace * Refactor/angular workspace (#1154) * update frontend build steps Co-authored-by: waterplea <alexander@inkin.ru> Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
2080
frontend/projects/ui/src/app/services/api/api.fixures.ts
Normal file
2080
frontend/projects/ui/src/app/services/api/api.fixures.ts
Normal file
File diff suppressed because it is too large
Load Diff
455
frontend/projects/ui/src/app/services/api/api.types.ts
Normal file
455
frontend/projects/ui/src/app/services/api/api.types.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
import { Dump, Revision } from 'patch-db-client'
|
||||
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { DataModel, DependencyError, Manifest, URL } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
export module RR {
|
||||
|
||||
// DB
|
||||
|
||||
export type GetRevisionsRes = Revision[] | Dump<DataModel>
|
||||
|
||||
export type GetDumpRes = Dump<DataModel>
|
||||
|
||||
export type SetDBValueReq = WithExpire<{ pointer: string, value: any }> // db.put.ui
|
||||
export type SetDBValueRes = WithRevision<null>
|
||||
|
||||
// auth
|
||||
|
||||
export type LoginReq = { password: string, metadata: SessionMetadata } // auth.login - unauthed
|
||||
export type loginRes = null
|
||||
|
||||
export type LogoutReq = { } // auth.logout
|
||||
export type LogoutRes = null
|
||||
|
||||
// server
|
||||
|
||||
export type SetShareStatsReq = WithExpire<{ value: boolean }> // server.config.share-stats
|
||||
export type SetShareStatsRes = WithRevision<null>
|
||||
|
||||
export type GetServerLogsReq = { cursor?: string, before_flag?: boolean, limit?: number }
|
||||
export type GetServerLogsRes = LogsRes
|
||||
|
||||
export type GetServerMetricsReq = { } // server.metrics
|
||||
export type GetServerMetricsRes = Metrics
|
||||
|
||||
export type UpdateServerReq = WithExpire<{ }> // server.update
|
||||
export type UpdateServerRes = WithRevision<'updating' | 'no-updates'>
|
||||
|
||||
export type RestartServerReq = { } // server.restart
|
||||
export type RestartServerRes = null
|
||||
|
||||
export type ShutdownServerReq = { } // server.shutdown
|
||||
export type ShutdownServerRes = null
|
||||
|
||||
export type SystemRebuildReq = { } // server.rebuild
|
||||
export type SystemRebuildRes = null
|
||||
|
||||
// sessions
|
||||
|
||||
export type GetSessionsReq = { } // sessions.list
|
||||
export type GetSessionsRes = {
|
||||
current: string
|
||||
sessions: { [hash: string]: Session }
|
||||
}
|
||||
|
||||
export type KillSessionsReq = WithExpire<{ ids: string[] }> // sessions.kill
|
||||
export type KillSessionsRes = WithRevision<null>
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
export type SetEosMarketplaceReq = WithExpire<{ url: string }> // marketplace.eos.set
|
||||
export type SetEosMarketplaceRes = WithRevision<null>
|
||||
|
||||
export type SetPackageMarketplaceReq = WithExpire<{ url: string }> // marketplace.package.set
|
||||
export type SetPackageMarketplaceRes = WithRevision<null>
|
||||
|
||||
// password
|
||||
|
||||
export type UpdatePasswordReq = { password: string } // password.set
|
||||
export type UpdatePasswordRes = null
|
||||
|
||||
// notification
|
||||
|
||||
export type GetNotificationsReq = WithExpire<{ before?: number, limit?: number }> // notification.list
|
||||
export type GetNotificationsRes = WithRevision<ServerNotification<number>[]>
|
||||
|
||||
export type DeleteNotificationReq = { id: number } // notification.delete
|
||||
export type DeleteNotificationRes = null
|
||||
|
||||
export type DeleteAllNotificationsReq = { before: number } // notification.delete-before
|
||||
export type DeleteAllNotificationsRes = null
|
||||
|
||||
// wifi
|
||||
|
||||
export type SetWifiCountryReq = { country: string }
|
||||
export type SetWifiCountryRes = null
|
||||
|
||||
export type GetWifiReq = { }
|
||||
export type GetWifiRes = {
|
||||
ssids: {
|
||||
[ssid: string]: number
|
||||
},
|
||||
connected?: string,
|
||||
country: string,
|
||||
ethernet: boolean,
|
||||
'available-wifi': AvailableWifi[]
|
||||
}
|
||||
|
||||
export type AddWifiReq = { // wifi.add
|
||||
ssid: string
|
||||
password: string
|
||||
priority: number
|
||||
connect: boolean
|
||||
}
|
||||
export type AddWifiRes = null
|
||||
|
||||
export type ConnectWifiReq = { ssid: string } // wifi.connect
|
||||
export type ConnectWifiRes = null
|
||||
|
||||
export type DeleteWifiReq = { ssid: string } // wifi.delete
|
||||
export type DeleteWifiRes = null
|
||||
|
||||
// ssh
|
||||
|
||||
export type GetSSHKeysReq = { } // ssh.list
|
||||
export type GetSSHKeysRes = SSHKey[]
|
||||
|
||||
export type AddSSHKeyReq = { key: string } // ssh.add
|
||||
export type AddSSHKeyRes = SSHKey
|
||||
|
||||
export type DeleteSSHKeyReq = { fingerprint: string } // ssh.delete
|
||||
export type DeleteSSHKeyRes = null
|
||||
|
||||
// backup
|
||||
|
||||
export type GetBackupTargetsReq = { } // backup.target.list
|
||||
export type GetBackupTargetsRes = { [id: string]: BackupTarget }
|
||||
|
||||
export type AddBackupTargetReq = { // backup.target.cifs.add
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string | null
|
||||
}
|
||||
export type AddBackupTargetRes = { [id: string]: CifsBackupTarget }
|
||||
|
||||
export type UpdateBackupTargetReq = AddBackupTargetReq & { id: string } // backup.target.cifs.update
|
||||
export type UpdateBackupTargetRes = AddBackupTargetRes
|
||||
|
||||
export type RemoveBackupTargetReq = { id: string } // backup.target.cifs.remove
|
||||
export type RemoveBackupTargetRes = null
|
||||
|
||||
export type GetBackupInfoReq = { 'target-id': string, password: string } // backup.target.info
|
||||
export type GetBackupInfoRes = BackupInfo
|
||||
|
||||
export type CreateBackupReq = WithExpire<{ // backup.create
|
||||
'target-id': string
|
||||
'old-password': string | null
|
||||
password: string
|
||||
}>
|
||||
export type CreateBackupRes = WithRevision<null>
|
||||
|
||||
// package
|
||||
|
||||
export type GetPackagePropertiesReq = { id: string } // package.properties
|
||||
export type GetPackagePropertiesRes<T extends number> = PackagePropertiesVersioned<T>
|
||||
|
||||
export type LogsRes = { entries: Log[], 'start-cursor'?: string, 'end-cursor'?: string }
|
||||
|
||||
export type GetPackageLogsReq = { id: string, cursor?: string, before_flag?: boolean, limit?: number } // package.logs
|
||||
export type GetPackageLogsRes = LogsRes
|
||||
|
||||
export type GetPackageMetricsReq = { id: string } // package.metrics
|
||||
export type GetPackageMetricsRes = Metric
|
||||
|
||||
export type InstallPackageReq = WithExpire<{ id: string, 'version-spec'?: string }> // package.install
|
||||
export type InstallPackageRes = WithRevision<null>
|
||||
|
||||
export type DryUpdatePackageReq = { id: string, version: string } // package.update.dry
|
||||
export type DryUpdatePackageRes = Breakages
|
||||
|
||||
export type GetPackageConfigReq = { id: string } // package.config.get
|
||||
export type GetPackageConfigRes = { spec: ConfigSpec, config: object }
|
||||
|
||||
export type DrySetPackageConfigReq = { id: string, config: object } // package.config.set.dry
|
||||
export type DrySetPackageConfigRes = Breakages
|
||||
|
||||
export type SetPackageConfigReq = WithExpire<DrySetPackageConfigReq> // package.config.set
|
||||
export type SetPackageConfigRes = WithRevision<null>
|
||||
|
||||
export type RestorePackagesReq = WithExpire<{ // package.backup.restore
|
||||
ids: string[]
|
||||
'target-id': string
|
||||
'old-password': string | null,
|
||||
password: string
|
||||
}>
|
||||
export type RestorePackagesRes = WithRevision<null>
|
||||
|
||||
export type ExecutePackageActionReq = { id: string, 'action-id': string, input?: object } // package.action
|
||||
export type ExecutePackageActionRes = ActionResponse
|
||||
|
||||
export type StartPackageReq = WithExpire<{ id: string }> // package.start
|
||||
export type StartPackageRes = WithRevision<null>
|
||||
|
||||
export type DryStopPackageReq = StopPackageReq // package.stop.dry
|
||||
export type DryStopPackageRes = Breakages
|
||||
|
||||
export type StopPackageReq = WithExpire<{ id: string }> // package.stop
|
||||
export type StopPackageRes = WithRevision<null>
|
||||
|
||||
export type DryUninstallPackageReq = UninstallPackageReq // package.uninstall.dry
|
||||
export type DryUninstallPackageRes = Breakages
|
||||
|
||||
export type UninstallPackageReq = WithExpire<{ id: string }> // package.uninstall
|
||||
export type UninstallPackageRes = WithRevision<null>
|
||||
|
||||
export type DeleteRecoveredPackageReq = { id: string } // package.delete-recovered
|
||||
export type DeleteRecoveredPackageRes = WithRevision<null>
|
||||
|
||||
export type DryConfigureDependencyReq = { 'dependency-id': string, 'dependent-id': string } // package.dependency.configure.dry
|
||||
export type DryConfigureDependencyRes = {
|
||||
'old-config': object
|
||||
'new-config': object
|
||||
spec: ConfigSpec
|
||||
}
|
||||
|
||||
// marketplace
|
||||
|
||||
export type GetMarketplaceDataReq = { }
|
||||
export type GetMarketplaceDataRes = MarketplaceData
|
||||
|
||||
export type GetMarketplaceEOSReq = {
|
||||
'eos-version-compat': string
|
||||
}
|
||||
export type GetMarketplaceEOSRes = MarketplaceEOS
|
||||
|
||||
export type GetMarketplacePackagesReq = {
|
||||
ids?: { id: string, version: string }[]
|
||||
'eos-version-compat': string
|
||||
// iff !ids
|
||||
category?: string
|
||||
query?: string
|
||||
page?: string
|
||||
'per-page'?: string
|
||||
}
|
||||
export type GetMarketplacePackagesRes = MarketplacePkg[]
|
||||
|
||||
export type GetReleaseNotesReq = { id: string }
|
||||
export type GetReleaseNotesRes = { [version: string]: string }
|
||||
|
||||
export type GetLatestVersionReq = { ids: string[] }
|
||||
export type GetLatestVersionRes = { [id: string]: string }
|
||||
|
||||
}
|
||||
|
||||
export type WithExpire<T> = { 'expire-id'?: string } & T
|
||||
export type WithRevision<T> = { response: T, revision?: Revision }
|
||||
|
||||
export interface MarketplaceData {
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
export interface MarketplaceEOS {
|
||||
version: string
|
||||
headline: string
|
||||
'release-notes': { [version: string]: string }
|
||||
}
|
||||
|
||||
export interface MarketplacePkg {
|
||||
icon: URL
|
||||
license: URL
|
||||
instructions: URL
|
||||
manifest: Manifest
|
||||
categories: string[]
|
||||
versions: string[]
|
||||
'dependency-metadata': {
|
||||
[id: string]: {
|
||||
title: string
|
||||
icon: URL
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export interface Breakages {
|
||||
[id: string]: TaggedDependencyError
|
||||
}
|
||||
|
||||
export interface TaggedDependencyError {
|
||||
dependency: string
|
||||
error: DependencyError
|
||||
}
|
||||
|
||||
export interface Log {
|
||||
timestamp: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ActionResponse {
|
||||
message: string
|
||||
value: string | null
|
||||
copyable: boolean
|
||||
qr: boolean
|
||||
}
|
||||
|
||||
export interface Metrics {
|
||||
[key: string]: {
|
||||
[key: string]: {
|
||||
value: string | number | null
|
||||
unit?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Metric {
|
||||
[key: string]: {
|
||||
value: string | number | null
|
||||
unit?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
'last-active': string
|
||||
'user-agent': string
|
||||
metadata: SessionMetadata
|
||||
}
|
||||
|
||||
export interface SessionMetadata {
|
||||
platforms: PlatformType[]
|
||||
}
|
||||
|
||||
export type PlatformType = 'cli' | 'ios' | 'ipad' | 'iphone' | 'android' | 'phablet' | 'tablet' | 'cordova' | 'capacitor' | 'electron' | 'pwa' | 'mobile' | 'mobileweb' | 'desktop' | 'hybrid'
|
||||
|
||||
export type BackupTarget = DiskBackupTarget | CifsBackupTarget
|
||||
|
||||
export interface EmbassyOSRecoveryInfo {
|
||||
version: string
|
||||
full: boolean
|
||||
'password-hash': string | null
|
||||
'wrapped-key': string | null
|
||||
}
|
||||
|
||||
export interface DiskBackupTarget {
|
||||
type: 'disk'
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
logicalname: string | null
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null
|
||||
}
|
||||
|
||||
export interface CifsBackupTarget {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
mountable: boolean
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null
|
||||
}
|
||||
|
||||
export type RecoverySource = DiskRecoverySource | CifsRecoverySource
|
||||
|
||||
export interface DiskRecoverySource {
|
||||
type: 'disk'
|
||||
logicalname: string // partition logicalname
|
||||
}
|
||||
|
||||
export interface CifsRecoverySource {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface DiskInfo {
|
||||
logicalname: string
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
partitions: PartitionInfo[]
|
||||
capacity: number
|
||||
guid: string | null
|
||||
}
|
||||
|
||||
export interface PartitionInfo {
|
||||
logicalname: string
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
'embassy-os': EmbassyOsDiskInfo | null
|
||||
}
|
||||
|
||||
export interface EmbassyOsDiskInfo {
|
||||
version: string
|
||||
full: boolean
|
||||
}
|
||||
|
||||
export interface BackupInfo {
|
||||
version: string,
|
||||
timestamp: string,
|
||||
'package-backups': {
|
||||
[id: string]: PackageBackupInfo
|
||||
}
|
||||
}
|
||||
|
||||
export interface PackageBackupInfo {
|
||||
title: string
|
||||
version: string
|
||||
'os-version': string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface ServerSpecs {
|
||||
[key: string]: string | number
|
||||
}
|
||||
|
||||
export interface SSHKey {
|
||||
'created-at': string
|
||||
alg: string
|
||||
hostname: string
|
||||
fingerprint: string
|
||||
}
|
||||
|
||||
export type ServerNotifications = ServerNotification<any>[]
|
||||
|
||||
export interface ServerNotification<T extends number> {
|
||||
id: number
|
||||
'package-id': string | null
|
||||
'created-at': string
|
||||
code: T
|
||||
level: NotificationLevel
|
||||
title: string
|
||||
message: string
|
||||
data: NotificationData<T>
|
||||
}
|
||||
|
||||
export enum NotificationLevel {
|
||||
Success = 'success',
|
||||
Info = 'info',
|
||||
Warning = 'warning',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export type NotificationData<T> = T extends 0 ? null :
|
||||
T extends 1 ? BackupReport :
|
||||
any
|
||||
|
||||
export interface BackupReport {
|
||||
server: {
|
||||
attempted: boolean
|
||||
error: string | null
|
||||
}
|
||||
packages: {
|
||||
[id: string]: {
|
||||
error: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AvailableWifi {
|
||||
ssid: string
|
||||
strength: number
|
||||
security: string []
|
||||
}
|
||||
219
frontend/projects/ui/src/app/services/api/embassy-api.service.ts
Normal file
219
frontend/projects/ui/src/app/services/api/embassy-api.service.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { Http, Update, Operation, Revision, Source, Store, RPCResponse } from 'patch-db-client'
|
||||
import { RR } from './api.types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { RequestError } from '../http.service'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
protected readonly sync$ = new Subject<Update<DataModel>>()
|
||||
|
||||
/** PatchDb Source interface. Post/Patch requests provide a source of patches to the db. */
|
||||
// sequenceStream '_' is not used by the live api, but is overridden by the mock
|
||||
watch$ (_?: Store<DataModel>): Observable<RPCResponse<Update<DataModel>>> {
|
||||
return this.sync$.asObservable().pipe(map( result => ({ result,
|
||||
jsonrpc: '2.0'})))
|
||||
}
|
||||
|
||||
// for getting static files: ex icons, instructions, licenses
|
||||
abstract getStatic (url: string): Promise<string>
|
||||
|
||||
// db
|
||||
|
||||
abstract getRevisions (since: number): Promise<RR.GetRevisionsRes>
|
||||
|
||||
abstract getDump (): Promise<RR.GetDumpRes>
|
||||
|
||||
protected abstract setDbValueRaw (params: RR.SetDBValueReq): Promise<RR.SetDBValueRes>
|
||||
setDbValue = (params: RR.SetDBValueReq) => this.syncResponse(
|
||||
() => this.setDbValueRaw(params),
|
||||
)()
|
||||
|
||||
// auth
|
||||
|
||||
abstract login (params: RR.LoginReq): Promise<RR.loginRes>
|
||||
|
||||
abstract logout (params: RR.LogoutReq): Promise<RR.LogoutRes>
|
||||
|
||||
abstract getSessions (params: RR.GetSessionsReq): Promise<RR.GetSessionsRes>
|
||||
|
||||
abstract killSessions (params: RR.KillSessionsReq): Promise<RR.KillSessionsRes>
|
||||
|
||||
// server
|
||||
|
||||
protected abstract setShareStatsRaw (params: RR.SetShareStatsReq): Promise<RR.SetShareStatsRes>
|
||||
setShareStats = (params: RR.SetShareStatsReq) => this.syncResponse(
|
||||
() => this.setShareStatsRaw(params),
|
||||
)()
|
||||
|
||||
abstract getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes>
|
||||
|
||||
abstract getServerMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetServerMetricsRes>
|
||||
|
||||
abstract getPkgMetrics (params: RR.GetPackageMetricsReq): Promise<RR.GetPackageMetricsRes>
|
||||
|
||||
protected abstract updateServerRaw (params: RR.UpdateServerReq): Promise<RR.UpdateServerRes>
|
||||
updateServer = (params: RR.UpdateServerReq) => this.syncResponse(
|
||||
() => this.updateServerWrapper(params),
|
||||
)()
|
||||
async updateServerWrapper (params: RR.UpdateServerReq) {
|
||||
const res = await this.updateServerRaw(params)
|
||||
if (res.response === 'no-updates') {
|
||||
throw new Error('Could ont find a newer version of EmbassyOS')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
abstract restartServer (params: RR.UpdateServerReq): Promise<RR.RestartServerRes>
|
||||
|
||||
abstract shutdownServer (params: RR.ShutdownServerReq): Promise<RR.ShutdownServerRes>
|
||||
|
||||
abstract systemRebuild (params: RR.SystemRebuildReq): Promise<RR.SystemRebuildRes>
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
abstract getEos (params: RR.GetMarketplaceEOSReq): Promise<RR.GetMarketplaceEOSRes>
|
||||
|
||||
abstract getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise<RR.GetMarketplaceDataRes>
|
||||
|
||||
abstract getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise<RR.GetMarketplacePackagesRes>
|
||||
|
||||
abstract getReleaseNotes (params: RR.GetReleaseNotesReq): Promise<RR.GetReleaseNotesRes>
|
||||
|
||||
abstract getLatestVersion (params: RR.GetLatestVersionReq): Promise<RR.GetLatestVersionRes>
|
||||
|
||||
// protected abstract setPackageMarketplaceRaw (params: RR.SetPackageMarketplaceReq): Promise<RR.SetPackageMarketplaceRes>
|
||||
// setPackageMarketplace = (params: RR.SetPackageMarketplaceReq) => this.syncResponse(
|
||||
// () => this.setPackageMarketplaceRaw(params),
|
||||
// )()
|
||||
|
||||
// password
|
||||
// abstract updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes>
|
||||
|
||||
// notification
|
||||
|
||||
abstract getNotificationsRaw (params: RR.GetNotificationsReq): Promise<RR.GetNotificationsRes>
|
||||
getNotifications = (params: RR.GetNotificationsReq) => this.syncResponse<RR.GetNotificationsRes['response'], any>(
|
||||
() => this.getNotificationsRaw(params),
|
||||
)()
|
||||
|
||||
abstract deleteNotification (params: RR.DeleteNotificationReq): Promise<RR.DeleteNotificationRes>
|
||||
|
||||
abstract deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise<RR.DeleteAllNotificationsRes>
|
||||
|
||||
// wifi
|
||||
|
||||
abstract getWifi (params: RR.GetWifiReq, timeout: number): Promise<RR.GetWifiRes>
|
||||
|
||||
abstract setWifiCountry (params: RR.SetWifiCountryReq): Promise<RR.SetWifiCountryRes>
|
||||
|
||||
abstract addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes>
|
||||
|
||||
abstract connectWifi (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes>
|
||||
|
||||
abstract deleteWifi (params: RR.DeleteWifiReq): Promise<RR.ConnectWifiRes>
|
||||
|
||||
// ssh
|
||||
|
||||
abstract getSshKeys (params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes>
|
||||
|
||||
abstract addSshKey (params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes>
|
||||
|
||||
abstract deleteSshKey (params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes>
|
||||
|
||||
// backup
|
||||
|
||||
abstract getBackupTargets (params: RR.GetBackupTargetsReq): Promise<RR.GetBackupTargetsRes>
|
||||
|
||||
abstract addBackupTarget (params: RR.AddBackupTargetReq): Promise<RR.AddBackupTargetRes>
|
||||
|
||||
abstract updateBackupTarget (params: RR.UpdateBackupTargetReq): Promise<RR.UpdateBackupTargetRes>
|
||||
|
||||
abstract removeBackupTarget (params: RR.RemoveBackupTargetReq): Promise<RR.RemoveBackupTargetRes>
|
||||
|
||||
abstract getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes>
|
||||
|
||||
protected abstract createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes>
|
||||
createBackup = (params: RR.CreateBackupReq) => this.syncResponse(
|
||||
() => this.createBackupRaw(params),
|
||||
)()
|
||||
|
||||
// package
|
||||
|
||||
abstract getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes<2>['data']>
|
||||
|
||||
abstract getPackageLogs (params: RR.GetPackageLogsReq): Promise<RR.GetPackageLogsRes>
|
||||
|
||||
protected abstract installPackageRaw (params: RR.InstallPackageReq): Promise<RR.InstallPackageRes>
|
||||
installPackage = (params: RR.InstallPackageReq) => this.syncResponse(
|
||||
() => this.installPackageRaw(params),
|
||||
)()
|
||||
|
||||
abstract dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise<RR.DryUpdatePackageRes>
|
||||
|
||||
abstract getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes>
|
||||
|
||||
abstract drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise<RR.DrySetPackageConfigRes>
|
||||
|
||||
protected abstract setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise<RR.SetPackageConfigRes>
|
||||
setPackageConfig = (params: RR.SetPackageConfigReq) => this.syncResponse(
|
||||
() => this.setPackageConfigRaw(params),
|
||||
)()
|
||||
|
||||
protected abstract restorePackagesRaw (params: RR.RestorePackagesReq): Promise<RR.RestorePackagesRes>
|
||||
restorePackages = (params: RR.RestorePackagesReq) => this.syncResponse(
|
||||
() => this.restorePackagesRaw(params),
|
||||
)()
|
||||
|
||||
abstract executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes>
|
||||
|
||||
protected abstract startPackageRaw (params: RR.StartPackageReq): Promise<RR.StartPackageRes>
|
||||
startPackage = (params: RR.StartPackageReq) => this.syncResponse(
|
||||
() => this.startPackageRaw(params),
|
||||
)()
|
||||
|
||||
abstract dryStopPackage (params: RR.DryStopPackageReq): Promise<RR.DryStopPackageRes>
|
||||
|
||||
protected abstract stopPackageRaw (params: RR.StopPackageReq): Promise<RR.StopPackageRes>
|
||||
stopPackage = (params: RR.StopPackageReq) => this.syncResponse(
|
||||
() => this.stopPackageRaw(params),
|
||||
)()
|
||||
|
||||
abstract dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise<RR.DryUninstallPackageRes>
|
||||
|
||||
protected abstract uninstallPackageRaw (params: RR.UninstallPackageReq): Promise<RR.UninstallPackageRes>
|
||||
uninstallPackage = (params: RR.UninstallPackageReq) => this.syncResponse(
|
||||
() => this.uninstallPackageRaw(params),
|
||||
)()
|
||||
|
||||
abstract dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes>
|
||||
|
||||
protected abstract deleteRecoveredPackageRaw (params: RR.UninstallPackageReq): Promise<RR.UninstallPackageRes>
|
||||
deleteRecoveredPackage = (params: RR.UninstallPackageReq) => this.syncResponse(
|
||||
() => this.deleteRecoveredPackageRaw(params),
|
||||
)()
|
||||
|
||||
|
||||
// Helper allowing quick decoration to sync the response patch and return the response contents.
|
||||
// Pass in a tempUpdate function which returns a UpdateTemp corresponding to a temporary
|
||||
// state change you'd like to enact prior to request and expired when request terminates.
|
||||
private syncResponse<T, F extends (...args: any[]) => Promise<{ response: T, revision?: Revision }>> (f: F, temp?: Operation): (...args: Parameters<F>) => Promise<T> {
|
||||
return (...a) => {
|
||||
// let expireId = undefined
|
||||
// if (temp) {
|
||||
// expireId = uuid.v4()
|
||||
// this.sync.next({ patch: [temp], expiredBy: expireId })
|
||||
// }
|
||||
|
||||
return f(a)
|
||||
.catch((e: RequestError) => {
|
||||
if (e.revision) this.sync$.next(e.revision)
|
||||
throw e
|
||||
})
|
||||
.then(({ response, revision }) => {
|
||||
if (revision) this.sync$.next(revision)
|
||||
return response
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpService, Method } from '../http.service'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
|
||||
constructor (
|
||||
private readonly http: HttpService,
|
||||
) {
|
||||
super();
|
||||
(window as any).rpcClient = this
|
||||
}
|
||||
|
||||
async getStatic (url: string): Promise<string> {
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
url,
|
||||
responseType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
// db
|
||||
|
||||
async getRevisions (since: number): Promise<RR.GetRevisionsRes> {
|
||||
return this.http.rpcRequest({ method: 'db.revisions', params: { since } })
|
||||
}
|
||||
|
||||
async getDump (): Promise<RR.GetDumpRes> {
|
||||
return this.http.rpcRequest({ method: 'db.dump' })
|
||||
}
|
||||
|
||||
async setDbValueRaw (params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
|
||||
return this.http.rpcRequest({ method: 'db.put.ui', params })
|
||||
}
|
||||
|
||||
// auth
|
||||
|
||||
async login (params: RR.LoginReq): Promise<RR.loginRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.login', params })
|
||||
}
|
||||
|
||||
async logout (params: RR.LogoutReq): Promise<RR.LogoutRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.logout', params })
|
||||
}
|
||||
|
||||
async getSessions (params: RR.GetSessionsReq): Promise<RR.GetSessionsRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.session.list', params })
|
||||
}
|
||||
|
||||
async killSessions (params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.session.kill', params })
|
||||
}
|
||||
|
||||
// server
|
||||
|
||||
async setShareStatsRaw (params: RR.SetShareStatsReq): Promise<RR.SetShareStatsRes> {
|
||||
return this.http.rpcRequest( { method: 'server.config.share-stats', params })
|
||||
}
|
||||
|
||||
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
||||
return this.http.rpcRequest( { method: 'server.logs', params })
|
||||
}
|
||||
|
||||
async getServerMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetServerMetricsRes> {
|
||||
return this.http.rpcRequest({ method: 'server.metrics', params })
|
||||
}
|
||||
|
||||
async updateServerRaw (params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.update', params })
|
||||
}
|
||||
|
||||
async restartServer (params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.restart', params })
|
||||
}
|
||||
|
||||
async shutdownServer (params: RR.ShutdownServerReq): Promise<RR.ShutdownServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.shutdown', params })
|
||||
}
|
||||
|
||||
async systemRebuild (params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.rebuild', params })
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async getEos (params: RR.GetMarketplaceEOSReq): Promise<RR.GetMarketplaceEOSRes> {
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
url: '/marketplace/eos/latest',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise<RR.GetMarketplaceDataRes> {
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
url: '/marketplace/package/data',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise<RR.GetMarketplacePackagesRes> {
|
||||
if (params.query) params.category = undefined
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
url: '/marketplace/package/index',
|
||||
params: {
|
||||
...params,
|
||||
ids: JSON.stringify(params.ids),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async getReleaseNotes (params: RR.GetReleaseNotesReq): Promise<RR.GetReleaseNotesRes> {
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
url: '/marketplace/package/release-notes',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async getLatestVersion (params: RR.GetLatestVersionReq): Promise<RR.GetLatestVersionRes> {
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
url: '/marketplace/latest-version',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
// async setPackageMarketplaceRaw (params: RR.SetPackageMarketplaceReq): Promise<RR.SetPackageMarketplaceRes> {
|
||||
// return this.http.rpcRequest({ method: 'marketplace.package.set', params })
|
||||
// }
|
||||
|
||||
// password
|
||||
// async updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes> {
|
||||
// return this.http.rpcRequest({ method: 'password.set', params })
|
||||
// }
|
||||
|
||||
// notification
|
||||
|
||||
async getNotificationsRaw (params: RR.GetNotificationsReq): Promise<RR.GetNotificationsRes> {
|
||||
return this.http.rpcRequest({ method: 'notification.list', params })
|
||||
}
|
||||
|
||||
async deleteNotification (params: RR.DeleteNotificationReq): Promise<RR.DeleteNotificationRes> {
|
||||
return this.http.rpcRequest({ method: 'notification.delete', params })
|
||||
}
|
||||
|
||||
async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise<RR.DeleteAllNotificationsRes> {
|
||||
return this.http.rpcRequest({ method: 'notification.delete-before', params })
|
||||
}
|
||||
|
||||
// wifi
|
||||
|
||||
async getWifi (params: RR.GetWifiReq, timeout?: number): Promise<RR.GetWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.get', params, timeout })
|
||||
}
|
||||
|
||||
async setWifiCountry (params: RR.SetWifiCountryReq): Promise<RR.SetWifiCountryRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.country.set', params })
|
||||
}
|
||||
|
||||
async addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.add', params })
|
||||
}
|
||||
|
||||
async connectWifi (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.connect', params })
|
||||
}
|
||||
|
||||
async deleteWifi (params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.delete', params })
|
||||
}
|
||||
|
||||
// ssh
|
||||
|
||||
async getSshKeys (params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.list', params })
|
||||
}
|
||||
|
||||
async addSshKey (params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.add', params })
|
||||
}
|
||||
|
||||
async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.delete', params })
|
||||
}
|
||||
|
||||
// backup
|
||||
|
||||
async getBackupTargets (params: RR.GetBackupTargetsReq): Promise<RR.GetBackupTargetsRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.list', params })
|
||||
}
|
||||
|
||||
async addBackupTarget (params: RR.AddBackupTargetReq): Promise<RR.AddBackupTargetRes> {
|
||||
params.path = params.path.replace('/\\/g', '/')
|
||||
return this.http.rpcRequest({ method: 'backup.target.cifs.add', params })
|
||||
}
|
||||
|
||||
async updateBackupTarget (params: RR.UpdateBackupTargetReq): Promise<RR.UpdateBackupTargetRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.cifs.update', params })
|
||||
}
|
||||
|
||||
async removeBackupTarget (params: RR.RemoveBackupTargetReq): Promise<RR.RemoveBackupTargetRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.cifs.remove', params })
|
||||
}
|
||||
|
||||
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.info', params })
|
||||
}
|
||||
|
||||
async createBackupRaw (params: RR.CreateBackupReq): Promise <RR.CreateBackupRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.create', params })
|
||||
}
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes < 2 > ['data'] > {
|
||||
return this.http.rpcRequest({ method: 'package.properties', params })
|
||||
.then(parsePropertiesPermissive)
|
||||
}
|
||||
|
||||
async getPackageLogs (params: RR.GetPackageLogsReq): Promise<RR.GetPackageLogsRes> {
|
||||
return this.http.rpcRequest( { method: 'package.logs', params })
|
||||
}
|
||||
|
||||
async getPkgMetrics (params: RR.GetPackageMetricsReq): Promise<RR.GetPackageMetricsRes> {
|
||||
return this.http.rpcRequest({ method: 'package.metrics', params })
|
||||
}
|
||||
|
||||
async installPackageRaw (params: RR.InstallPackageReq): Promise<RR.InstallPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.install', params })
|
||||
}
|
||||
|
||||
async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise<RR.DryUpdatePackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.update.dry', params })
|
||||
}
|
||||
|
||||
async getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.get', params })
|
||||
}
|
||||
|
||||
async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise<RR.DrySetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.set.dry', params })
|
||||
}
|
||||
|
||||
async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise<RR.SetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.set', params })
|
||||
}
|
||||
|
||||
async restorePackagesRaw (params: RR.RestorePackagesReq): Promise<RR.RestorePackagesRes> {
|
||||
return this.http.rpcRequest({ method: 'package.backup.restore', params })
|
||||
}
|
||||
|
||||
async executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes> {
|
||||
return this.http.rpcRequest({ method: 'package.action', params })
|
||||
}
|
||||
|
||||
async startPackageRaw (params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.start', params })
|
||||
}
|
||||
|
||||
async dryStopPackage (params: RR.DryStopPackageReq): Promise<RR.DryStopPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.stop.dry', params })
|
||||
}
|
||||
|
||||
async stopPackageRaw (params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.stop', params })
|
||||
}
|
||||
|
||||
async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise<RR.DryUninstallPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.uninstall.dry', params })
|
||||
}
|
||||
|
||||
async deleteRecoveredPackageRaw (params: RR.DeleteRecoveredPackageReq): Promise<RR.DeleteRecoveredPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.delete-recovered', params })
|
||||
}
|
||||
|
||||
async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise<RR.UninstallPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.uninstall', params })
|
||||
}
|
||||
|
||||
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes> {
|
||||
return this.http.rpcRequest({ method: 'package.dependency.configure.dry', params })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,789 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { pauseFor } from '../../util/misc.util'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client'
|
||||
import { DataModel, DependencyErrorType, InstallProgress, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { CifsBackupTarget, Log, RR, WithRevision } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { Mock } from './api.fixures'
|
||||
import markdown from 'raw-loader!../../../../../../assets/markdown/md-sample.md'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService extends ApiService {
|
||||
readonly mockPatch$ = new BehaviorSubject<Update<DataModel>>(undefined)
|
||||
private readonly revertTime = 4000
|
||||
sequence: number
|
||||
|
||||
constructor (
|
||||
private readonly bootstrapper: LocalStorageBootstrap,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getStatic (url: string): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
return markdown
|
||||
}
|
||||
|
||||
// db
|
||||
|
||||
async getRevisions (since: number): Promise<RR.GetRevisionsRes> {
|
||||
return this.getDump()
|
||||
}
|
||||
|
||||
async getDump (): Promise<RR.GetDumpRes> {
|
||||
const cache = await this.bootstrapper.init()
|
||||
return {
|
||||
id: cache.sequence,
|
||||
value: cache.data,
|
||||
expireId: null,
|
||||
}
|
||||
}
|
||||
|
||||
async setDbValueRaw (params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/ui' + params.pointer,
|
||||
value: params.value,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
// auth
|
||||
|
||||
async login (params: RR.LoginReq): Promise<RR.loginRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(() => {
|
||||
this.mockPatch$.next({ id: 1, value: mockPatchData, expireId: null })
|
||||
}, 2000)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async logout (params: RR.LogoutReq): Promise<RR.LogoutRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async getSessions (params: RR.GetSessionsReq): Promise<RR.GetSessionsRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.Sessions
|
||||
}
|
||||
|
||||
async killSessions (params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// server
|
||||
|
||||
async setShareStatsRaw (params: RR.SetShareStatsReq): Promise<RR.SetShareStatsRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/share-stats',
|
||||
value: params.value,
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
||||
await pauseFor(2000)
|
||||
let entries: Log[]
|
||||
if (Math.random() < .2) {
|
||||
entries = Mock.ServerLogs
|
||||
} else {
|
||||
const arrLength = params.limit ? Math.ceil(params.limit / Mock.ServerLogs.length) : 10
|
||||
entries = new Array(arrLength).fill(Mock.ServerLogs).reduce((acc, val) => acc.concat(val), [])
|
||||
}
|
||||
return {
|
||||
entries,
|
||||
'start-cursor': 'startCursor',
|
||||
'end-cursor': 'endCursor',
|
||||
}
|
||||
}
|
||||
|
||||
async getServerMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetServerMetricsRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.getServerMetrics()
|
||||
}
|
||||
|
||||
async getPkgMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetPackageMetricsRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.getAppMetrics()
|
||||
}
|
||||
|
||||
async updateServerRaw (params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
|
||||
await pauseFor(2000)
|
||||
const initialProgress = {
|
||||
size: 10000,
|
||||
downloaded: 0,
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateOSProgress(initialProgress.size)
|
||||
}, 500)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/update-progress',
|
||||
value: initialProgress,
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch, 'updating')
|
||||
}
|
||||
|
||||
async restartServer (params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async shutdownServer (params: RR.ShutdownServerReq): Promise<RR.ShutdownServerRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async systemRebuild (params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async getEos (params: RR.GetMarketplaceEOSReq): Promise<RR.GetMarketplaceEOSRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.MarketplaceEos
|
||||
}
|
||||
|
||||
async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise<RR.GetMarketplaceDataRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
categories: ['featured', 'bitcoin', 'lightning', 'data', 'messaging', 'social', 'alt coin'],
|
||||
}
|
||||
}
|
||||
|
||||
async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise<RR.GetMarketplacePackagesRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.MarketplacePkgsList
|
||||
}
|
||||
|
||||
async getReleaseNotes (params: RR.GetReleaseNotesReq): Promise<RR.GetReleaseNotesRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.ReleaseNotes
|
||||
}
|
||||
|
||||
async getLatestVersion (params: RR.GetLatestVersionReq): Promise<RR.GetLatestVersionRes> {
|
||||
await pauseFor(2000)
|
||||
return params.ids.reduce((obj, id) => {
|
||||
obj[id] = '1.3.0'
|
||||
return obj
|
||||
}, { })
|
||||
}
|
||||
|
||||
// async setPackageMarketplaceRaw (params: RR.SetPackageMarketplaceReq): Promise<RR.SetPackageMarketplaceRes> {
|
||||
// await pauseFor(2000)
|
||||
// const patch = [
|
||||
// {
|
||||
// op: PatchOp.REPLACE,
|
||||
// path: '/server-info/package-marketplace',
|
||||
// value: params.url,
|
||||
// },
|
||||
// ]
|
||||
// return this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
// }
|
||||
|
||||
// password
|
||||
// async updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes> {
|
||||
// await pauseFor(2000)
|
||||
// return null
|
||||
// }
|
||||
|
||||
// notification
|
||||
|
||||
async getNotificationsRaw (params: RR.GetNotificationsReq): Promise<RR.GetNotificationsRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/unread-notification-count',
|
||||
value: 0,
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch, Mock.Notifications)
|
||||
}
|
||||
|
||||
async deleteNotification (params: RR.DeleteNotificationReq): Promise<RR.DeleteNotificationRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise<RR.DeleteAllNotificationsRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// wifi
|
||||
|
||||
async getWifi (params: RR.GetWifiReq): Promise < RR.GetWifiRes > {
|
||||
await pauseFor(2000)
|
||||
return Mock.Wifi
|
||||
}
|
||||
|
||||
async setWifiCountry (params: RR.SetWifiCountryReq): Promise <RR.SetWifiCountryRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async connectWifi (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async deleteWifi (params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// ssh
|
||||
|
||||
async getSshKeys (params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.SshKeys
|
||||
}
|
||||
|
||||
async addSshKey (params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.SshKey
|
||||
}
|
||||
|
||||
async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// backup
|
||||
|
||||
async getBackupTargets (params: RR.GetBackupTargetsReq): Promise<RR.GetBackupTargetsRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.BackupTargets
|
||||
}
|
||||
|
||||
async addBackupTarget (params: RR.AddBackupTargetReq): Promise<RR.AddBackupTargetRes> {
|
||||
await pauseFor(2000)
|
||||
const { hostname, path, username } = params
|
||||
return {
|
||||
'latfgvwdbhjsndmk': {
|
||||
type: 'cifs',
|
||||
hostname,
|
||||
path: path.replace(/\\/g, '/'),
|
||||
username,
|
||||
mountable: true,
|
||||
'embassy-os': null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async updateBackupTarget (params: RR.UpdateBackupTargetReq): Promise<RR.UpdateBackupTargetRes> {
|
||||
await pauseFor(2000)
|
||||
const { id, hostname, path, username } = params
|
||||
return {
|
||||
[id]: {
|
||||
...Mock.BackupTargets[id] as CifsBackupTarget,
|
||||
hostname,
|
||||
path,
|
||||
username,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async removeBackupTarget (params: RR.RemoveBackupTargetReq): Promise<RR.RemoveBackupTargetRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.BackupInfo
|
||||
}
|
||||
|
||||
async createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
|
||||
await pauseFor(2000)
|
||||
const path = '/server-info/status'
|
||||
const ids = ['bitcoind', 'lnd']
|
||||
|
||||
setTimeout(async () => {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const appPath = `/package-data/${ids[i]}/installed/status/main/status`
|
||||
const appPatch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: appPath,
|
||||
value: PackageMainStatus.BackingUp,
|
||||
},
|
||||
]
|
||||
this.updateMock(appPatch)
|
||||
|
||||
await pauseFor(8000)
|
||||
|
||||
appPatch[0].value = PackageMainStatus.Stopped
|
||||
this.updateMock(appPatch)
|
||||
}
|
||||
|
||||
await pauseFor(1000)
|
||||
|
||||
// set server back to running
|
||||
const lastPatch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path,
|
||||
value: ServerStatus.Running,
|
||||
},
|
||||
]
|
||||
this.updateMock(lastPatch)
|
||||
}, 500)
|
||||
|
||||
const originalPatch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path,
|
||||
value: ServerStatus.BackingUp,
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(originalPatch)
|
||||
}
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes<2>['data']> {
|
||||
await pauseFor(2000)
|
||||
return parsePropertiesPermissive(Mock.PackageProperties)
|
||||
}
|
||||
|
||||
async getPackageLogs (params: RR.GetPackageLogsReq): Promise<RR.GetPackageLogsRes> {
|
||||
await pauseFor(2000)
|
||||
let entries
|
||||
if (Math.random() < .2) {
|
||||
entries = Mock.PackageLogs
|
||||
} else {
|
||||
const arrLength = params.limit ? Math.ceil(params.limit / Mock.PackageLogs.length) : 10
|
||||
entries = new Array(arrLength).fill(Mock.PackageLogs).reduce((acc, val) => acc.concat(val), [])
|
||||
}
|
||||
return {
|
||||
entries,
|
||||
'start-cursor': 'startCursor',
|
||||
'end-cursor': 'endCursor',
|
||||
}
|
||||
}
|
||||
|
||||
async installPackageRaw (params: RR.InstallPackageReq): Promise<RR.InstallPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const initialProgress: InstallProgress = {
|
||||
size: 120,
|
||||
downloaded: 0,
|
||||
'download-complete': false,
|
||||
validated: 0,
|
||||
'validation-complete': false,
|
||||
unpacked: 0,
|
||||
'unpack-complete': false,
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(params.id, initialProgress)
|
||||
}, 1000)
|
||||
|
||||
const pkg: PackageDataEntry = {
|
||||
...Mock.LocalPkgs[params.id],
|
||||
state: PackageState.Installing,
|
||||
'install-progress': initialProgress,
|
||||
installed: undefined,
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/package-data/${params.id}`,
|
||||
value: pkg,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise<RR.DryUpdatePackageRes> {
|
||||
await pauseFor(2000)
|
||||
return { }
|
||||
}
|
||||
|
||||
async getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
config: Mock.MockConfig,
|
||||
spec: Mock.ConfigSpec,
|
||||
}
|
||||
}
|
||||
|
||||
async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise<RR.DrySetPackageConfigRes> {
|
||||
await pauseFor(2000)
|
||||
return { }
|
||||
}
|
||||
|
||||
async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise<RR.SetPackageConfigRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${params.id}/installed/status/configured`,
|
||||
value: true,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
async restorePackagesRaw (params: RR.RestorePackagesReq): Promise<RR.RestorePackagesRes> {
|
||||
await pauseFor(2000)
|
||||
const patch: Operation[] = params.ids.map(id => {
|
||||
|
||||
const initialProgress: InstallProgress = {
|
||||
size: 120,
|
||||
downloaded: 120,
|
||||
'download-complete': true,
|
||||
validated: 0,
|
||||
'validation-complete': false,
|
||||
unpacked: 0,
|
||||
'unpack-complete': false,
|
||||
}
|
||||
|
||||
const pkg: PackageDataEntry = {
|
||||
...Mock.LocalPkgs[id],
|
||||
state: PackageState.Restoring,
|
||||
'install-progress': initialProgress,
|
||||
installed: undefined,
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(id, initialProgress)
|
||||
}, 2000)
|
||||
|
||||
return {
|
||||
op: PatchOp.ADD,
|
||||
path: `/package-data/${id}`,
|
||||
value: pkg,
|
||||
}
|
||||
})
|
||||
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
async executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.ActionResponse
|
||||
}
|
||||
|
||||
async startPackageRaw (params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
||||
const path = `/package-data/${params.id}/installed/status/main`
|
||||
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: PackageMainStatus.Running,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/started',
|
||||
value: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
this.updateMock(patch2)
|
||||
|
||||
const patch3 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/health',
|
||||
value: {
|
||||
'ephemeral-health-check': {
|
||||
result: 'starting',
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
result: 'disabled',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.updateMock(patch3)
|
||||
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch4 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/health',
|
||||
value: {
|
||||
'ephemeral-health-check': {
|
||||
result: 'starting',
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
result: 'disabled',
|
||||
},
|
||||
'chain-state': {
|
||||
result: 'loading',
|
||||
message: 'Bitcoin is syncing from genesis',
|
||||
},
|
||||
'p2p-interface': {
|
||||
result: 'success',
|
||||
},
|
||||
'rpc-interface': {
|
||||
result: 'failure',
|
||||
error: 'RPC interface unreachable.',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.updateMock(patch4)
|
||||
}, 2000)
|
||||
|
||||
const originalPatch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: PackageMainStatus.Starting,
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(originalPatch)
|
||||
}
|
||||
|
||||
async dryStopPackage (params: RR.DryStopPackageReq): Promise<RR.DryStopPackageRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
'lnd': {
|
||||
dependency: 'bitcoind',
|
||||
error: {
|
||||
type: DependencyErrorType.NotRunning,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async stopPackageRaw (params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const path = `/package-data/${params.id}/installed/status/main`
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: PackageMainStatus.Stopped,
|
||||
},
|
||||
]
|
||||
this.updateMock(patch2)
|
||||
}, this.revertTime)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: PackageMainStatus.Stopping,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/health',
|
||||
value: { },
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise<RR.DryUninstallPackageRes> {
|
||||
await pauseFor(2000)
|
||||
return { }
|
||||
}
|
||||
|
||||
async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise<RR.UninstallPackageRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/package-data/${params.id}`,
|
||||
} as RemoveOperation,
|
||||
]
|
||||
this.updateMock(patch2)
|
||||
}, this.revertTime)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${params.id}/state`,
|
||||
value: PackageState.Removing,
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
async deleteRecoveredPackageRaw (params: RR.DeleteRecoveredPackageReq): Promise<RR.DeleteRecoveredPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/recovered-packages/${params.id}`,
|
||||
} as RemoveOperation,
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
'old-config': Mock.MockConfig,
|
||||
'new-config': Mock.MockDependencyConfig,
|
||||
spec: Mock.ConfigSpec,
|
||||
}
|
||||
}
|
||||
|
||||
private async updateProgress (id: string, initialProgress: InstallProgress): Promise<void> {
|
||||
const phases = [
|
||||
{ progress: 'downloaded', completion: 'download-complete'},
|
||||
{ progress: 'validated', completion: 'validation-complete'},
|
||||
{ progress: 'unpacked', completion: 'unpack-complete'},
|
||||
]
|
||||
for (let phase of phases) {
|
||||
let i = initialProgress[phase.progress]
|
||||
while (i < initialProgress.size) {
|
||||
await pauseFor(250)
|
||||
i = Math.min(i + 5, initialProgress.size)
|
||||
initialProgress[phase.progress] = i
|
||||
if (i === initialProgress.size) {
|
||||
initialProgress[phase.completion] = true
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${id}/install-progress`,
|
||||
value: initialProgress,
|
||||
},
|
||||
]
|
||||
this.updateMock(patch)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2: any = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${id}`,
|
||||
value: { ...Mock.LocalPkgs[id] },
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/package-data/${id}/install-progress`,
|
||||
},
|
||||
]
|
||||
this.updateMock(patch2)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
private async updateOSProgress (size: number) {
|
||||
let downloaded = 0
|
||||
while (downloaded < size) {
|
||||
await pauseFor(250)
|
||||
downloaded += 500
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/server-info/update-progress/downloaded`,
|
||||
value: downloaded,
|
||||
},
|
||||
]
|
||||
this.updateMock(patch)
|
||||
}
|
||||
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/server-info/update-progress/downloaded`,
|
||||
value: size,
|
||||
},
|
||||
]
|
||||
this.updateMock(patch2)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch3: Operation[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status',
|
||||
value: ServerStatus.Updated,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: '/server-info/update-progress',
|
||||
},
|
||||
]
|
||||
this.updateMock(patch3)
|
||||
// quickly revert server to "running" for continued testing
|
||||
await pauseFor(100)
|
||||
const patch4 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status',
|
||||
value: ServerStatus.Running,
|
||||
},
|
||||
]
|
||||
this.updateMock(patch4)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
private async updateMock (patch: Operation[]): Promise<void> {
|
||||
if (!this.sequence) {
|
||||
const { sequence } = await this.bootstrapper.init()
|
||||
this.sequence = sequence
|
||||
}
|
||||
const revision = {
|
||||
id: ++this.sequence,
|
||||
patch,
|
||||
expireId: null,
|
||||
}
|
||||
this.mockPatch$.next(revision)
|
||||
}
|
||||
|
||||
private async withRevision<T> (patch: Operation[], response: T = null): Promise<WithRevision<T>> {
|
||||
if (!this.sequence) {
|
||||
const { sequence } = await this.bootstrapper.init()
|
||||
this.sequence = sequence
|
||||
}
|
||||
|
||||
const revision = {
|
||||
id: ++this.sequence,
|
||||
patch,
|
||||
expireId: null,
|
||||
}
|
||||
|
||||
return { response, revision }
|
||||
}
|
||||
}
|
||||
620
frontend/projects/ui/src/app/services/api/mock-patch.ts
Normal file
620
frontend/projects/ui/src/app/services/api/mock-patch.ts
Normal file
@@ -0,0 +1,620 @@
|
||||
import { DataModel, DependencyErrorType, DockerIoFormat, HealthResult, Manifest, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
export const mockPatchData: DataModel = {
|
||||
'ui': {
|
||||
'name': 'Matt\'s Embassy',
|
||||
'auto-check-updates': true,
|
||||
'pkg-order': [],
|
||||
'ack-welcome': '1.0.0',
|
||||
'ack-share-stats': false,
|
||||
},
|
||||
'server-info': {
|
||||
'id': 'embassy-abcdefgh',
|
||||
'version': '0.3.0',
|
||||
'last-backup': null,
|
||||
'status': ServerStatus.Running,
|
||||
'lan-address': 'https://embassy-abcdefgh.local',
|
||||
'tor-address': 'http://myveryownspecialtoraddress.onion',
|
||||
'eos-marketplace': 'https://beta-registry-0-3.start9labs.com',
|
||||
'package-marketplace': null,
|
||||
'share-stats': false,
|
||||
'unread-notification-count': 4,
|
||||
'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'eos-version-compat': '>=0.3.0',
|
||||
},
|
||||
'recovered-packages': {
|
||||
'btc-rpc-proxy': {
|
||||
'title': 'Bitcoin Proxy',
|
||||
'icon': 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
'version': '0.2.2',
|
||||
},
|
||||
},
|
||||
'package-data': {
|
||||
'bitcoind': {
|
||||
'state': PackageState.Installed,
|
||||
'static-files': {
|
||||
'license': '/public/package-data/bitcoind/0.20.0/LICENSE.md',
|
||||
'icon': '/assets/img/service-icons/bitcoind.png',
|
||||
'instructions': '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md',
|
||||
},
|
||||
'manifest': {
|
||||
'id': 'bitcoind',
|
||||
'title': 'Bitcoin Core',
|
||||
'version': '0.20.0',
|
||||
'description': {
|
||||
'short': 'A Bitcoin full node by Bitcoin Core.',
|
||||
'long': 'Bitcoin is a decentralized consensus protocol and settlement network.',
|
||||
},
|
||||
'release-notes': 'Taproot, Schnorr, and more.',
|
||||
'license': 'MIT',
|
||||
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
|
||||
'upstream-repo': 'https://github.com/bitcoin/bitcoin',
|
||||
'support-site': 'https://bitcoin.org',
|
||||
'marketing-site': 'https://bitcoin.org',
|
||||
'donation-url': 'https://start9.com',
|
||||
'alerts': {
|
||||
'install': 'Bitcoin can take over a week to sync.',
|
||||
'uninstall': 'Chain state will be lost, as will any funds stored on your Bitcoin Core waller that have not been backed up.',
|
||||
'restore': null,
|
||||
'start': null,
|
||||
'stop': 'Stopping Bitcoin is bad for your health.',
|
||||
},
|
||||
'main': {
|
||||
'type': 'docker',
|
||||
'image': '',
|
||||
'system': true,
|
||||
'entrypoint': '',
|
||||
'args': [],
|
||||
'mounts': { },
|
||||
'io-format': DockerIoFormat.Yaml,
|
||||
'inject': false,
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': '.49m',
|
||||
},
|
||||
'health-checks': {
|
||||
'chain-state': { 'name': 'Chain State', 'description': 'Checks the chainstate' },
|
||||
'ephemeral-health-check': { 'name': 'Ephemeral Health Check', 'description': 'Checks to see if your new user registrations are on. If they are but you\'re not expecting any new user signups, you should disable this in Config, as anyone who knows your onion URL can create accounts on your server.' },
|
||||
'p2p-interface': { 'name': 'P2P Interface', 'description': 'Checks to see if your new user registrations are on. If they are but you\'re not expecting any new user signups, you should disable this in Config, as anyone who knows your onion URL can create accounts on your server.' },
|
||||
'rpc-interface': { 'name': 'RPC Interface', 'description': 'Checks the RPC Interface' },
|
||||
'unnecessary-health-check': { 'name': 'Unneccessary Health Check', 'description': 'Is totally not necessary to do this health check.' },
|
||||
} as any,
|
||||
'config': {
|
||||
'get': { },
|
||||
'set': { },
|
||||
} as any,
|
||||
'volumes': { },
|
||||
'min-os-version': '0.2.12',
|
||||
'interfaces': {
|
||||
'ui': {
|
||||
'name': 'Node Visualizer',
|
||||
'description': 'Web application for viewing information about your node and the Bitcoin network.',
|
||||
'ui': true,
|
||||
'tor-config': {
|
||||
'port-mapping': { },
|
||||
},
|
||||
'lan-config': { },
|
||||
'protocols': [],
|
||||
},
|
||||
'rpc': {
|
||||
'name': 'RPC',
|
||||
'description': 'Used by wallets to interact with your Bitcoin Core node.',
|
||||
'ui': false,
|
||||
'tor-config': {
|
||||
'port-mapping': { },
|
||||
},
|
||||
'lan-config': { },
|
||||
'protocols': [],
|
||||
},
|
||||
'p2p': {
|
||||
'name': 'P2P',
|
||||
'description': 'Used by other Bitcoin nodes to communicate and interact with your node.',
|
||||
'ui': false,
|
||||
'tor-config': {
|
||||
'port-mapping': { },
|
||||
},
|
||||
'lan-config': { },
|
||||
'protocols': [],
|
||||
},
|
||||
},
|
||||
'backup': {
|
||||
'create': {
|
||||
'type': 'docker',
|
||||
'image': '',
|
||||
'system': true,
|
||||
'entrypoint': '',
|
||||
'args': [],
|
||||
'mounts': { },
|
||||
'io-format': DockerIoFormat.Yaml,
|
||||
'inject': false,
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': null,
|
||||
},
|
||||
'restore': {
|
||||
'type': 'docker',
|
||||
'image': '',
|
||||
'system': true,
|
||||
'entrypoint': '',
|
||||
'args': [],
|
||||
'mounts': { },
|
||||
'io-format': DockerIoFormat.Yaml,
|
||||
'inject': false,
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': null,
|
||||
},
|
||||
},
|
||||
'migrations': null,
|
||||
'actions': {
|
||||
'resync': {
|
||||
'name': 'Resync Blockchain',
|
||||
'description': 'Use this to resync the Bitcoin blockchain from genesis',
|
||||
'warning': 'This will take a couple of days.',
|
||||
'allowed-statuses': [
|
||||
PackageMainStatus.Running,
|
||||
PackageMainStatus.Stopped,
|
||||
],
|
||||
'implementation': {
|
||||
'type': 'docker',
|
||||
'image': '',
|
||||
'system': true,
|
||||
'entrypoint': '',
|
||||
'args': [],
|
||||
'mounts': { },
|
||||
'io-format': DockerIoFormat.Yaml,
|
||||
'inject': false,
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': null,
|
||||
},
|
||||
'input-spec': {
|
||||
'reason': {
|
||||
'type': 'string',
|
||||
'name': 'Re-sync Reason',
|
||||
'description': 'Your reason for re-syncing. Why are you doing this?',
|
||||
'nullable': false,
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
'pattern': '^[a-zA-Z]+$',
|
||||
'pattern-description': 'Must contain only letters.',
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'name': 'Your Name',
|
||||
'description': 'Tell the class your name.',
|
||||
'nullable': true,
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
'pattern': null,
|
||||
'pattern-description': null,
|
||||
'warning': 'You may loose all your money by providing your name.',
|
||||
},
|
||||
'notifications': {
|
||||
'name': 'Notification Preferences',
|
||||
'type': 'list',
|
||||
'subtype': 'enum',
|
||||
'description': 'how you want to be notified',
|
||||
'range': '[1,3]',
|
||||
'default': [
|
||||
'email',
|
||||
],
|
||||
'spec': {
|
||||
'value-names': {
|
||||
'email': 'Email',
|
||||
'text': 'Text',
|
||||
'call': 'Call',
|
||||
'push': 'Push',
|
||||
'webhook': 'Webhook',
|
||||
},
|
||||
'values': [
|
||||
'email',
|
||||
'text',
|
||||
'call',
|
||||
'push',
|
||||
'webhook',
|
||||
],
|
||||
},
|
||||
},
|
||||
'days-ago': {
|
||||
'type': 'number',
|
||||
'name': 'Days Ago',
|
||||
'description': 'Number of days to re-sync.',
|
||||
'nullable': false,
|
||||
'default': 100,
|
||||
'range': '[0, 9999]',
|
||||
'integral': true,
|
||||
},
|
||||
'top-speed': {
|
||||
'type': 'number',
|
||||
'name': 'Top Speed',
|
||||
'description': 'The fastest you can possibly run.',
|
||||
'nullable': false,
|
||||
'default': null,
|
||||
'range': '[-1000, 1000]',
|
||||
'integral': false,
|
||||
'units': 'm/s',
|
||||
},
|
||||
'testnet': {
|
||||
'name': 'Testnet',
|
||||
'type': 'boolean',
|
||||
'description': 'determines whether your node is running on testnet or mainnet',
|
||||
'warning': 'Chain will have to resync!',
|
||||
'default': false,
|
||||
},
|
||||
'randomEnum': {
|
||||
'name': 'Random Enum',
|
||||
'type': 'enum',
|
||||
'value-names': {
|
||||
'null': 'Null',
|
||||
'good': 'Good',
|
||||
'bad': 'Bad',
|
||||
'ugly': 'Ugly',
|
||||
},
|
||||
'default': 'null',
|
||||
'description': 'This is not even real.',
|
||||
'warning': 'Be careful changing this!',
|
||||
'values': [
|
||||
'null',
|
||||
'good',
|
||||
'bad',
|
||||
'ugly',
|
||||
],
|
||||
},
|
||||
'emergency-contact': {
|
||||
'name': 'Emergency Contact',
|
||||
'type': 'object',
|
||||
'unique-by': null,
|
||||
'description': 'The person to contact in case of emergency.',
|
||||
'spec': {
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'name': 'Name',
|
||||
'description': null,
|
||||
'nullable': false,
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
'pattern': '^[a-zA-Z]+$',
|
||||
'pattern-description': 'Must contain only letters.',
|
||||
},
|
||||
'email': {
|
||||
'type': 'string',
|
||||
'name': 'Email',
|
||||
'description': null,
|
||||
'nullable': false,
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'ips': {
|
||||
'name': 'Whitelist IPs',
|
||||
'type': 'list',
|
||||
'subtype': 'string',
|
||||
'description': 'external ip addresses that are authorized to access your Bitcoin node',
|
||||
'warning': 'Any IP you allow here will have RPC access to your Bitcoin node.',
|
||||
'range': '[1,10]',
|
||||
'default': [
|
||||
'192.168.1.1',
|
||||
],
|
||||
'spec': {
|
||||
'pattern': '^[0-9]{1,3}([,.][0-9]{1,3})?$',
|
||||
'pattern-description': 'Must be a valid IP address',
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
},
|
||||
'bitcoinNode': {
|
||||
'name': 'Bitcoin Node Settings',
|
||||
'type': 'union',
|
||||
'unique-by': null,
|
||||
'description': 'The node settings',
|
||||
'default': 'internal',
|
||||
'warning': 'Careful changing this',
|
||||
'tag': {
|
||||
'id': 'type',
|
||||
'name': 'Type',
|
||||
'variant-names': {
|
||||
'internal': 'Internal',
|
||||
'external': 'External',
|
||||
},
|
||||
},
|
||||
'variants': {
|
||||
'internal': {
|
||||
'lan-address': {
|
||||
'name': 'LAN Address',
|
||||
'type': 'pointer',
|
||||
'subtype': 'package',
|
||||
'target': 'lan-address',
|
||||
'package-id': 'bitcoind',
|
||||
'description': 'the lan address',
|
||||
'interface': '',
|
||||
},
|
||||
'friendly-name': {
|
||||
'name': 'Friendly Name',
|
||||
'type': 'string',
|
||||
'description': 'the lan address',
|
||||
'nullable': true,
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
},
|
||||
'external': {
|
||||
'public-domain': {
|
||||
'name': 'Public Domain',
|
||||
'type': 'string',
|
||||
'description': 'the public address of the node',
|
||||
'nullable': false,
|
||||
'default': 'bitcoinnode.com',
|
||||
'pattern': '.*',
|
||||
'pattern-description': 'anything',
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'permissions': { },
|
||||
'dependencies': { },
|
||||
},
|
||||
'installed': {
|
||||
'manifest': { } as Manifest,
|
||||
'last-backup': null,
|
||||
'status': {
|
||||
'configured': true,
|
||||
'main': {
|
||||
'status': PackageMainStatus.Running,
|
||||
'started': '2021-06-14T20:49:17.774Z',
|
||||
'health': {
|
||||
'ephemeral-health-check': {
|
||||
'result': HealthResult.Starting,
|
||||
},
|
||||
'chain-state': {
|
||||
'result': HealthResult.Loading,
|
||||
'message': 'Bitcoin is syncing from genesis',
|
||||
},
|
||||
'p2p-interface': {
|
||||
'result': HealthResult.Success,
|
||||
},
|
||||
'rpc-interface': {
|
||||
'result': HealthResult.Failure,
|
||||
'error': 'RPC interface unreachable.',
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
'result': HealthResult.Disabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
'dependency-errors': { },
|
||||
},
|
||||
'interface-addresses': {
|
||||
'ui': {
|
||||
'tor-address': 'bitcoind-ui-address.onion',
|
||||
'lan-address': 'bitcoind-ui-address.local',
|
||||
},
|
||||
'rpc': {
|
||||
'tor-address': 'bitcoind-rpc-address.onion',
|
||||
'lan-address': 'bitcoind-rpc-address.local',
|
||||
},
|
||||
'p2p': {
|
||||
'tor-address': 'bitcoind-p2p-address.onion',
|
||||
'lan-address': 'bitcoind-p2p-address.local',
|
||||
},
|
||||
},
|
||||
'system-pointers': [],
|
||||
'current-dependents': {
|
||||
'lnd': {
|
||||
'pointers': [],
|
||||
'health-checks': [],
|
||||
},
|
||||
},
|
||||
'current-dependencies': { },
|
||||
'dependency-info': { },
|
||||
},
|
||||
},
|
||||
'lnd': {
|
||||
'state': PackageState.Installed,
|
||||
'static-files': {
|
||||
'license': '/public/package-data/lnd/0.11.1/LICENSE.md',
|
||||
'icon': '/assets/img/service-icons/lnd.png',
|
||||
'instructions': '/public/package-data/lnd/0.11.1/INSTRUCTIONS.md',
|
||||
},
|
||||
'manifest': {
|
||||
'id': 'lnd',
|
||||
'title': 'Lightning Network Daemon',
|
||||
'version': '0.11.1',
|
||||
'description': {
|
||||
'short': 'A bolt spec compliant client.',
|
||||
'long': 'More info about LND. More info about LND. More info about LND.',
|
||||
},
|
||||
'release-notes': 'Dual funded channels!',
|
||||
'license': 'MIT',
|
||||
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
|
||||
'upstream-repo': 'https://github.com/lightningnetwork/lnd',
|
||||
'support-site': 'https://lightning.engineering/',
|
||||
'marketing-site': 'https://lightning.engineering/',
|
||||
'donation-url': null,
|
||||
'alerts': {
|
||||
'install': null,
|
||||
'uninstall': null,
|
||||
'restore': 'If this is a duplicate instance of the same LND node, you may loose your funds.',
|
||||
'start': 'Starting LND is good for your health.',
|
||||
'stop': null,
|
||||
},
|
||||
'main': {
|
||||
'type': 'docker',
|
||||
'image': '',
|
||||
'system': true,
|
||||
'entrypoint': '',
|
||||
'args': [],
|
||||
'mounts': { },
|
||||
'io-format': DockerIoFormat.Yaml,
|
||||
'inject': false,
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': '0.5s',
|
||||
},
|
||||
'health-checks': { },
|
||||
'config': {
|
||||
'get': null,
|
||||
'set': null,
|
||||
},
|
||||
'volumes': { },
|
||||
'min-os-version': '0.2.12',
|
||||
'interfaces': {
|
||||
'rpc': {
|
||||
'name': 'RPC interface',
|
||||
'description': 'Good for connecting to your node at a distance.',
|
||||
'ui': true,
|
||||
'tor-config': {
|
||||
'port-mapping': { },
|
||||
},
|
||||
'lan-config': {
|
||||
'44': {
|
||||
'ssl': true,
|
||||
'mapping': 33,
|
||||
},
|
||||
},
|
||||
'protocols': [],
|
||||
},
|
||||
'grpc': {
|
||||
'name': 'GRPC',
|
||||
'description': 'Certain wallet use grpc.',
|
||||
'ui': false,
|
||||
'tor-config': {
|
||||
'port-mapping': { },
|
||||
},
|
||||
'lan-config': {
|
||||
'66': {
|
||||
'ssl': true,
|
||||
'mapping': 55,
|
||||
},
|
||||
},
|
||||
'protocols': [],
|
||||
},
|
||||
},
|
||||
'backup': {
|
||||
'create': {
|
||||
'type': 'docker',
|
||||
'image': '',
|
||||
'system': true,
|
||||
'entrypoint': '',
|
||||
'args': [],
|
||||
'mounts': { },
|
||||
'io-format': DockerIoFormat.Yaml,
|
||||
'inject': false,
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': null,
|
||||
},
|
||||
'restore': {
|
||||
'type': 'docker',
|
||||
'image': '',
|
||||
'system': true,
|
||||
'entrypoint': '',
|
||||
'args': [],
|
||||
'mounts': { },
|
||||
'io-format': DockerIoFormat.Yaml,
|
||||
'inject': false,
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': null,
|
||||
},
|
||||
},
|
||||
'migrations': null,
|
||||
'actions': {
|
||||
'resync': {
|
||||
'name': 'Resync Network Graph',
|
||||
'description': 'Your node will resync its network graph.',
|
||||
'warning': 'This will take a couple hours.',
|
||||
'allowed-statuses': [
|
||||
PackageMainStatus.Running,
|
||||
],
|
||||
'implementation': {
|
||||
'type': 'docker',
|
||||
'image': '',
|
||||
'system': true,
|
||||
'entrypoint': '',
|
||||
'args': [],
|
||||
'mounts': { },
|
||||
'io-format': DockerIoFormat.Yaml,
|
||||
'inject': false,
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': null,
|
||||
},
|
||||
'input-spec': null,
|
||||
},
|
||||
},
|
||||
'permissions': { },
|
||||
'dependencies': {
|
||||
'bitcoind': {
|
||||
'version': '=0.21.0',
|
||||
'description': 'LND needs bitcoin to live.',
|
||||
'requirement': {
|
||||
'type': 'opt-out',
|
||||
'how': 'You can use an external node from your Embassy if you prefer.',
|
||||
},
|
||||
'config': null,
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
'version': '>=0.2.2',
|
||||
'description': 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.',
|
||||
'requirement': {
|
||||
'type': 'opt-in',
|
||||
'how': 'To use Proxy\'s user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.',
|
||||
},
|
||||
'config': null,
|
||||
},
|
||||
},
|
||||
},
|
||||
'installed': {
|
||||
'manifest': { } as Manifest,
|
||||
'last-backup': null,
|
||||
'status': {
|
||||
'configured': true,
|
||||
'main': {
|
||||
'status': PackageMainStatus.Stopped,
|
||||
},
|
||||
'dependency-errors': {
|
||||
'btc-rpc-proxy': {
|
||||
'type': DependencyErrorType.ConfigUnsatisfied,
|
||||
'error': 'This is a config unsatisfied error',
|
||||
},
|
||||
},
|
||||
},
|
||||
'interface-addresses': {
|
||||
'rpc': {
|
||||
'tor-address': 'lnd-rpc-address.onion',
|
||||
'lan-address': 'lnd-rpc-address.local',
|
||||
},
|
||||
'grpc': {
|
||||
'tor-address': 'lnd-grpc-address.onion',
|
||||
'lan-address': 'lnd-grpc-address.local',
|
||||
},
|
||||
},
|
||||
'system-pointers': [],
|
||||
'current-dependents': { },
|
||||
'current-dependencies': {
|
||||
'bitcoind': {
|
||||
'pointers': [],
|
||||
'health-checks': [],
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
'pointers': [],
|
||||
'health-checks': [],
|
||||
},
|
||||
},
|
||||
'dependency-info': {
|
||||
'bitcoind': {
|
||||
'manifest': {
|
||||
'title': 'Bitcoin Core',
|
||||
} as Manifest,
|
||||
'icon': 'assets/img/service-icons/bitcoind.png',
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
'manifest': {
|
||||
'title': 'Bitcoin Proxy',
|
||||
} as Manifest,
|
||||
'icon': 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
38
frontend/projects/ui/src/app/services/auth.service.ts
Normal file
38
frontend/projects/ui/src/app/services/auth.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
import { distinctUntilChanged } from 'rxjs/operators'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
|
||||
export enum AuthState {
|
||||
UNVERIFIED,
|
||||
VERIFIED,
|
||||
}
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly LOGGED_IN_KEY = 'loggedInKey'
|
||||
private readonly authState$: BehaviorSubject<AuthState> = new BehaviorSubject(undefined)
|
||||
|
||||
constructor (
|
||||
private readonly storage: Storage,
|
||||
) { }
|
||||
|
||||
async init (): Promise<void> {
|
||||
const loggedIn = await this.storage.get(this.LOGGED_IN_KEY)
|
||||
this.authState$.next( loggedIn ? AuthState.VERIFIED : AuthState.UNVERIFIED)
|
||||
}
|
||||
|
||||
watch$ (): Observable<AuthState> {
|
||||
return this.authState$.pipe(distinctUntilChanged())
|
||||
}
|
||||
|
||||
async setVerified (): Promise<void> {
|
||||
await this.storage.set(this.LOGGED_IN_KEY, true)
|
||||
this.authState$.next(AuthState.VERIFIED)
|
||||
}
|
||||
|
||||
async setUnverified (): Promise<void> {
|
||||
this.authState$.next(AuthState.UNVERIFIED)
|
||||
}
|
||||
}
|
||||
112
frontend/projects/ui/src/app/services/config.service.ts
Normal file
112
frontend/projects/ui/src/app/services/config.service.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
InterfaceDef,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
PackageState,
|
||||
} from './patch-db/data-model'
|
||||
import { WorkspaceConfig } from '@shared'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
ui: { gitHash, patchDb, api, mocks },
|
||||
} = require('../../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConfigService {
|
||||
origin = removePort(removeProtocol(window.origin))
|
||||
version = require('../../../../../package.json').version
|
||||
|
||||
useMocks = useMocks
|
||||
mocks = mocks
|
||||
|
||||
gitHash = gitHash
|
||||
patchDb = patchDb
|
||||
api = api
|
||||
|
||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||
isConsulate = window['platform'] === 'ios'
|
||||
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
||||
|
||||
isTor(): boolean {
|
||||
return (
|
||||
(useMocks && mocks.maskAs === 'tor') || this.origin.endsWith('.onion')
|
||||
)
|
||||
}
|
||||
|
||||
isLan(): boolean {
|
||||
return (
|
||||
(useMocks && mocks.maskAs === 'lan') || this.origin.endsWith('.local')
|
||||
)
|
||||
}
|
||||
|
||||
isLaunchable(
|
||||
state: PackageState,
|
||||
status: PackageMainStatus,
|
||||
interfaces: Record<string, InterfaceDef>,
|
||||
): boolean {
|
||||
if (state !== PackageState.Installed) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
status === PackageMainStatus.Running &&
|
||||
((hasTorUi(interfaces) && this.isTor()) ||
|
||||
(hasLanUi(interfaces) && !this.isTor()))
|
||||
)
|
||||
}
|
||||
|
||||
launchableURL(pkg: PackageDataEntry): string {
|
||||
return this.isTor()
|
||||
? `http://${torUiAddress(pkg)}`
|
||||
: `https://${lanUiAddress(pkg)}`
|
||||
}
|
||||
}
|
||||
|
||||
export function hasTorUi(interfaces: Record<string, InterfaceDef>): boolean {
|
||||
const int = getUiInterfaceValue(interfaces)
|
||||
return !!int?.['tor-config']
|
||||
}
|
||||
|
||||
export function hasLanUi(interfaces: Record<string, InterfaceDef>): boolean {
|
||||
const int = getUiInterfaceValue(interfaces)
|
||||
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 lanUiAddress(pkg: PackageDataEntry): string {
|
||||
const key = getUiInterfaceKey(pkg.manifest.interfaces)
|
||||
return pkg.installed['interface-addresses'][key]['lan-address']
|
||||
}
|
||||
|
||||
export function hasUi(interfaces: Record<string, InterfaceDef>): boolean {
|
||||
return hasTorUi(interfaces) || hasLanUi(interfaces)
|
||||
}
|
||||
|
||||
export function removeProtocol(str: string): string {
|
||||
if (str.startsWith('http://')) return str.slice(7)
|
||||
if (str.startsWith('https://')) return str.slice(8)
|
||||
return str
|
||||
}
|
||||
|
||||
export function removePort(str: string): string {
|
||||
return str.split(':')[0]
|
||||
}
|
||||
|
||||
export function getUiInterfaceKey(
|
||||
interfaces: Record<string, InterfaceDef>,
|
||||
): string {
|
||||
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)
|
||||
}
|
||||
68
frontend/projects/ui/src/app/services/connection.service.ts
Normal file
68
frontend/projects/ui/src/app/services/connection.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject, combineLatest, fromEvent, merge, Subscription } from 'rxjs'
|
||||
import { PatchConnection, PatchDbService } from './patch-db/patch-db.service'
|
||||
import { distinctUntilChanged } from 'rxjs/operators'
|
||||
import { ConfigService } from './config.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConnectionService {
|
||||
private readonly networkState$ = new BehaviorSubject<boolean>(true)
|
||||
private readonly connectionFailure$ = new BehaviorSubject<ConnectionFailure>(ConnectionFailure.None)
|
||||
|
||||
constructor (
|
||||
private readonly configService: ConfigService,
|
||||
private readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
watchFailure$ () {
|
||||
return this.connectionFailure$.asObservable()
|
||||
}
|
||||
|
||||
start (): Subscription[] {
|
||||
const sub1 = merge(fromEvent(window, 'online'), fromEvent(window, 'offline'))
|
||||
.subscribe(event => {
|
||||
this.networkState$.next(event.type === 'online')
|
||||
})
|
||||
|
||||
const sub2 = combineLatest([
|
||||
// 1
|
||||
this.networkState$
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
// 2
|
||||
this.patch.watchPatchConnection$()
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
// 3
|
||||
this.patch.watch$('server-info', 'update-progress')
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
])
|
||||
.subscribe(async ([network, patchConnection, progress]) => {
|
||||
if (!network) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Network)
|
||||
} else if (patchConnection !== PatchConnection.Disconnected) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!!progress && progress.downloaded === progress.size) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!this.configService.isTor()) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Lan)
|
||||
} else {
|
||||
this.connectionFailure$.next(ConnectionFailure.Tor)
|
||||
}
|
||||
})
|
||||
return [sub1, sub2]
|
||||
}
|
||||
}
|
||||
|
||||
export enum ConnectionFailure {
|
||||
None = 'none',
|
||||
Network = 'network',
|
||||
Tor = 'tor',
|
||||
Lan = 'lan',
|
||||
}
|
||||
13
frontend/projects/ui/src/app/services/destroy.service.ts
Normal file
13
frontend/projects/ui/src/app/services/destroy.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { ReplaySubject } from "rxjs";
|
||||
|
||||
/**
|
||||
* Observable abstraction over ngOnDestroy to use with takeUntil
|
||||
*/
|
||||
@Injectable()
|
||||
export class DestroyService extends ReplaySubject<void> implements OnDestroy {
|
||||
ngOnDestroy() {
|
||||
this.next();
|
||||
this.complete();
|
||||
}
|
||||
}
|
||||
19
frontend/projects/ui/src/app/services/emver.service.ts
Normal file
19
frontend/projects/ui/src/app/services/emver.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import * as emver from '@start9labs/emver'
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class Emver {
|
||||
constructor () { }
|
||||
|
||||
compare (lhs: string, rhs: string): number {
|
||||
if (!lhs || !rhs) return null
|
||||
return emver.compare(lhs, rhs)
|
||||
}
|
||||
|
||||
satisfies (version: string, range: string): boolean {
|
||||
return emver.satisfies(version, range)
|
||||
}
|
||||
}
|
||||
63
frontend/projects/ui/src/app/services/error-toast.service.ts
Normal file
63
frontend/projects/ui/src/app/services/error-toast.service.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { IonicSafeString, ToastController } from '@ionic/angular'
|
||||
import { RequestError } from './http.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ErrorToastService {
|
||||
private toast: HTMLIonToastElement
|
||||
|
||||
constructor (
|
||||
private readonly toastCtrl: ToastController,
|
||||
) { }
|
||||
|
||||
async present (e: RequestError, link?: string): Promise<void> {
|
||||
console.error(e)
|
||||
|
||||
if (this.toast) return
|
||||
|
||||
this.toast = await this.toastCtrl.create({
|
||||
header: 'Error',
|
||||
message: getErrorMessage(e, link),
|
||||
duration: 0,
|
||||
position: 'top',
|
||||
cssClass: 'error-toast',
|
||||
buttons: [
|
||||
{
|
||||
side: 'end',
|
||||
icon: 'close',
|
||||
handler: () => {
|
||||
this.dismiss()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await this.toast.present()
|
||||
}
|
||||
|
||||
async dismiss (): Promise<void> {
|
||||
if (this.toast) {
|
||||
await this.toast.dismiss()
|
||||
this.toast = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage (e: RequestError, link?: string): string | IonicSafeString {
|
||||
let message: string | IonicSafeString
|
||||
|
||||
if (e.message) message = `${message ? message + ' ' : ''}${e.message}`
|
||||
if (e.details) message = `${message ? message + ': ' : ''}${e.details}`
|
||||
|
||||
if (!message) {
|
||||
message = 'Unknown Error.'
|
||||
link = 'https://docs.start9.com/support/FAQ/index.html'
|
||||
}
|
||||
|
||||
if (link) {
|
||||
message = new IonicSafeString(`${message}<br /><br /><a href=${link} target="_blank" rel="noreferrer" style="color: white;">Get Help</a>`)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
431
frontend/projects/ui/src/app/services/form.service.ts
Normal file
431
frontend/projects/ui/src/app/services/form.service.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'
|
||||
import { ConfigSpec, isValueSpecListOf, ListValueSpecNumber, ListValueSpecObject, ListValueSpecOf, ListValueSpecString, ListValueSpecUnion, UniqueBy, ValueSpec, ValueSpecEnum, ValueSpecList, ValueSpecNumber, ValueSpecObject, ValueSpecString, ValueSpecUnion } from '../pkg-config/config-types'
|
||||
import { getDefaultString, Range } from '../pkg-config/config-utilities'
|
||||
const Mustache = require('mustache')
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class FormService {
|
||||
|
||||
constructor (
|
||||
private readonly formBuilder: FormBuilder,
|
||||
) { }
|
||||
|
||||
createForm (spec: ConfigSpec, current: { [key: string]: any } = { }): FormGroup {
|
||||
return this.getFormGroup(spec, [], current)
|
||||
}
|
||||
|
||||
getUnionObject (spec: ValueSpecUnion | ListValueSpecUnion, selection: string, current?: { [key: string]: any }): FormGroup {
|
||||
const { variants, tag } = spec
|
||||
const { name, description, warning } = isFullUnion(spec) ? spec : { ...spec.tag, warning: undefined }
|
||||
|
||||
const enumSpec: ValueSpecEnum = {
|
||||
type: 'enum',
|
||||
name,
|
||||
description,
|
||||
warning,
|
||||
default: selection,
|
||||
values: Object.keys(variants),
|
||||
'value-names': tag['variant-names'],
|
||||
}
|
||||
return this.getFormGroup({ [spec.tag.id]: enumSpec, ...spec.variants[selection] }, [], current)
|
||||
}
|
||||
|
||||
getListItem (spec: ValueSpecList, entry: any) {
|
||||
const listItemValidators = this.getListItemValidators(spec)
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return this.formBuilder.control(entry, listItemValidators)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
return this.formBuilder.control(entry, listItemValidators)
|
||||
} else if (isValueSpecListOf(spec, 'enum')) {
|
||||
return this.formBuilder.control(entry)
|
||||
} else if (isValueSpecListOf(spec, 'object')) {
|
||||
return this.getFormGroup(spec.spec.spec, listItemValidators, entry)
|
||||
} else if (isValueSpecListOf(spec, 'union')) {
|
||||
return this.getUnionObject(spec.spec, spec.spec.default, entry)
|
||||
}
|
||||
}
|
||||
|
||||
private getListItemValidators (spec: ValueSpecList) {
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return this.stringValidators(spec.spec)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
return this.numberValidators(spec.spec)
|
||||
}
|
||||
}
|
||||
|
||||
private getFormGroup (config: ConfigSpec, validators: ValidatorFn[] = [], current: { [key: string]: any } = { }): FormGroup {
|
||||
let group = { }
|
||||
Object.entries(config).map(([key, spec]) => {
|
||||
if (spec.type === 'pointer') return
|
||||
group[key] = this.getFormEntry(spec, current ? current[key] : undefined)
|
||||
})
|
||||
return this.formBuilder.group(group, { validators } )
|
||||
}
|
||||
|
||||
private getFormEntry (spec: ValueSpec, currentValue?: any): FormGroup | FormArray | FormControl {
|
||||
let validators: ValidatorFn[]
|
||||
let value: any
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
validators = this.stringValidators(spec)
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
value = spec.default ? getDefaultString(spec.default) : null
|
||||
}
|
||||
return this.formBuilder.control(value, validators)
|
||||
case 'number':
|
||||
validators = this.numberValidators(spec)
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
value = spec.default || null
|
||||
}
|
||||
return this.formBuilder.control(value, validators)
|
||||
case 'object':
|
||||
return this.getFormGroup(spec.spec, [], currentValue)
|
||||
case 'list':
|
||||
validators = this.listValidators(spec)
|
||||
const mapped = (Array.isArray(currentValue) ? currentValue : spec.default as any[]).map(entry => {
|
||||
return this.getListItem(spec, entry)
|
||||
})
|
||||
return this.formBuilder.array(mapped, validators)
|
||||
case 'union':
|
||||
return this.getUnionObject(spec, currentValue?.[spec.tag.id] || spec.default, currentValue)
|
||||
case 'boolean':
|
||||
case 'enum':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value)
|
||||
}
|
||||
}
|
||||
|
||||
private stringValidators (spec: ValueSpecString | ListValueSpecString): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (!(spec as ValueSpecString).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
if (spec.pattern) {
|
||||
validators.push(Validators.pattern(spec.pattern))
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
private numberValidators (spec: ValueSpecNumber | ListValueSpecNumber): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(isNumber())
|
||||
|
||||
if (!(spec as ValueSpecNumber).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
if (spec.integral) {
|
||||
validators.push(isInteger())
|
||||
}
|
||||
|
||||
validators.push(numberInRange(spec.range))
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
private listValidators (spec: ValueSpecList): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(listInRange(spec.range))
|
||||
|
||||
validators.push(listItemIssue())
|
||||
|
||||
if (!isValueSpecListOf(spec, 'enum')) {
|
||||
validators.push(listUnique(spec))
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
}
|
||||
|
||||
function isFullUnion (spec: ValueSpecUnion | ListValueSpecUnion): spec is ValueSpecUnion {
|
||||
return !!(spec as ValueSpecUnion).name
|
||||
}
|
||||
|
||||
export function numberInRange (stringRange: string): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value = control.value
|
||||
if (!value) return null
|
||||
try {
|
||||
Range.from(stringRange).checkIncludes(value)
|
||||
return null
|
||||
} catch (e) {
|
||||
return { numberNotInRange: { value: `Number must be ${e.message}` } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isNumber (): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return !control.value || control.value == Number(control.value) ?
|
||||
null :
|
||||
{ notNumber: { value: control.value } }
|
||||
}
|
||||
}
|
||||
|
||||
export function isInteger (): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return !control.value || control.value == Math.trunc(control.value) ?
|
||||
null :
|
||||
{ numberNotInteger: { value: control.value } }
|
||||
}
|
||||
}
|
||||
|
||||
export function listInRange (stringRange: string): ValidatorFn {
|
||||
return (control: FormArray): ValidationErrors | null => {
|
||||
try {
|
||||
Range.from(stringRange).checkIncludes(control.value.length)
|
||||
return null
|
||||
} catch (e) {
|
||||
return { listNotInRange: { value: `List must be ${e.message}` } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listItemIssue (): ValidatorFn {
|
||||
return (parentControl: FormArray): ValidationErrors | null => {
|
||||
const problemChild = parentControl.controls.find(c => c.invalid)
|
||||
if (problemChild) {
|
||||
return { listItemIssue: { value: 'Invalid entries' } }
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listUnique (spec: ValueSpecList): ValidatorFn {
|
||||
return (control: FormArray): ValidationErrors | null => {
|
||||
const list = control.value
|
||||
for (let idx = 0; idx < list.length; idx++) {
|
||||
for (let idx2 = idx + 1; idx2 < list.length; idx2++) {
|
||||
if (listItemEquals(spec, list[idx], list[idx2])) {
|
||||
let display1: string
|
||||
let display2: string
|
||||
let uniqueMessage = isObjectOrUnion(spec.spec) ? uniqueByMessageWrapper(spec.spec['unique-by'], spec.spec, list[idx]) : ''
|
||||
|
||||
if (isObjectOrUnion(spec.spec) && spec.spec['display-as']) {
|
||||
display1 = `"${(Mustache as any).render(spec.spec['display-as'], list[idx])}"`
|
||||
display2 = `"${(Mustache as any).render(spec.spec['display-as'], list[idx2])}"`
|
||||
} else {
|
||||
display1 = `Entry ${idx + 1}`
|
||||
display2 = `Entry ${idx2 + 1}`
|
||||
}
|
||||
|
||||
return { listNotUnique: { value: `${display1} and ${display2} are not unique.${uniqueMessage}` } }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function listItemEquals (spec: ValueSpecList, val1: any, val2: any): boolean {
|
||||
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)
|
||||
case 'union':
|
||||
return unionEquals(spec.spec['unique-by'], spec.spec as ListValueSpecUnion, val1, val2)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function itemEquals (spec: ValueSpec, val1: any, val2: any): boolean {
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'enum':
|
||||
return val1 == val2
|
||||
case 'object':
|
||||
return objEquals(spec['unique-by'], (spec as ValueSpecObject), val1, val2)
|
||||
case 'union':
|
||||
return unionEquals(spec['unique-by'], (spec as ValueSpecUnion), val1, val2)
|
||||
case 'list':
|
||||
if (val1.length !== val2.length) {
|
||||
return false
|
||||
}
|
||||
for (let idx = 0; idx < val1.length; idx++) {
|
||||
if (listItemEquals(spec, val1[idx], val2[idx])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function listObjEquals (uniqueBy: UniqueBy, spec: ListValueSpecObject, val1: any, val2: any): boolean {
|
||||
if (uniqueBy === null) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return itemEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
} else if ('any' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
if (listObjEquals(subSpec, spec, val1, val2)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if ('all' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.all) {
|
||||
if (!listObjEquals(subSpec, spec, val1, val2)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function objEquals (uniqueBy: UniqueBy, spec: ValueSpecObject, val1: any, val2: any): boolean {
|
||||
if (uniqueBy === null) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return itemEquals(spec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
} else if ('any' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
if (objEquals(subSpec, spec, val1, val2)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if ('all' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.all) {
|
||||
if (!objEquals(subSpec, spec, val1, val2)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function unionEquals (uniqueBy: UniqueBy, spec: ValueSpecUnion | ListValueSpecUnion, val1: any, val2: any): boolean {
|
||||
const tagId = spec.tag.id
|
||||
const variant = spec.variants[val1[tagId]]
|
||||
if (uniqueBy === null) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
if (uniqueBy === tagId) {
|
||||
return val1[tagId] === val2[tagId]
|
||||
} else {
|
||||
return itemEquals(variant[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
}
|
||||
} else if ('any' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
if (unionEquals(subSpec, spec, val1, val2)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if ('all' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.all) {
|
||||
if (!unionEquals(subSpec, spec, val1, val2)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueByMessageWrapper (uniqueBy: UniqueBy, spec: ListValueSpecObject | ListValueSpecUnion, obj: object) {
|
||||
let configSpec: ConfigSpec
|
||||
if (isUnion(spec)) {
|
||||
const variantKey = obj[spec.tag.id]
|
||||
configSpec = spec.variants[variantKey]
|
||||
} else {
|
||||
configSpec = spec.spec
|
||||
}
|
||||
|
||||
const message = uniqueByMessage(uniqueBy, configSpec)
|
||||
if (message) {
|
||||
return ' Must be unique by: ' + message + '.'
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueByMessage (uniqueBy: UniqueBy, configSpec: ConfigSpec, outermost = true): string {
|
||||
let joinFunc
|
||||
const subSpecs = []
|
||||
if (uniqueBy === null) {
|
||||
return null
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return configSpec[uniqueBy] ? configSpec[uniqueBy].name : uniqueBy
|
||||
} else if ('any' in uniqueBy) {
|
||||
joinFunc = ' OR '
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
subSpecs.push(uniqueByMessage(subSpec, configSpec, false))
|
||||
}
|
||||
} else if ('all' in uniqueBy) {
|
||||
joinFunc = ' AND '
|
||||
for (let subSpec of uniqueBy.all) {
|
||||
subSpecs.push(uniqueByMessage(subSpec, configSpec, false))
|
||||
}
|
||||
}
|
||||
const ret = subSpecs.filter(ss => ss).join(joinFunc)
|
||||
return outermost || subSpecs.filter(ss => ss).length === 1 ? ret : '(' + ret + ')'
|
||||
}
|
||||
|
||||
function isObjectOrUnion (spec: ListValueSpecOf<any>): spec is ListValueSpecObject | ListValueSpecUnion {
|
||||
// only lists of objects and unions have unique-by
|
||||
return spec['unique-by'] !== undefined
|
||||
}
|
||||
|
||||
function isUnion (spec: any): spec is ListValueSpecUnion {
|
||||
// only unions have tag
|
||||
return !!spec.tag
|
||||
}
|
||||
|
||||
export function convertValuesRecursive (configSpec: ConfigSpec, group: FormGroup) {
|
||||
Object.entries(configSpec).forEach(([key, valueSpec]) => {
|
||||
if (valueSpec.type === 'number') {
|
||||
const control = group.get(key)
|
||||
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)
|
||||
} else if (valueSpec.type === 'list') {
|
||||
const formArr = group.get(key) as FormArray
|
||||
if (valueSpec.subtype === 'number') {
|
||||
formArr.controls.forEach(control => {
|
||||
control.setValue(control.value ? Number(control.value) : null)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'string') {
|
||||
formArr.controls.forEach(control => {
|
||||
if (!control.value) control.setValue(null)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'object') {
|
||||
formArr.controls.forEach((formGroup: FormGroup) => {
|
||||
const objectSpec = valueSpec.spec as ListValueSpecObject
|
||||
convertValuesRecursive(objectSpec.spec, formGroup)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'union') {
|
||||
formArr.controls.forEach((formGroup: FormGroup) => {
|
||||
const unionSpec = valueSpec.spec as ListValueSpecUnion
|
||||
const spec = unionSpec.variants[formGroup.controls[unionSpec.tag.id].value]
|
||||
convertValuesRecursive(spec, formGroup)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ErrorHandler, Injectable } from '@angular/core'
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
|
||||
handleError (e: any): void {
|
||||
console.error(e)
|
||||
const chunkFailedMessage = /Loading chunk [\d]+ failed/
|
||||
|
||||
if (chunkFailedMessage.test(e.message)) {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
189
frontend/projects/ui/src/app/services/http.service.ts
Normal file
189
frontend/projects/ui/src/app/services/http.service.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http'
|
||||
import { Observable, from, interval, race } from 'rxjs'
|
||||
import { map, take } from 'rxjs/operators'
|
||||
import { ConfigService } from './config.service'
|
||||
import { Revision } from 'patch-db-client'
|
||||
import { AuthService } from './auth.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class HttpService {
|
||||
fullUrl: string
|
||||
|
||||
constructor (
|
||||
private readonly http: HttpClient,
|
||||
private readonly config: ConfigService,
|
||||
private readonly auth: AuthService,
|
||||
) {
|
||||
const port = window.location.port
|
||||
this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}`
|
||||
}
|
||||
|
||||
async rpcRequest<T> (rpcOpts: RPCOptions): Promise<T> {
|
||||
const { url, version } = this.config.api
|
||||
rpcOpts.params = rpcOpts.params || { }
|
||||
const httpOpts: HttpOptions = {
|
||||
method: Method.POST,
|
||||
body: rpcOpts,
|
||||
url: `/${url}/${version}`,
|
||||
}
|
||||
if (rpcOpts.timeout) httpOpts.timeout = rpcOpts.timeout
|
||||
|
||||
const res = await this.httpRequest<RPCResponse<T>>(httpOpts)
|
||||
if (isRpcError(res)) {
|
||||
if (res.error.code === 34) this.auth.setUnverified()
|
||||
throw new RpcError(res.error)
|
||||
}
|
||||
|
||||
if (isRpcSuccess(res)) return res.result
|
||||
}
|
||||
|
||||
async httpRequest<T> (httpOpts: HttpOptions): Promise<T> {
|
||||
if (httpOpts.withCredentials !== false) {
|
||||
httpOpts.withCredentials = true
|
||||
}
|
||||
|
||||
const urlIsRelative = httpOpts.url.startsWith('/')
|
||||
const url = urlIsRelative ?
|
||||
this.fullUrl + httpOpts.url :
|
||||
httpOpts.url
|
||||
|
||||
Object.keys(httpOpts.params || { }).forEach(key => {
|
||||
if (httpOpts.params[key] === undefined) {
|
||||
delete httpOpts.params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const options = {
|
||||
responseType: httpOpts.responseType || 'json',
|
||||
body: httpOpts.body,
|
||||
observe: 'events',
|
||||
reportProgress: false,
|
||||
withCredentials: httpOpts.withCredentials,
|
||||
headers: httpOpts.headers,
|
||||
params: httpOpts.params,
|
||||
timeout: httpOpts.timeout,
|
||||
} as any
|
||||
|
||||
let req: Observable<{ body: T }>
|
||||
switch (httpOpts.method) {
|
||||
case Method.GET: req = this.http.get(url, options) as any; break
|
||||
case Method.POST: req = this.http.post(url, httpOpts.body, options) as any; break
|
||||
case Method.PUT: req = this.http.put(url, httpOpts.body, options) as any; break
|
||||
case Method.PATCH: req = this.http.patch(url, httpOpts.body, options) as any; break
|
||||
case Method.DELETE: req = this.http.delete(url, options) as any; break
|
||||
}
|
||||
|
||||
return (httpOpts.timeout ? withTimeout(req, httpOpts.timeout) : req)
|
||||
.toPromise()
|
||||
.then(res => res.body)
|
||||
.catch(e => { throw new HttpError(e) })
|
||||
}
|
||||
}
|
||||
|
||||
function RpcError (e: RPCError['error']): void {
|
||||
const { code, message, data } = e
|
||||
|
||||
this.code = code
|
||||
this.message = message
|
||||
|
||||
if (typeof data === 'string') {
|
||||
this.details = e.data
|
||||
this.revision = null
|
||||
} else {
|
||||
this.details = data.details
|
||||
this.revision = data.revision
|
||||
}
|
||||
}
|
||||
|
||||
function HttpError (e: HttpErrorResponse): void {
|
||||
const { status, statusText } = e
|
||||
|
||||
this.code = status
|
||||
this.message = statusText
|
||||
this.details = null
|
||||
this.revision = null
|
||||
}
|
||||
|
||||
function isRpcError<Error, Result> (arg: { error: Error } | { result: Result}): arg is { error: Error } {
|
||||
return !!(arg as any).error
|
||||
}
|
||||
|
||||
function isRpcSuccess<Error, Result> (arg: { error: Error } | { result: Result}): arg is { result: Result } {
|
||||
return !!(arg as any).result
|
||||
}
|
||||
|
||||
export interface RequestError {
|
||||
code: number
|
||||
message: string
|
||||
details: string
|
||||
revision: Revision | null
|
||||
}
|
||||
|
||||
export enum Method {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
PUT = 'PUT',
|
||||
PATCH = 'PATCH',
|
||||
DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
export interface RPCOptions {
|
||||
method: string
|
||||
params?: object
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface RPCBase {
|
||||
jsonrpc: '2.0'
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface RPCRequest<T> extends RPCBase {
|
||||
method: string
|
||||
params?: T
|
||||
}
|
||||
|
||||
export interface RPCSuccess<T> extends RPCBase {
|
||||
result: T
|
||||
}
|
||||
|
||||
export interface RPCError extends RPCBase {
|
||||
error: {
|
||||
code: number
|
||||
message: string
|
||||
data?: {
|
||||
details: string
|
||||
revision: Revision | null
|
||||
debug: string | null
|
||||
} | string
|
||||
}
|
||||
}
|
||||
|
||||
export type RPCResponse<T> = RPCSuccess<T> | RPCError
|
||||
|
||||
type HttpError = HttpErrorResponse & { error: { code: string, message: string } }
|
||||
|
||||
export interface HttpOptions {
|
||||
method: Method
|
||||
url: string
|
||||
headers?: HttpHeaders | {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
params?: HttpParams | {
|
||||
[param: string]: string | string[]
|
||||
}
|
||||
responseType?: 'json' | 'text'
|
||||
withCredentials?: boolean
|
||||
body?: any
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
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.
|
||||
interval(timeout).pipe(take(1), map(() => { throw new Error('timeout') })),
|
||||
)
|
||||
}
|
||||
28
frontend/projects/ui/src/app/services/modal.service.ts
Normal file
28
frontend/projects/ui/src/app/services/modal.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { DependentInfo } from 'src/app/util/misc.util'
|
||||
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ModalService {
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly modalCtrl: ModalController,
|
||||
) {}
|
||||
|
||||
async presentModalConfig(componentProps: ComponentProps): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppConfigPage,
|
||||
componentProps,
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
|
||||
interface ComponentProps {
|
||||
pkgId: string
|
||||
dependentInfo?: DependentInfo
|
||||
}
|
||||
376
frontend/projects/ui/src/app/services/patch-db/data-model.ts
Normal file
376
frontend/projects/ui/src/app/services/patch-db/data-model.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
|
||||
export interface DataModel {
|
||||
'server-info': ServerInfo
|
||||
'package-data': { [id: string]: PackageDataEntry }
|
||||
'recovered-packages': { [id: string]: RecoveredPackageDataEntry }
|
||||
ui: UIData
|
||||
}
|
||||
|
||||
export interface UIData {
|
||||
name: string
|
||||
'auto-check-updates': boolean
|
||||
'pkg-order': string[]
|
||||
'ack-welcome': string // EOS version
|
||||
'ack-share-stats': boolean
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
id: string
|
||||
version: string
|
||||
'last-backup': string | null
|
||||
'lan-address': URL
|
||||
'tor-address': URL
|
||||
status: ServerStatus
|
||||
'eos-marketplace': URL
|
||||
'package-marketplace': URL | null // uses EOS marketplace if null
|
||||
'share-stats': boolean
|
||||
'unread-notification-count': number
|
||||
'update-progress'?: {
|
||||
size: number
|
||||
downloaded: number
|
||||
}
|
||||
'eos-version-compat': string
|
||||
'password-hash': string
|
||||
}
|
||||
|
||||
export enum ServerStatus {
|
||||
Running = 'running',
|
||||
Updated = 'updated',
|
||||
BackingUp = 'backing-up',
|
||||
}
|
||||
export interface RecoveredPackageDataEntry {
|
||||
title: string,
|
||||
icon: URL,
|
||||
version: string,
|
||||
}
|
||||
|
||||
export interface PackageDataEntry {
|
||||
state: PackageState
|
||||
'static-files': {
|
||||
license: URL
|
||||
instructions: URL
|
||||
icon: URL
|
||||
}
|
||||
manifest: Manifest
|
||||
installed?: InstalledPackageDataEntry, // exists when: installed, updating
|
||||
'install-progress'?: InstallProgress, // exists when: installing, updating
|
||||
}
|
||||
|
||||
export interface InstallProgress {
|
||||
size: number | null
|
||||
downloaded: number
|
||||
'download-complete': boolean
|
||||
validated: number
|
||||
'validation-complete': boolean
|
||||
unpacked: number
|
||||
'unpack-complete': boolean
|
||||
}
|
||||
|
||||
export interface InstalledPackageDataEntry {
|
||||
status: Status
|
||||
manifest: Manifest,
|
||||
'last-backup': string | null
|
||||
'system-pointers': any[]
|
||||
'current-dependents': { [id: string]: CurrentDependencyInfo }
|
||||
'current-dependencies': { [id: string]: CurrentDependencyInfo }
|
||||
'dependency-info': {
|
||||
[id: string]: {
|
||||
manifest: Manifest
|
||||
icon: URL
|
||||
}
|
||||
}
|
||||
'interface-addresses': {
|
||||
[id: string]: { 'tor-address': string, 'lan-address': string }
|
||||
}
|
||||
}
|
||||
|
||||
export interface CurrentDependencyInfo {
|
||||
pointers: any[]
|
||||
'health-checks': string[] // array of health check IDs
|
||||
}
|
||||
|
||||
export enum PackageState {
|
||||
Installing = 'installing',
|
||||
Installed = 'installed',
|
||||
Updating = 'updating',
|
||||
Removing = 'removing',
|
||||
Restoring = 'restoring',
|
||||
}
|
||||
|
||||
export interface Manifest {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
description: {
|
||||
short: string
|
||||
long: string
|
||||
}
|
||||
'release-notes': string
|
||||
license: string // name
|
||||
'wrapper-repo': URL
|
||||
'upstream-repo': URL
|
||||
'support-site': URL
|
||||
'marketing-site': URL
|
||||
'donation-url': URL | null
|
||||
alerts: {
|
||||
install: string | null
|
||||
uninstall: string | null
|
||||
restore: string | null
|
||||
start: string | null
|
||||
stop: string | null
|
||||
}
|
||||
main: ActionImpl
|
||||
'health-checks': Record<string, ActionImpl & { name: string, description: string }>
|
||||
config: ConfigActions | null
|
||||
volumes: Record<string, Volume>
|
||||
'min-os-version': string
|
||||
interfaces: Record<string, InterfaceDef>
|
||||
backup: BackupActions
|
||||
migrations: Migrations
|
||||
actions: Record<string, Action>
|
||||
permissions: any // @TODO 0.3.1
|
||||
dependencies: DependencyInfo
|
||||
}
|
||||
|
||||
export interface ActionImpl {
|
||||
type: 'docker'
|
||||
image: string
|
||||
system: boolean
|
||||
entrypoint: string
|
||||
args: string[]
|
||||
mounts: { [id: string]: string }
|
||||
'io-format': DockerIoFormat | null
|
||||
inject: boolean
|
||||
'shm-size': string
|
||||
'sigterm-timeout': string | null
|
||||
}
|
||||
|
||||
export enum DockerIoFormat {
|
||||
Json = 'json',
|
||||
Yaml = 'yaml',
|
||||
Cbor = 'cbor',
|
||||
Toml = 'toml',
|
||||
}
|
||||
|
||||
export interface ConfigActions {
|
||||
get: ActionImpl
|
||||
set: ActionImpl
|
||||
}
|
||||
|
||||
export type Volume = VolumeData
|
||||
|
||||
export interface VolumeData {
|
||||
type: VolumeType.Data
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
export interface VolumeAssets {
|
||||
type: VolumeType.Assets
|
||||
}
|
||||
|
||||
export interface VolumePointer {
|
||||
type: VolumeType.Pointer
|
||||
'package-id': string
|
||||
'volume-id': string
|
||||
path: string
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
export interface VolumeCertificate {
|
||||
type: VolumeType.Certificate
|
||||
'interface-id': string
|
||||
}
|
||||
|
||||
export interface VolumeBackup {
|
||||
type: VolumeType.Backup
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
export enum VolumeType {
|
||||
Data = 'data',
|
||||
Assets = 'assets',
|
||||
Pointer = 'pointer',
|
||||
Certificate = 'certificate',
|
||||
Backup = 'backup',
|
||||
}
|
||||
|
||||
export interface InterfaceDef {
|
||||
name: string
|
||||
description: string
|
||||
'tor-config': TorConfig | null
|
||||
'lan-config': LanConfig | null
|
||||
ui: boolean
|
||||
protocols: string[]
|
||||
}
|
||||
|
||||
export interface TorConfig {
|
||||
'port-mapping': { [port: number]: number }
|
||||
}
|
||||
|
||||
export type LanConfig = {
|
||||
[port: number]: { ssl: boolean, mapping: number }
|
||||
}
|
||||
|
||||
export interface BackupActions {
|
||||
create: ActionImpl
|
||||
restore: ActionImpl
|
||||
}
|
||||
|
||||
export interface Migrations {
|
||||
from: { [versionRange: string]: ActionImpl }
|
||||
to: { [versionRange: string]: ActionImpl }
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
name: string
|
||||
description: string
|
||||
warning: string | null
|
||||
implementation: ActionImpl
|
||||
'allowed-statuses': (PackageMainStatus.Stopped | PackageMainStatus.Running)[]
|
||||
'input-spec': ConfigSpec
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
configured: boolean
|
||||
main: MainStatus
|
||||
'dependency-errors': { [id: string]: DependencyError | null }
|
||||
}
|
||||
|
||||
export type MainStatus = MainStatusStopped | MainStatusStopping | MainStatusStarting | MainStatusRunning | MainStatusBackingUp
|
||||
|
||||
export interface MainStatusStopped {
|
||||
status: PackageMainStatus.Stopped
|
||||
}
|
||||
|
||||
export interface MainStatusStopping {
|
||||
status: PackageMainStatus.Stopping
|
||||
}
|
||||
|
||||
export interface MainStatusStarting {
|
||||
status: PackageMainStatus.Starting
|
||||
}
|
||||
|
||||
export interface MainStatusRunning {
|
||||
status: PackageMainStatus.Running
|
||||
started: string // UTC date string
|
||||
health: { [id: string]: HealthCheckResult }
|
||||
}
|
||||
|
||||
export interface MainStatusBackingUp {
|
||||
status: PackageMainStatus.BackingUp
|
||||
started: string | null // UTC date string
|
||||
}
|
||||
|
||||
export enum PackageMainStatus {
|
||||
Starting = 'starting',
|
||||
Running = 'running',
|
||||
Stopping = 'stopping',
|
||||
Stopped = 'stopped',
|
||||
BackingUp = 'backing-up',
|
||||
}
|
||||
|
||||
export type HealthCheckResult = HealthCheckResultStarting |
|
||||
HealthCheckResultLoading |
|
||||
HealthCheckResultDisabled |
|
||||
HealthCheckResultSuccess |
|
||||
HealthCheckResultFailure
|
||||
|
||||
export enum HealthResult {
|
||||
Starting = 'starting',
|
||||
Loading = 'loading',
|
||||
Disabled = 'disabled',
|
||||
Success = 'success',
|
||||
Failure = 'failure',
|
||||
}
|
||||
|
||||
export interface HealthCheckResultStarting {
|
||||
result: HealthResult.Starting
|
||||
}
|
||||
|
||||
export interface HealthCheckResultDisabled {
|
||||
result: HealthResult.Disabled
|
||||
}
|
||||
|
||||
export interface HealthCheckResultSuccess {
|
||||
result: HealthResult.Success
|
||||
}
|
||||
|
||||
export interface HealthCheckResultLoading {
|
||||
result: HealthResult.Loading
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface HealthCheckResultFailure {
|
||||
result: HealthResult.Failure
|
||||
error: string
|
||||
}
|
||||
|
||||
export type DependencyError = DependencyErrorNotInstalled |
|
||||
DependencyErrorNotRunning |
|
||||
DependencyErrorIncorrectVersion |
|
||||
DependencyErrorConfigUnsatisfied |
|
||||
DependencyErrorHealthChecksFailed |
|
||||
DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'not-installed',
|
||||
NotRunning = 'not-running',
|
||||
IncorrectVersion = 'incorrect-version',
|
||||
ConfigUnsatisfied = 'config-unsatisfied',
|
||||
HealthChecksFailed = 'health-checks-failed',
|
||||
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
error: string
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
check: HealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
|
||||
export interface DependencyInfo {
|
||||
[id: string]: DependencyEntry
|
||||
}
|
||||
|
||||
export interface DependencyEntry {
|
||||
version: string
|
||||
requirement: {
|
||||
type: 'opt-in'
|
||||
how: string
|
||||
} | {
|
||||
type: 'opt-out'
|
||||
how: string
|
||||
} | {
|
||||
type: 'required'
|
||||
}
|
||||
description: string | null
|
||||
config: {
|
||||
check: ActionImpl,
|
||||
'auto-configure': ActionImpl
|
||||
}
|
||||
}
|
||||
|
||||
export type URL = string
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Bootstrapper, DBCache } from 'patch-db-client'
|
||||
import { DataModel } from './data-model'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LocalStorageBootstrap implements Bootstrapper<DataModel> {
|
||||
static CONTENT_KEY = 'patch-db-cache'
|
||||
|
||||
constructor (
|
||||
private readonly storage: Storage,
|
||||
) { }
|
||||
|
||||
async init (): Promise<DBCache<DataModel>> {
|
||||
const cache: DBCache<DataModel> = await this.storage.get(LocalStorageBootstrap.CONTENT_KEY)
|
||||
return cache || { sequence: 0, data: { } as DataModel }
|
||||
}
|
||||
|
||||
async update (cache: DBCache<DataModel>): Promise<void> {
|
||||
await this.storage.set(LocalStorageBootstrap.CONTENT_KEY, cache)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { MockSource, PollSource, WebsocketSource } from 'patch-db-client'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from './data-model'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
import { PatchDbService } from './patch-db.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { MockApiService } from '../api/embassy-mock-api.service'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { exists } from 'src/app/util/misc.util'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
|
||||
export function PatchDbServiceFactory (
|
||||
config: ConfigService,
|
||||
embassyApi: ApiService,
|
||||
bootstrapper: LocalStorageBootstrap,
|
||||
auth: AuthService,
|
||||
storage: Storage,
|
||||
): PatchDbService {
|
||||
|
||||
const { useMocks, patchDb: { poll } } = config
|
||||
|
||||
if (useMocks) {
|
||||
const source = new MockSource<DataModel>(
|
||||
(embassyApi as MockApiService).mockPatch$.pipe(filter(exists)),
|
||||
)
|
||||
return new PatchDbService(
|
||||
source,
|
||||
source,
|
||||
embassyApi,
|
||||
bootstrapper,
|
||||
auth,
|
||||
storage,
|
||||
)
|
||||
} else {
|
||||
const protocol = window.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = window.location.host
|
||||
const wsSource = new WebsocketSource<DataModel>(
|
||||
`${protocol}://${host}/ws/db`,
|
||||
)
|
||||
const pollSource = new PollSource<DataModel>({ ...poll }, embassyApi)
|
||||
|
||||
return new PatchDbService(
|
||||
wsSource,
|
||||
pollSource,
|
||||
embassyApi,
|
||||
bootstrapper,
|
||||
auth,
|
||||
storage,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Inject, Injectable, InjectionToken } from '@angular/core'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client'
|
||||
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
finalize,
|
||||
mergeMap,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators'
|
||||
import { isEmptyObject, pauseFor } from 'src/app/util/misc.util'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DataModel } from './data-model'
|
||||
|
||||
export const PATCH_HTTP = new InjectionToken<Source<DataModel>>('')
|
||||
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>>('')
|
||||
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('')
|
||||
export const AUTH = new InjectionToken<AuthService>('')
|
||||
export const STORAGE = new InjectionToken<Storage>('')
|
||||
|
||||
export enum PatchConnection {
|
||||
Initializing = 'initializing',
|
||||
Connected = 'connected',
|
||||
Disconnected = 'disconnected',
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchDbService {
|
||||
private readonly WS_SUCCESS = 'wsSuccess'
|
||||
private patchConnection$ = new BehaviorSubject(PatchConnection.Initializing)
|
||||
private wsSuccess$ = new BehaviorSubject(false)
|
||||
private polling$ = new BehaviorSubject(false)
|
||||
private patchDb: PatchDB<DataModel>
|
||||
private subs: Subscription[] = []
|
||||
private sources$: BehaviorSubject<Source<DataModel>[]> = new BehaviorSubject([
|
||||
this.wsSource,
|
||||
])
|
||||
|
||||
data: DataModel
|
||||
errors = 0
|
||||
|
||||
getData () {
|
||||
return this.patchDb.store.cache.data
|
||||
}
|
||||
|
||||
get loaded (): boolean {
|
||||
return this.patchDb?.store?.cache?.data && !isEmptyObject(this.patchDb.store.cache.data)
|
||||
}
|
||||
|
||||
constructor (
|
||||
@Inject(PATCH_SOURCE) private readonly wsSource: Source<DataModel>,
|
||||
@Inject(PATCH_SOURCE) private readonly pollSource: Source<DataModel>,
|
||||
@Inject(PATCH_HTTP) private readonly http: ApiService,
|
||||
@Inject(BOOTSTRAPPER)
|
||||
private readonly bootstrapper: Bootstrapper<DataModel>,
|
||||
@Inject(AUTH) private readonly auth: AuthService,
|
||||
@Inject(STORAGE) private readonly storage: Storage,
|
||||
) { }
|
||||
|
||||
async init (): Promise<void> {
|
||||
const cache = await this.bootstrapper.init()
|
||||
this.sources$.next([this.wsSource, this.http])
|
||||
|
||||
this.patchDb = new PatchDB(this.sources$, this.http, cache)
|
||||
|
||||
this.patchConnection$.next(PatchConnection.Initializing)
|
||||
this.data = this.patchDb.store.cache.data
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
await this.init()
|
||||
|
||||
this.subs.push(
|
||||
// Connection Error
|
||||
this.patchDb.connectionError$
|
||||
.pipe(
|
||||
debounceTime(420),
|
||||
withLatestFrom(this.polling$),
|
||||
mergeMap(async ([e, polling]) => {
|
||||
if (polling) {
|
||||
console.log('patchDB: POLLING FAILED', e)
|
||||
this.patchConnection$.next(PatchConnection.Disconnected)
|
||||
await pauseFor(2000)
|
||||
this.sources$.next([this.pollSource, this.http])
|
||||
return
|
||||
}
|
||||
|
||||
console.log('patchDB: WEBSOCKET FAILED', e)
|
||||
this.polling$.next(true)
|
||||
this.sources$.next([this.pollSource, this.http])
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
console.warn('patchDB: SYNC COMPLETE')
|
||||
},
|
||||
}),
|
||||
|
||||
// RPC ERROR
|
||||
this.patchDb.rpcError$
|
||||
.pipe(
|
||||
tap(({ error }) => {
|
||||
if (error.code === 34) {
|
||||
console.log('patchDB: Unauthorized. Logging out.')
|
||||
this.auth.setUnverified()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
console.warn('patchDB: SYNC COMPLETE')
|
||||
},
|
||||
}),
|
||||
|
||||
// GOOD CONNECTION
|
||||
this.patchDb.cache$
|
||||
.pipe(
|
||||
debounceTime(420),
|
||||
withLatestFrom(this.patchConnection$, this.wsSuccess$, this.polling$),
|
||||
tap(async ([cache, connection, wsSuccess, polling]) => {
|
||||
this.bootstrapper.update(cache)
|
||||
|
||||
if (connection === PatchConnection.Initializing) {
|
||||
console.log(
|
||||
polling
|
||||
? 'patchDB: POLL CONNECTED'
|
||||
: 'patchDB: WEBSOCKET CONNECTED',
|
||||
)
|
||||
this.patchConnection$.next(PatchConnection.Connected)
|
||||
if (!wsSuccess && !polling) {
|
||||
console.log('patchDB: WEBSOCKET SUCCESS')
|
||||
this.storage.set(this.WS_SUCCESS, 'true')
|
||||
this.wsSuccess$.next(true)
|
||||
}
|
||||
} else if (
|
||||
connection === PatchConnection.Disconnected &&
|
||||
wsSuccess
|
||||
) {
|
||||
console.log('patchDB: SWITCHING BACK TO WEBSOCKETS')
|
||||
this.patchConnection$.next(PatchConnection.Initializing)
|
||||
this.polling$.next(false)
|
||||
this.sources$.next([this.wsSource, this.http])
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
console.warn('patchDB: SYNC COMPLETE')
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
stop (): void {
|
||||
if (this.patchDb) {
|
||||
console.log('patchDB: STOPPING')
|
||||
this.patchConnection$.next(PatchConnection.Initializing)
|
||||
this.patchDb.store.reset()
|
||||
}
|
||||
this.subs.forEach((x) => x.unsubscribe())
|
||||
this.subs = []
|
||||
}
|
||||
|
||||
watchPatchConnection$ (): Observable<PatchConnection> {
|
||||
return this.patchConnection$.asObservable()
|
||||
}
|
||||
|
||||
watch$: Store<DataModel>['watch$'] = (...args: (string | number)[]): Observable<DataModel> => {
|
||||
const argsString = '/' + args.join('/')
|
||||
console.log('patchDB: WATCHING ', argsString)
|
||||
return this.patchDb.store.watch$(...(args as [])).pipe(
|
||||
tap((data) => console.log('patchDB: NEW VALUE', argsString, data)),
|
||||
catchError((e) => {
|
||||
console.error('patchDB: WATCH ERROR', e)
|
||||
return of(e.message)
|
||||
}),
|
||||
finalize(() => console.log('patchDB: UNSUBSCRIBING', argsString)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { isEmptyObject } from '../util/misc.util'
|
||||
import {
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
PackageState,
|
||||
Status,
|
||||
} from './patch-db/data-model'
|
||||
|
||||
export interface PackageStatus {
|
||||
primary: PrimaryStatus
|
||||
dependency: DependencyStatus | null
|
||||
health: HealthStatus | null
|
||||
}
|
||||
|
||||
export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
|
||||
let primary: PrimaryStatus
|
||||
let dependency: DependencyStatus | null = null
|
||||
let health: HealthStatus | null = null
|
||||
|
||||
if (pkg.state === PackageState.Installed) {
|
||||
primary = getPrimaryStatus(pkg.installed.status)
|
||||
dependency = getDependencyStatus(pkg)
|
||||
health = getHealthStatus(pkg.installed.status)
|
||||
} else {
|
||||
primary = pkg.state as string as PrimaryStatus
|
||||
}
|
||||
|
||||
return { primary, dependency, health }
|
||||
}
|
||||
|
||||
function getPrimaryStatus(status: Status): PrimaryStatus {
|
||||
if (!status.configured) {
|
||||
return PrimaryStatus.NeedsConfig
|
||||
} else {
|
||||
return status.main.status as any as PrimaryStatus
|
||||
}
|
||||
}
|
||||
|
||||
function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus {
|
||||
const installed = pkg.installed
|
||||
if (isEmptyObject(installed['current-dependencies'])) return null
|
||||
|
||||
const depErrors = installed.status['dependency-errors']
|
||||
const depIds = Object.keys(depErrors).filter(key => !!depErrors[key])
|
||||
|
||||
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
|
||||
}
|
||||
|
||||
function getHealthStatus(status: Status): HealthStatus {
|
||||
if (status.main.status === PackageMainStatus.Running) {
|
||||
const values = Object.values(status.main.health)
|
||||
if (values.some(h => h.result === 'failure')) {
|
||||
return HealthStatus.Failure
|
||||
} else if (values.some(h => h.result === 'starting')) {
|
||||
return HealthStatus.Starting
|
||||
} else if (values.some(h => h.result === 'loading')) {
|
||||
return HealthStatus.Loading
|
||||
} else {
|
||||
return HealthStatus.Healthy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface StatusRendering {
|
||||
display: string
|
||||
color: string
|
||||
showDots?: boolean
|
||||
}
|
||||
|
||||
export enum PrimaryStatus {
|
||||
// state
|
||||
Installing = 'installing',
|
||||
Updating = 'updating',
|
||||
Removing = 'removing',
|
||||
Restoring = 'restoring',
|
||||
// status
|
||||
Starting = 'starting',
|
||||
Running = 'running',
|
||||
Stopping = 'stopping',
|
||||
Stopped = 'stopped',
|
||||
BackingUp = 'backing-up',
|
||||
// config
|
||||
NeedsConfig = 'needs-config',
|
||||
}
|
||||
|
||||
export enum DependencyStatus {
|
||||
Warning = 'warning',
|
||||
Satisfied = 'satisfied',
|
||||
}
|
||||
|
||||
export enum HealthStatus {
|
||||
Failure = 'failure',
|
||||
Starting = 'starting',
|
||||
Loading = 'loading',
|
||||
Healthy = 'healthy',
|
||||
}
|
||||
|
||||
export const PrimaryRendering: { [key: string]: StatusRendering } = {
|
||||
[PrimaryStatus.Installing]: { display: 'Installing', color: 'primary', showDots: true },
|
||||
[PrimaryStatus.Updating]: { display: 'Updating', color: 'primary', showDots: true },
|
||||
[PrimaryStatus.Removing]: { display: 'Removing', color: 'danger', showDots: true },
|
||||
[PrimaryStatus.Restoring]: { display: 'Restoring', color: 'primary', showDots: true },
|
||||
[PrimaryStatus.Stopping]: { display: 'Stopping', color: 'dark-shade', showDots: true },
|
||||
[PrimaryStatus.Stopped]: { display: 'Stopped', color: 'dark-shade', showDots: false },
|
||||
[PrimaryStatus.BackingUp]: { display: 'Backing Up', color: 'primary', showDots: true },
|
||||
[PrimaryStatus.Starting]: { display: 'Starting', color: 'primary', showDots: true },
|
||||
[PrimaryStatus.Running]: { display: 'Running', color: 'success', showDots: false },
|
||||
[PrimaryStatus.NeedsConfig]: { display: 'Needs Config', color: 'warning' },
|
||||
}
|
||||
|
||||
export const DependencyRendering: { [key: string]: StatusRendering } = {
|
||||
[DependencyStatus.Warning]: { display: 'Issue', color: 'warning' },
|
||||
[DependencyStatus.Satisfied]: { display: 'Satisfied', color: 'success' },
|
||||
}
|
||||
|
||||
export const HealthRendering: { [key: string]: StatusRendering } = {
|
||||
[HealthStatus.Failure]: { display: 'Failure', color: 'danger' },
|
||||
[HealthStatus.Starting]: { display: 'Starting', color: 'primary' },
|
||||
[HealthStatus.Loading]: { display: 'Loading', color: 'primary' },
|
||||
[HealthStatus.Healthy]: { display: 'Healthy', color: 'success' },
|
||||
}
|
||||
163
frontend/projects/ui/src/app/services/server-config.service.ts
Normal file
163
frontend/projects/ui/src/app/services/server-config.service.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AlertInput, AlertButton, IonicSafeString } from '@ionic/core'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { ConfigSpec } from '../pkg-config/config-types'
|
||||
import { AlertController, LoadingController } from '@ionic/angular'
|
||||
import { ErrorToastService } from './error-toast.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ServerConfigService {
|
||||
|
||||
constructor (
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly embassyApi: ApiService,
|
||||
) { }
|
||||
|
||||
async presentAlert (key: string, current?: any): Promise<HTMLIonAlertElement> {
|
||||
const spec = serverConfig[key]
|
||||
|
||||
let inputs: AlertInput[]
|
||||
let buttons: AlertButton[] = [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async (data: any) => {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Saving...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
loader.present()
|
||||
|
||||
try {
|
||||
await this.saveFns[key](data)
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
]
|
||||
|
||||
switch (spec.type) {
|
||||
case 'boolean':
|
||||
inputs = [
|
||||
{
|
||||
name: 'enabled',
|
||||
type: 'radio',
|
||||
label: 'Enabled',
|
||||
value: true,
|
||||
checked: current,
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
type: 'radio',
|
||||
label: 'Disabled',
|
||||
value: false,
|
||||
checked: !current,
|
||||
},
|
||||
]
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: spec.name,
|
||||
message: spec.description,
|
||||
inputs,
|
||||
buttons,
|
||||
})
|
||||
await alert.present()
|
||||
return alert
|
||||
}
|
||||
|
||||
// async presentModalForm (key: string) {
|
||||
// const modal = await this.modalCtrl.create({
|
||||
// component: AppActionInputPage,
|
||||
// componentProps: {
|
||||
// title: serverConfig[key].name,
|
||||
// spec: (serverConfig[key] as ValueSpecObject).spec,
|
||||
// },
|
||||
// })
|
||||
|
||||
// modal.onWillDismiss().then(res => {
|
||||
// if (!res.data) return
|
||||
// this.saveFns[key](res.data)
|
||||
// })
|
||||
|
||||
// await modal.present()
|
||||
// }
|
||||
|
||||
saveFns: { [key: string]: (val: any) => Promise<any> } = {
|
||||
'auto-check-updates': async (enabled: boolean) => {
|
||||
return this.embassyApi.setDbValue({ pointer: '/auto-check-updates', value: enabled })
|
||||
},
|
||||
// 'eos-marketplace': async () => {
|
||||
// return this.embassyApi.setEosMarketplace()
|
||||
// },
|
||||
// 'package-marketplace': async (url: string) => {
|
||||
// return this.embassyApi.setPackageMarketplace({ url })
|
||||
// },
|
||||
'share-stats': async (enabled: boolean) => {
|
||||
return this.embassyApi.setShareStats({ value: enabled })
|
||||
},
|
||||
// password: async (password: string) => {
|
||||
// return this.embassyApi.updatePassword({ password })
|
||||
// },
|
||||
}
|
||||
}
|
||||
|
||||
export const serverConfig: ConfigSpec = {
|
||||
'auto-check-updates': {
|
||||
type: 'boolean',
|
||||
name: 'Auto Check for Updates',
|
||||
description: 'If enabled, EmbassyOS will automatically check for updates of itself and any installed services. Updating will still require your approval and action. Updates will never be performed automatically.',
|
||||
default: true,
|
||||
},
|
||||
// 'eos-marketplace': {
|
||||
// type: 'boolean',
|
||||
// name: 'Tor Only Marketplace',
|
||||
// description: `Use Start9's Tor (instead of clearnet) Marketplace.`,
|
||||
// warning: 'This will result in higher latency and slower download times.',
|
||||
// default: false,
|
||||
// },
|
||||
// 'package-marketplace': {
|
||||
// type: 'string',
|
||||
// name: 'Package Marketplace',
|
||||
// description: `Use for alternative embassy marketplace. Leave empty to use start9's marketplace.`,
|
||||
// nullable: true,
|
||||
// // @TODO regex for URL
|
||||
// // pattern: '',
|
||||
// 'pattern-description': 'Must be a valid URL.',
|
||||
// masked: false,
|
||||
// copyable: false,
|
||||
// },
|
||||
'share-stats': {
|
||||
type: 'boolean',
|
||||
name: 'Report Bugs',
|
||||
description: new IonicSafeString(`If enabled, generic error codes will be anonymously transmitted over Tor to the Start9 team. This helps us identify and fix bugs quickly. <a href="https://docs.start9.com/user-manual/general/user-preferences/report-bugs.html" target="_blank" rel="noreferrer">Read more</a> `) as any,
|
||||
default: false,
|
||||
},
|
||||
// password: {
|
||||
// type: 'string',
|
||||
// name: 'Change Password',
|
||||
// description: `Your Embassy's master password, used for authentication and disk encryption.`,
|
||||
// nullable: false,
|
||||
// // @TODO regex for 12 chars
|
||||
// // pattern: '',
|
||||
// 'pattern-description': 'Must contain at least 12 characters.',
|
||||
// warning: 'If you forget your master password, there is absolutely no way to recover your data. This can result in loss of money! Keep in mind, old backups will still be encrypted by the password used to encrypt them.',
|
||||
// masked: false,
|
||||
// copyable: false,
|
||||
// },
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SplitPaneTracker {
|
||||
sidebarOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||
}
|
||||
253
frontend/projects/ui/src/app/services/startup-alerts.service.ts
Normal file
253
frontend/projects/ui/src/app/services/startup-alerts.service.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AlertController, IonicSafeString, ModalController, NavController } from '@ionic/angular'
|
||||
import { wizardModal } from '../components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from '../components/install-wizard/prebaked-wizards'
|
||||
import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
|
||||
import { displayEmver } from '../pipes/emver.pipe'
|
||||
import { RR } from './api/api.types'
|
||||
import { ConfigService } from './config.service'
|
||||
import { Emver } from './emver.service'
|
||||
import { MarketplaceService } from '../pages/marketplace-routes/marketplace.service'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { PatchDbService } from './patch-db/patch-db.service'
|
||||
import { filter, take } from 'rxjs/operators'
|
||||
import { isEmptyObject } from '../util/misc.util'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ServerConfigService } from './server-config.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StartupAlertsService {
|
||||
private checks: Check<any>[]
|
||||
|
||||
constructor (
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly config: ConfigService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly api: ApiService,
|
||||
private readonly emver: Emver,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly serverConfig: ServerConfigService,
|
||||
) {
|
||||
const osWelcome: Check<boolean> = {
|
||||
name: 'osWelcome',
|
||||
shouldRun: () => this.shouldRunOsWelcome(),
|
||||
check: async () => true,
|
||||
display: () => this.displayOsWelcome(),
|
||||
}
|
||||
const shareStats: Check<boolean> = {
|
||||
name: 'shareStats',
|
||||
shouldRun: () => this.shouldRunShareStats(),
|
||||
check: async () => true,
|
||||
display: () => this.displayShareStats(),
|
||||
}
|
||||
const osUpdate: Check<RR.GetMarketplaceEOSRes | undefined> = {
|
||||
name: 'osUpdate',
|
||||
shouldRun: () => this.shouldRunOsUpdateCheck(),
|
||||
check: () => this.osUpdateCheck(),
|
||||
display: pkg => this.displayOsUpdateCheck(pkg),
|
||||
}
|
||||
const pkgsUpdate: Check<boolean> = {
|
||||
name: 'pkgsUpdate',
|
||||
shouldRun: () => this.shouldRunAppsCheck(),
|
||||
check: () => this.appsCheck(),
|
||||
display: () => this.displayAppsCheck(),
|
||||
}
|
||||
this.checks = [osWelcome, shareStats, osUpdate, pkgsUpdate]
|
||||
}
|
||||
|
||||
// This takes our three checks and filters down to those that should run.
|
||||
// Then, the reduce fires, quickly iterating through yielding a promise (previousDisplay) to the next element
|
||||
// Each promise fires more or less concurrently, so each c.check(server) is run concurrently
|
||||
// Then, since we await previousDisplay before c.display(res), each promise executing gets hung awaiting the display of the previous run
|
||||
runChecks (): Subscription {
|
||||
return this.patch.watch$()
|
||||
.pipe(
|
||||
filter(data => !isEmptyObject(data)),
|
||||
take(1),
|
||||
)
|
||||
.subscribe(async () => {
|
||||
await this.checks
|
||||
.filter(c => !this.config.skipStartupAlerts && c.shouldRun())
|
||||
// returning true in the below block means to continue to next modal
|
||||
// returning false means to skip all subsequent modals
|
||||
.reduce(async (previousDisplay, c) => {
|
||||
let checkRes: any
|
||||
try {
|
||||
checkRes = await c.check()
|
||||
} catch (e) {
|
||||
console.error(`Exception in ${c.name} check:`, e)
|
||||
return true
|
||||
}
|
||||
|
||||
const displayRes = await previousDisplay
|
||||
|
||||
if (!checkRes) return true
|
||||
if (displayRes) return c.display(checkRes)
|
||||
}, Promise.resolve(true))
|
||||
})
|
||||
}
|
||||
|
||||
// ** should run **
|
||||
|
||||
private shouldRunOsWelcome (): boolean {
|
||||
return this.patch.getData().ui['ack-welcome'] !== this.config.version
|
||||
}
|
||||
private shouldRunShareStats (): boolean {
|
||||
return !this.patch.getData().ui['ack-share-stats']
|
||||
}
|
||||
|
||||
private shouldRunOsUpdateCheck (): boolean {
|
||||
return this.patch.getData().ui['auto-check-updates']
|
||||
}
|
||||
|
||||
private shouldRunAppsCheck (): boolean {
|
||||
return this.patch.getData().ui['auto-check-updates']
|
||||
}
|
||||
|
||||
// ** check **
|
||||
|
||||
private async osUpdateCheck (): Promise<RR.GetMarketplaceEOSRes | undefined> {
|
||||
const res = await this.api.getEos({
|
||||
'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'],
|
||||
})
|
||||
|
||||
if (this.emver.compare(this.config.version, res.version) === -1) {
|
||||
return res
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private async appsCheck (): Promise<boolean> {
|
||||
const updates = await this.marketplaceService.getUpdates(this.patch.getData()['package-data'])
|
||||
return !!updates.length
|
||||
}
|
||||
|
||||
// ** display **
|
||||
|
||||
private async displayOsWelcome (): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: OSWelcomePage,
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
componentProps: {
|
||||
version: this.config.version,
|
||||
},
|
||||
})
|
||||
modal.onWillDismiss().then(() => {
|
||||
this.api.setDbValue({ pointer: '/ack-welcome', value: this.config.version })
|
||||
.catch()
|
||||
return resolve(true)
|
||||
})
|
||||
await modal.present()
|
||||
})
|
||||
}
|
||||
|
||||
private async displayShareStats (): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.serverConfig.presentAlert('share-stats', this.patch.getData()['server-info']['share-stats'])
|
||||
|
||||
alert.onDidDismiss().then(() => {
|
||||
this.api.setDbValue({ pointer: '/ack-share-stats', value: this.config.version })
|
||||
.catch()
|
||||
return resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async displayOsUpdateCheck (eos: RR.GetMarketplaceEOSRes): Promise<boolean> {
|
||||
const { update } = await this.presentAlertNewOS(eos.version)
|
||||
if (update) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.updateOS({
|
||||
version: eos.version,
|
||||
headline: eos.headline,
|
||||
releaseNotes: eos['release-notes'],
|
||||
}),
|
||||
)
|
||||
if (cancelled) return true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private async displayAppsCheck (): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Updates Available!',
|
||||
message: new IonicSafeString(
|
||||
`<div style="display: flex; flex-direction: column; justify-content: space-around; min-height: 100px">
|
||||
<div>New service updates are available in the Marketplace.</div>
|
||||
<div style="font-size:x-small">You can disable these checks in your Embassy Config</div>
|
||||
</div>
|
||||
`,
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
handler: () => resolve(true),
|
||||
},
|
||||
{
|
||||
text: 'View in Marketplace',
|
||||
handler: () => {
|
||||
this.navCtrl.navigateForward('/marketplace').then(() => resolve(false))
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
})
|
||||
}
|
||||
|
||||
// more
|
||||
|
||||
private async presentAlertNewOS (versionLatest: string): Promise<{ cancel?: true, update?: true }> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'New EmbassyOS Version!',
|
||||
message: new IonicSafeString(
|
||||
`<div style="display: flex; flex-direction: column; justify-content: space-around; min-height: 100px">
|
||||
<div>Update EmbassyOS to version ${displayEmver(versionLatest)}?</div>
|
||||
<div style="font-size:x-small">You can disable these checks in your Embassy Config</div>
|
||||
</div>
|
||||
`,
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Not now',
|
||||
role: 'cancel',
|
||||
handler: () => resolve({ cancel: true }),
|
||||
},
|
||||
{
|
||||
text: 'Update',
|
||||
handler: () => resolve({ update: true }),
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type Check<T> = {
|
||||
// validates whether a check should run based on server properties
|
||||
shouldRun: () => boolean
|
||||
// executes a check, often requiring api call. It should return a false-y value if there should be no display.
|
||||
check: () => Promise<T>
|
||||
// display an alert based on the result of the check.
|
||||
// return false if subsequent modals should not be displayed
|
||||
display: (a: T) => Promise<boolean>
|
||||
// for logging purposes
|
||||
name: string
|
||||
}
|
||||
22
frontend/projects/ui/src/app/services/ui-launcher.service.ts
Normal file
22
frontend/projects/ui/src/app/services/ui-launcher.service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { PackageDataEntry } from './patch-db/data-model'
|
||||
import { ConfigService } from './config.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UiLauncherService {
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
launch(pkg: PackageDataEntry): void {
|
||||
this.document.defaultView.open(
|
||||
this.config.launchableURL(pkg),
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user