mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 06:19:44 +00:00
0.3.0 refactor
ui: adds overlay layer to patch-db-client ui: getting towards mocks ui: cleans up factory init ui: nice type hack ui: live api for patch ui: api service source + http starts up ui: api source + http ui: rework patchdb config, pass stashTimeout into patchDbModel wires in temp patching into api service ui: example of wiring patchdbmodel into page begin integration remove unnecessary method linting first data rendering rework app initialization http source working for ssh delete call temp patches working entire Embassy tab complete not in kansas anymore ripping, saving progress progress for API request response types and endoint defs Update data-model.ts shambles, but in a good way progress big progress progress installed list working big progress progress progress begin marketplace redesign Update api-types.ts Update api-types.ts marketplace improvements cosmetic dependencies and recommendations begin nym auth approach install wizard restore flow and donations
This commit is contained in:
committed by
Aiden McClelland
parent
fd685ae32c
commit
594d93eb3b
@@ -1,107 +0,0 @@
|
||||
|
||||
//////////////// Install/Uninstall ////////////////////////////////////////////////
|
||||
|
||||
type AppDependentBreakage = {
|
||||
// id of the dependent app which will or did break (Stopped) given the action.
|
||||
id: string
|
||||
title: string
|
||||
iconUrl: string
|
||||
}
|
||||
|
||||
POST /apps/:appId/install(?dryrun)
|
||||
|
||||
body: {
|
||||
version: string, //semver
|
||||
}
|
||||
response : ApiAppInstalledFull & { breakages: AppDependentBreakage[] }
|
||||
|
||||
|
||||
|
||||
POST /apps/:appId/uninstall(?dryrun)
|
||||
|
||||
response : { breakages: AppDependentBreakage[] }
|
||||
|
||||
/////////////////////////////// Store/Show /////////////////////////////////////////////////
|
||||
|
||||
|
||||
type ApiAppAvailableFull = ... {
|
||||
// app base data
|
||||
id: string
|
||||
title: string
|
||||
status: AppStatus | null
|
||||
versionInstalled: string | null
|
||||
iconURL: string
|
||||
|
||||
// preview data
|
||||
versionLatest: string
|
||||
descriptionShort: string
|
||||
|
||||
// version specific data
|
||||
releaseNotes: string
|
||||
serviceRequirements: AppDependencyRequirement[]
|
||||
|
||||
// other data
|
||||
descriptionLong: string,
|
||||
version: string[],
|
||||
}
|
||||
|
||||
type AppDependencyRequirement = ... {
|
||||
//app base data (minus status + version installed)
|
||||
id: string
|
||||
title: string
|
||||
iconURL: string
|
||||
|
||||
// dependency data
|
||||
optional: string | null
|
||||
default: boolean
|
||||
versionSpec: string
|
||||
description: string | null
|
||||
violation: AppDependencyRequirementViolation | null
|
||||
}
|
||||
|
||||
type AppDependencyRequirementViolation =
|
||||
{ name: 'missing'; suggestedVersion: string; } |
|
||||
{ name: 'incompatible-version'; suggestedVersion: string; } |
|
||||
{ name: 'incompatible-config'; ruleViolations: string; auto-configurable: boolean } | // (auto-configurable for if/when we do that)
|
||||
{ name: 'incompatible-status'; status: AppStatus; }
|
||||
|
||||
|
||||
// Get App Available Full
|
||||
GET /apps/:appId/store
|
||||
|
||||
response: ApiAppAvailableFull
|
||||
|
||||
|
||||
// Get Version Specific Data for an App Available
|
||||
GET /apps/:appId/store/:version
|
||||
|
||||
response: {
|
||||
// version specific data
|
||||
releaseNotes: string
|
||||
serviceRequirements: AppDependencyRequirement[]
|
||||
}
|
||||
|
||||
///////////////////////////// Installed/Show ///////////////////////////////////////////
|
||||
|
||||
|
||||
type ApiAppInstalledFull {
|
||||
// app base data
|
||||
id: string
|
||||
title: string
|
||||
status: AppStatus | null
|
||||
versionInstalled: string | null
|
||||
iconURL: string
|
||||
|
||||
// preview data
|
||||
|
||||
// other data
|
||||
instructions: string | null
|
||||
lastBackup: string | null
|
||||
configuredRequirements: AppDependencyRequirement[] | null // null if not yet configured
|
||||
}
|
||||
|
||||
|
||||
// Get App Installed Full
|
||||
GET /apps/:appId/installed
|
||||
|
||||
reseponse: AppInstalledFull
|
||||
@@ -1,43 +1,307 @@
|
||||
import { ConfigSpec } from 'src/app/app-config/config-types'
|
||||
import { AppAvailableFull, AppInstalledFull, AppInstalledPreview } from 'src/app/models/app-types'
|
||||
import { Rules } from '../../models/app-model'
|
||||
import { SSHFingerprint, ServerStatus, ServerSpecs } from '../../models/server-model'
|
||||
import { Dump, Operation, 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/models/patch-db/data-model'
|
||||
|
||||
/** SERVER **/
|
||||
export module RR {
|
||||
|
||||
export interface ApiServer {
|
||||
name: string
|
||||
status: ServerStatus
|
||||
versionInstalled: string
|
||||
alternativeRegistryUrl: string | null
|
||||
specs: ServerSpecs
|
||||
wifi: {
|
||||
ssids: string[]
|
||||
current: string | null
|
||||
// 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 SubmitPinReq = { pin: string } // auth.pin - unauthed
|
||||
export type SubmitPinRes = null
|
||||
|
||||
export type SubmitPasswordReq = { password: string } // auth.password - unauthed
|
||||
export type SubmitPasswordRes = null
|
||||
|
||||
export type LogoutReq = { } // auth.logout
|
||||
export type LogoutRes = null
|
||||
|
||||
// server
|
||||
|
||||
export type GetServerLogsReq = { before?: string } // server.logs
|
||||
export type GetServerLogsRes = Log[]
|
||||
|
||||
export type GetServerMetricsReq = { } // server.metrics
|
||||
export type GetServerMetricsRes = ServerMetrics
|
||||
|
||||
export type UpdateServerReq = WithExpire<{ }> // server.update
|
||||
export type UpdateServerRes = WithRevision<null>
|
||||
|
||||
export type RestartServerReq = { } // server.restart
|
||||
export type RestartServerRes = null
|
||||
|
||||
export type ShutdownServerReq = { } // server.shutdown
|
||||
export type ShutdownServerRes = null
|
||||
|
||||
// network
|
||||
|
||||
export type RefreshLanReq = { } // network.lan.refresh
|
||||
export type RefreshLanRes = null
|
||||
|
||||
// registry
|
||||
|
||||
export type SetRegistryReq = WithExpire<{ url: string }> // registry.set
|
||||
export type SetRegistryRes = WithRevision<null>
|
||||
|
||||
// notification
|
||||
|
||||
export type GetNotificationsReq = WithExpire<{ page: number, 'per-page': number }> // notification.list
|
||||
export type GetNotificationsRes = WithRevision<ServerNotification<number>[]>
|
||||
|
||||
export type DeleteNotificationReq = { id: string } // notification.delete
|
||||
export type DeleteNotificationRes = null
|
||||
|
||||
// wifi
|
||||
|
||||
export type AddWifiReq = { // wifi.add
|
||||
ssid: string
|
||||
password: string
|
||||
country: string
|
||||
priority: number
|
||||
connect: boolean
|
||||
}
|
||||
ssh: SSHFingerprint[]
|
||||
serverId: string
|
||||
welcomeAck: boolean
|
||||
autoCheckUpdates: boolean
|
||||
export type AddWifiRes = null
|
||||
|
||||
export type ConnectWifiReq = WithExpire<{ ssid: string }> // wifi.connect
|
||||
export type ConnectWifiRes = WithRevision<null>
|
||||
|
||||
export type DeleteWifiReq = WithExpire<{ ssid: string }> // wifi.delete
|
||||
export type DeleteWifiRes = WithRevision<null>
|
||||
|
||||
// ssh
|
||||
|
||||
export type GetSSHKeysReq = { } // ssh.get
|
||||
export type GetSSHKeysRes = SSHKeys
|
||||
|
||||
export type AddSSHKeyReq = { pubkey: string } // ssh.add
|
||||
export type AddSSHKeyRes = SSHKeys
|
||||
|
||||
export type DeleteSSHKeyReq = { hash: string } // ssh.delete
|
||||
export type DeleteSSHKeyRes = null
|
||||
|
||||
// backup
|
||||
|
||||
export type CreateBackupReq = WithExpire<{ logicalname: string, password: string }> // backup.create
|
||||
export type CreateBackupRes = WithRevision<null>
|
||||
|
||||
export type RestoreBackupReq = { logicalname: string, password: string } // backup.restore - unauthed
|
||||
export type RestoreBackupRes = null
|
||||
|
||||
// disk
|
||||
|
||||
export type GetDisksReq = { } // disk.list
|
||||
export type GetDisksRes = DiskInfo
|
||||
|
||||
export type EjectDisksReq = { logicalname: string } // disk.eject
|
||||
export type EjectDisksRes = null
|
||||
|
||||
// package
|
||||
|
||||
export type GetPackagePropertiesReq = { id: string } // package.properties
|
||||
export type GetPackagePropertiesRes<T extends number> = PackagePropertiesVersioned<T>
|
||||
|
||||
export type GetPackageLogsReq = { id: string, before?: string } // package.logs
|
||||
export type GetPackageLogsRes = Log[]
|
||||
|
||||
export type InstallPackageReq = WithExpire<{ id: string, version: string }> // package.install
|
||||
export type InstallPackageRes = WithRevision<null>
|
||||
|
||||
export type DryUpdatePackageReq = { id: string, version: string } // package.update.dry
|
||||
export type DryUpdatePackageRes = BreakageRes
|
||||
|
||||
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 = BreakageRes
|
||||
|
||||
export type SetPackageConfigReq = WithExpire<DrySetPackageConfigReq> // package.config.set
|
||||
export type SetPackageConfigRes = WithRevision<null>
|
||||
|
||||
export type RestorePackageReq = WithExpire<{ id: string, logicalname: string, password: string }> // package.backup.restore
|
||||
export type RestorePackageRes = 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 = BreakageRes
|
||||
|
||||
export type StopPackageReq = WithExpire<{ id: string }> // package.stop
|
||||
export type StopPackageRes = WithRevision<null>
|
||||
|
||||
export type DryRemovePackageReq = RemovePackageReq // package.remove.dry
|
||||
export type DryRemovePackageRes = BreakageRes
|
||||
|
||||
export type RemovePackageReq = WithExpire<{ id: string }> // package.remove
|
||||
export type RemovePackageRes = WithRevision<null>
|
||||
|
||||
export type DryConfigureDependencyReq = { 'dependency-id': string, 'dependent-id': string } // package.dependency.configure.dry
|
||||
export type DryConfigureDependencyRes = object
|
||||
|
||||
|
||||
// marketplace
|
||||
|
||||
export type GetMarketplaceDataReq = { }
|
||||
export type GetMarketplaceDataRes = MarketplaceData
|
||||
|
||||
export type GetMarketplaceEOSReq = { }
|
||||
export type GetMarketplaceEOSRes = MarketplaceEOS
|
||||
|
||||
export type GetAvailableListReq = { category?: string, query?: string, page: number, 'per-page': number }
|
||||
export type GetAvailableListRes = AvailablePreview[]
|
||||
|
||||
export type GetAvailableShowReq = { id: string, version?: string }
|
||||
export type GetAvailableShowRes = AvailableShow
|
||||
}
|
||||
|
||||
/** APPS **/
|
||||
export type ApiAppAvailableFull = Omit<AppAvailableFull, 'versionViewing'>
|
||||
export type WithExpire<T> = { 'expire-id'?: string } & T
|
||||
export type WithRevision<T> = { response: T, revision?: Revision }
|
||||
|
||||
export type ApiAppInstalledPreview = Omit<AppInstalledPreview, 'hasUI' | 'launchable'>
|
||||
export type ApiAppInstalledFull = Omit<AppInstalledFull, 'hasFetchedFull' | 'hasUI' | 'launchable'>
|
||||
|
||||
export interface ApiAppConfig {
|
||||
spec: ConfigSpec
|
||||
config: object | null
|
||||
rules: Rules[]
|
||||
export interface MarketplaceData {
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
/** MISC **/
|
||||
|
||||
export type Unit = { never?: never; } // hack for the unit typ
|
||||
|
||||
export type V1Status = {
|
||||
status: 'nothing' | 'instructions' | 'available'
|
||||
export interface MarketplaceEOS {
|
||||
version: string
|
||||
headline: string
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface AvailablePreview {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
icon: URL
|
||||
descriptionShort: string
|
||||
}
|
||||
|
||||
export interface AvailableShow {
|
||||
icon: URL
|
||||
manifest: Manifest
|
||||
categories: string[]
|
||||
versions: string[]
|
||||
'dependency-metadata': {
|
||||
[id: string]: {
|
||||
title: string
|
||||
icon: URL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface BreakageRes {
|
||||
patch: Operation[],
|
||||
breakages: Breakages
|
||||
}
|
||||
|
||||
export interface Breakages {
|
||||
[id: string]: TaggedDependencyError
|
||||
}
|
||||
|
||||
export interface TaggedDependencyError {
|
||||
dependency: string,
|
||||
error: DependencyError,
|
||||
}
|
||||
|
||||
export interface Log {
|
||||
timestamp: string
|
||||
log: string
|
||||
}
|
||||
|
||||
export interface ActionResponse {
|
||||
message: string
|
||||
value: string | number | boolean | null
|
||||
copyable: boolean
|
||||
qr: boolean
|
||||
}
|
||||
|
||||
export interface ServerMetrics {
|
||||
[key: string]: {
|
||||
[key: string]: {
|
||||
value: string | number | null
|
||||
unit?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DiskInfo {
|
||||
[id: string]: DiskInfoEntry
|
||||
}
|
||||
|
||||
export interface DiskInfoEntry {
|
||||
size: string
|
||||
description: string | null
|
||||
partitions: PartitionInfo
|
||||
}
|
||||
|
||||
export interface PartitionInfo {
|
||||
[logicalname: string]: PartitionInfoEntry
|
||||
}
|
||||
|
||||
export interface PartitionInfoEntry {
|
||||
'is-mounted': boolean // We do not allow backups to mounted partitions
|
||||
size: string | null
|
||||
label: string | null
|
||||
}
|
||||
|
||||
export interface ServerSpecs {
|
||||
[key: string]: string | number
|
||||
}
|
||||
|
||||
export interface SSHKeys {
|
||||
[hash: string]: SSHKeyEntry
|
||||
}
|
||||
|
||||
export interface SSHKeyEntry {
|
||||
alg: string
|
||||
hostname: string
|
||||
hash: string
|
||||
}
|
||||
|
||||
export type ServerNotifications = ServerNotification<any>[]
|
||||
|
||||
export interface ServerNotification<T extends number> {
|
||||
id: string
|
||||
'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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { HttpService } from '../http.service'
|
||||
import { AppModel } from '../../models/app-model'
|
||||
import { MockApiService } from './mock-api.service'
|
||||
import { LiveApiService } from './live-api.service'
|
||||
import { ServerModel } from 'src/app/models/server-model'
|
||||
import { ConfigService } from '../config.service'
|
||||
|
||||
export function ApiServiceFactory (config: ConfigService, http: HttpService, appModel: AppModel, serverModel: ServerModel) {
|
||||
if (config.api.useMocks) {
|
||||
return new MockApiService(appModel, serverModel, config)
|
||||
export function ApiServiceFactory (config: ConfigService, http: HttpService) {
|
||||
if (config.api.mocks) {
|
||||
return new MockApiService(config)
|
||||
} else {
|
||||
return new LiveApiService(http, appModel, serverModel, config)
|
||||
return new LiveApiService(http, config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +1,209 @@
|
||||
import { Rules } from '../../models/app-model'
|
||||
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
|
||||
import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview, V1Status } from './api-types'
|
||||
import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util'
|
||||
import { ConfigSpec } from 'src/app/app-config/config-types'
|
||||
import { Http, Source, Update, Operation, Revision } from 'patch-db-client'
|
||||
import { RR } from './api-types'
|
||||
import { DataModel } from 'src/app/models/patch-db/data-model'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import * as uuid from 'uuid'
|
||||
|
||||
export abstract class ApiService {
|
||||
private $unauthorizedApiResponse$: Subject<{ }> = new Subject()
|
||||
export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
protected readonly sync = new Subject<Update<DataModel>>()
|
||||
private syncing = true
|
||||
|
||||
watch401$ (): Observable<{ }> {
|
||||
return this.$unauthorizedApiResponse$.asObservable()
|
||||
/** 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$ (_?: Observable<number>): Observable<Update<DataModel>> {
|
||||
return this.sync.asObservable().pipe(filter(() => this.syncing))
|
||||
}
|
||||
|
||||
authenticatedRequestsEnabled: boolean = false
|
||||
// used for determining internet connectivity
|
||||
abstract ping (): Promise<void>
|
||||
|
||||
protected received401 () {
|
||||
this.authenticatedRequestsEnabled = false
|
||||
this.$unauthorizedApiResponse$.next()
|
||||
// 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 submitPin (params: RR.SubmitPinReq): Promise<RR.SubmitPinRes>
|
||||
|
||||
abstract submitPassword (params: RR.SubmitPasswordReq): Promise<RR.SubmitPasswordReq>
|
||||
|
||||
abstract logout (params: RR.LogoutReq): Promise<RR.LogoutRes>
|
||||
|
||||
// server
|
||||
|
||||
abstract getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes>
|
||||
|
||||
abstract getServerMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetServerMetricsRes>
|
||||
|
||||
protected abstract updateServerRaw (params: RR.UpdateServerReq): Promise<RR.UpdateServerRes>
|
||||
updateServer = (params: RR.UpdateServerReq) => this.syncResponse(
|
||||
() => this.updateServerRaw(params),
|
||||
)()
|
||||
|
||||
abstract restartServer (params: RR.UpdateServerReq): Promise<RR.RestartServerRes>
|
||||
|
||||
abstract shutdownServer (params: RR.ShutdownServerReq): Promise<RR.ShutdownServerRes>
|
||||
|
||||
// network
|
||||
|
||||
abstract refreshLan (params: RR.RefreshLanReq): Promise<RR.RefreshLanRes>
|
||||
|
||||
// registry
|
||||
|
||||
protected abstract setRegistryRaw (params: RR.SetRegistryReq): Promise<RR.SetRegistryRes>
|
||||
setRegistry = (params: RR.SetRegistryReq) => this.syncResponse(
|
||||
() => this.setRegistryRaw(params),
|
||||
)()
|
||||
|
||||
// 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>
|
||||
|
||||
// wifi
|
||||
|
||||
abstract addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes>
|
||||
|
||||
protected abstract connectWifiRaw (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes>
|
||||
connectWifi = (params: RR.ConnectWifiReq) => this.syncResponse(
|
||||
() => this.connectWifiRaw(params),
|
||||
)()
|
||||
|
||||
protected abstract deleteWifiRaw (params: RR.DeleteWifiReq): Promise<RR.ConnectWifiRes>
|
||||
deleteWifi = (params: RR.DeleteWifiReq) => this.syncResponse(
|
||||
() => this.deleteWifiRaw(params),
|
||||
)()
|
||||
|
||||
// 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
|
||||
|
||||
protected abstract createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes>
|
||||
createBackup = (params: RR.CreateBackupReq) => this.syncResponse(
|
||||
() => this.createBackupRaw(params),
|
||||
)()
|
||||
|
||||
protected abstract restoreBackupRaw (params: RR.RestoreBackupReq): Promise<RR.RestoreBackupRes>
|
||||
restoreBackup = (params: RR.RestoreBackupReq) => this.syncResponse(
|
||||
() => this.restoreBackupRaw(params),
|
||||
)()
|
||||
|
||||
// disk
|
||||
|
||||
abstract getDisks (params: RR.GetDisksReq): Promise<RR.GetDisksRes>
|
||||
|
||||
abstract ejectDisk (params: RR.EjectDisksReq): Promise<RR.EjectDisksRes>
|
||||
|
||||
// package
|
||||
|
||||
abstract getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes<any>['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 restorePackageRaw (params: RR.RestorePackageReq): Promise<RR.RestorePackageRes>
|
||||
restorePackage = (params: RR.RestorePackageReq) => this.syncResponse(
|
||||
() => this.restorePackageRaw(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 dryRemovePackage (params: RR.DryRemovePackageReq): Promise<RR.DryRemovePackageRes>
|
||||
|
||||
protected abstract removePackageRaw (params: RR.RemovePackageReq): Promise<RR.RemovePackageRes>
|
||||
removePackage = (params: RR.RemovePackageReq) => this.syncResponse(
|
||||
() => this.removePackageRaw(params),
|
||||
)()
|
||||
|
||||
abstract dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes>
|
||||
|
||||
// marketplace
|
||||
|
||||
abstract getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise<RR.GetMarketplaceDataRes>
|
||||
|
||||
abstract getEos (params: RR.GetMarketplaceEOSReq): Promise<RR.GetMarketplaceEOSRes>
|
||||
|
||||
abstract getAvailableList (params: RR.GetAvailableListReq): Promise<RR.GetAvailableListRes>
|
||||
|
||||
abstract getAvailableShow (params: RR.GetAvailableShowReq): Promise<RR.GetAvailableShowRes>
|
||||
|
||||
// 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).then(({ response, revision }) => {
|
||||
if (revision) this.sync.next(revision)
|
||||
return response
|
||||
}) as any
|
||||
}
|
||||
}
|
||||
|
||||
abstract testConnection (url: string): Promise<true>
|
||||
abstract getCheckAuth (): Promise<Unit> // Throws an error on failed auth.
|
||||
abstract postLogin (password: string): Promise<Unit> // Throws an error on failed auth.
|
||||
abstract postLogout (): Promise<Unit> // Throws an error on failed auth.
|
||||
abstract getServer (timeout?: number): Promise<ApiServer>
|
||||
abstract getVersionLatest (): Promise<ReqRes.GetVersionLatestRes>
|
||||
abstract getServerMetrics (): Promise<ReqRes.GetServerMetricsRes>
|
||||
abstract getNotifications (page: number, perPage: number): Promise<S9Notification[]>
|
||||
abstract deleteNotification (id: string): Promise<Unit>
|
||||
abstract toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit>
|
||||
abstract updateAgent (version: any): Promise<Unit>
|
||||
abstract acknowledgeOSWelcome (version: string): Promise<Unit>
|
||||
abstract getAvailableApps (): Promise<AppAvailablePreview[]>
|
||||
abstract getAvailableApp (appId: string): Promise<AppAvailableFull>
|
||||
abstract getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo>
|
||||
abstract getInstalledApp (appId: string): Promise<AppInstalledFull>
|
||||
abstract getAppMetrics (appId: string): Promise<AppMetrics>
|
||||
abstract getInstalledApps (): Promise<AppInstalledPreview[]>
|
||||
abstract getExternalDisks (): Promise<DiskInfo[]>
|
||||
abstract getAppConfig (appId: string): Promise<{ spec: ConfigSpec, config: object, rules: Rules[] }>
|
||||
abstract getAppLogs (appId: string, params?: ReqRes.GetAppLogsReq): Promise<string[]>
|
||||
abstract getServerLogs (): Promise<string[]>
|
||||
abstract installApp (appId: string, version: string, dryRun?: boolean): Promise<AppInstalledFull & { breakages: DependentBreakage[] }>
|
||||
abstract uninstallApp (appId: string, dryRun?: boolean): Promise<{ breakages: DependentBreakage[] }>
|
||||
abstract startApp (appId: string): Promise<Unit>
|
||||
abstract stopApp (appId: string, dryRun?: boolean): Promise<{ breakages: DependentBreakage[] }>
|
||||
abstract restartApp (appId: string): Promise<Unit>
|
||||
abstract createAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit>
|
||||
abstract restoreAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit>
|
||||
abstract stopAppBackup (appId: string): Promise<Unit>
|
||||
abstract patchAppConfig (app: AppInstalledPreview, config: object, dryRun?: boolean): Promise<{ breakages: DependentBreakage[] }>
|
||||
abstract postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }>
|
||||
abstract patchServerConfig (attr: string, value: any): Promise<Unit>
|
||||
abstract wipeAppData (app: AppInstalledPreview): Promise<Unit>
|
||||
abstract addSSHKey (sshKey: string): Promise<Unit>
|
||||
abstract deleteSSHKey (sshKey: SSHFingerprint): Promise<Unit>
|
||||
abstract addWifi (ssid: string, password: string, country: string, connect: boolean): Promise<Unit>
|
||||
abstract connectWifi (ssid: string): Promise<Unit>
|
||||
abstract deleteWifi (ssid: string): Promise<Unit>
|
||||
abstract restartServer (): Promise<Unit>
|
||||
abstract shutdownServer (): Promise<Unit>
|
||||
abstract ejectExternalDisk (logicalName: string): Promise<Unit>
|
||||
abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse>
|
||||
abstract refreshLAN (): Promise<Unit>
|
||||
abstract checkV1Status (): Promise<V1Status>
|
||||
// @TODO better types?
|
||||
// private async process<T, F extends (args: object) => Promise<{ response: T, revision?: Revision }>> (f: F, temps: Operation[] = []): Promise<T> {
|
||||
// let expireId = undefined
|
||||
// if (temps.length) {
|
||||
// expireId = uuid.v4()
|
||||
// this.sync.next({ patch: temps, expiredBy: expireId })
|
||||
// }
|
||||
// const { response, revision } = await f({ ...f.arguments, expireId })
|
||||
// if (revision) this.sync.next(revision)
|
||||
// return response
|
||||
// }
|
||||
}
|
||||
|
||||
export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } {
|
||||
return !!(arg as any).error
|
||||
}
|
||||
|
||||
export function isRpcSuccess<Error, Result> (arg: { error: Error } | { result: Result }): arg is { result: Result } {
|
||||
return !!(arg as any).result
|
||||
}
|
||||
|
||||
export module ReqRes {
|
||||
export type GetVersionRes = { version: string }
|
||||
export type PostLoginReq = { password: string }
|
||||
export type PostLoginRes = Unit
|
||||
export type ServiceActionRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: string,
|
||||
method: string
|
||||
}
|
||||
export type ServiceActionResponse = {
|
||||
jsonrpc: '2.0',
|
||||
id: string
|
||||
} & ({ error: { code: number, message: string } } | { result: string })
|
||||
export type GetCheckAuthRes = { }
|
||||
export type GetServerRes = ApiServer
|
||||
export type GetVersionLatestRes = { versionLatest: string, releaseNotes: string }
|
||||
export type GetServerMetricsRes = ServerMetrics
|
||||
export type GetAppAvailableRes = ApiAppAvailableFull
|
||||
export type GetAppAvailableVersionInfoRes = AppAvailableVersionSpecificInfo
|
||||
export type GetAppsAvailableRes = AppAvailablePreview[]
|
||||
export type GetExternalDisksRes = DiskInfo[]
|
||||
export type GetAppInstalledRes = ApiAppInstalledFull
|
||||
export type GetAppConfigRes = ApiAppConfig
|
||||
export type GetAppLogsReq = { after?: string, before?: string, page?: string, perPage?: string }
|
||||
export type GetServerLogsReq = { }
|
||||
export type GetAppLogsRes = string[]
|
||||
export type GetServerLogsRes = string[]
|
||||
export type GetAppMetricsRes = AppMetricsVersioned<number>
|
||||
export type GetAppsInstalledRes = ApiAppInstalledPreview[]
|
||||
export type PostInstallAppReq = { version: string }
|
||||
export type PostInstallAppRes = ApiAppInstalledFull & { breakages: DependentBreakage[] }
|
||||
export type PostUpdateAgentReq = { version: string }
|
||||
export type PostAppBackupCreateReq = { logicalname: string, password: string }
|
||||
export type PostAppBackupCreateRes = Unit
|
||||
export type PostAppBackupRestoreReq = { logicalname: string, password: string }
|
||||
export type PostAppBackupRestoreRes = Unit
|
||||
export type PostAppBackupStopRes = Unit
|
||||
export type PatchAppConfigReq = { config: object }
|
||||
export type PatchServerConfigReq = { value: string }
|
||||
export type GetNotificationsReq = { page: string, perPage: string }
|
||||
export type GetNotificationsRes = S9Notification[]
|
||||
export type PostAddWifiReq = { ssid: string, password: string, country: string, skipConnect: boolean }
|
||||
export type PostConnectWifiReq = { country: string }
|
||||
export type PostAddSSHKeyReq = { sshKey: string }
|
||||
export type PostAddSSHKeyRes = SSHFingerprint
|
||||
}
|
||||
|
||||
// used for type inference in syncResponse
|
||||
type ExtractResultPromise<T extends Promise<any>> = T extends Promise<infer R> ? Promise<R> : any
|
||||
|
||||
@@ -1,338 +1,223 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpService, Method, HttpOptions } from '../http.service'
|
||||
import { AppModel, AppStatus } from '../../models/app-model'
|
||||
import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
|
||||
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
|
||||
import { ApiService, ReqRes } from './api.service'
|
||||
import { ApiAppInstalledPreview, ApiServer, Unit, V1Status } from './api-types'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { isUnauthorized } from 'src/app/util/web.util'
|
||||
import { Replace } from 'src/app/util/types.util'
|
||||
import { AppMetrics, parseMetricsPermissive } from 'src/app/util/metrics.util'
|
||||
import { modulateTime } from 'src/app/util/misc.util'
|
||||
import { Observable, of, throwError } from 'rxjs'
|
||||
import { catchError, mapTo } from 'rxjs/operators'
|
||||
import * as uuid from 'uuid'
|
||||
import { HttpService, Method } from '../http.service'
|
||||
import { ApiService } from './api.service'
|
||||
import { RR } from './api-types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { ConfigService } from '../config.service'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
constructor (
|
||||
private readonly http: HttpService,
|
||||
// TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel
|
||||
private readonly appModel: AppModel,
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly config: ConfigService,
|
||||
) { super() }
|
||||
|
||||
testConnection (url: string): Promise<true> {
|
||||
return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise()
|
||||
async ping (): Promise<void> {
|
||||
return this.http.rpcRequest({ method: 'ping', params: { } })
|
||||
}
|
||||
|
||||
// Used to check whether password or key is valid. If so, it will be used implicitly by all other calls.
|
||||
async getCheckAuth (): Promise<Unit> {
|
||||
return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' })
|
||||
async getStatic (url: string): Promise<string> {
|
||||
return this.http.httpRequest({ method: Method.GET, url })
|
||||
}
|
||||
|
||||
async postLogin (password: string): Promise<Unit> {
|
||||
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' })
|
||||
// db
|
||||
|
||||
async getRevisions (since: number): Promise<RR.GetRevisionsRes> {
|
||||
return this.http.rpcRequest({ method: 'db.revisions', params: { since } })
|
||||
}
|
||||
|
||||
async postLogout (): Promise<Unit> {
|
||||
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return { } })
|
||||
async getDump (): Promise<RR.GetDumpRes> {
|
||||
return this.http.rpcRequest({ method: 'db.dump' })
|
||||
}
|
||||
|
||||
async getServer (timeout?: number): Promise<ApiServer> {
|
||||
return this.authRequest<ReqRes.GetServerRes>({ method: Method.GET, url: '/', readTimeout: timeout })
|
||||
async setDbValueRaw (params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
|
||||
return this.http.rpcRequest({ method: 'db.put.ui', params })
|
||||
}
|
||||
|
||||
async acknowledgeOSWelcome (version: string): Promise<Unit> {
|
||||
return this.authRequest<Unit>({ method: Method.POST, url: `/welcome/${version}` })
|
||||
// auth
|
||||
|
||||
async submitPin (params: RR.SubmitPinReq): Promise<RR.SubmitPinRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.pin', params })
|
||||
}
|
||||
|
||||
async getVersionLatest (): Promise<ReqRes.GetVersionLatestRes> {
|
||||
return this.authRequest<ReqRes.GetVersionLatestRes>({ method: Method.GET, url: '/versionLatest' }, { version: '' })
|
||||
async submitPassword (params: RR.SubmitPasswordReq): Promise<RR.SubmitPasswordRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.password', params })
|
||||
}
|
||||
|
||||
async getServerMetrics (): Promise<ReqRes.GetServerMetricsRes> {
|
||||
return this.authRequest<ReqRes.GetServerMetricsRes>({ method: Method.GET, url: `/metrics` })
|
||||
async logout (params: RR.LogoutReq): Promise<RR.LogoutRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.logout', params })
|
||||
}
|
||||
|
||||
async getNotifications (page: number, perPage: number): Promise<S9Notification[]> {
|
||||
const params: ReqRes.GetNotificationsReq = {
|
||||
page: String(page),
|
||||
perPage: String(perPage),
|
||||
}
|
||||
return this.authRequest<ReqRes.GetNotificationsRes>({ method: Method.GET, url: `/notifications`, params })
|
||||
// server
|
||||
|
||||
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
||||
return this.http.rpcRequest( { method: 'server.logs', params })
|
||||
}
|
||||
|
||||
async deleteNotification (id: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` })
|
||||
async getServerMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetServerMetricsRes> {
|
||||
return this.http.rpcRequest({ method: 'server.metrics', params })
|
||||
}
|
||||
|
||||
async getExternalDisks (): Promise<DiskInfo[]> {
|
||||
return this.authRequest<ReqRes.GetExternalDisksRes>({ method: Method.GET, url: `/disks` })
|
||||
async updateServerRaw (params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.update', params })
|
||||
}
|
||||
|
||||
// TODO: EJECT-DISKS
|
||||
async ejectExternalDisk (logicalName: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/disks/eject`, data: { logicalName } })
|
||||
async restartServer (params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.restart', params })
|
||||
}
|
||||
|
||||
async updateAgent (version: string): Promise<Unit> {
|
||||
const data: ReqRes.PostUpdateAgentReq = {
|
||||
version: `=${version}`,
|
||||
}
|
||||
return this.authRequest({ method: Method.POST, url: '/update', data })
|
||||
async shutdownServer (params: RR.ShutdownServerReq): Promise<RR.ShutdownServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.shutdown', params })
|
||||
}
|
||||
|
||||
async getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> {
|
||||
return this
|
||||
.authRequest<Replace<ReqRes.GetAppAvailableVersionInfoRes, 'versionViewing', 'version'>>({ method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` })
|
||||
.then(res => ({ ...res, versionViewing: res.version }))
|
||||
.then(res => {
|
||||
delete res['version']
|
||||
return res
|
||||
})
|
||||
// network
|
||||
|
||||
async refreshLan (params: RR.RefreshLanReq): Promise<RR.RefreshLanRes> {
|
||||
return this.http.rpcRequest({ method: 'network.lan.refresh', params })
|
||||
}
|
||||
|
||||
async getAvailableApps (): Promise<AppAvailablePreview[]> {
|
||||
const res = await this.authRequest<ReqRes.GetAppsAvailableRes>({ method: Method.GET, url: '/apps/store' })
|
||||
return res.map(a => {
|
||||
const latestVersionTimestamp = new Date(a.latestVersionTimestamp)
|
||||
if (isNaN(latestVersionTimestamp as any)) throw new Error(`Invalid latestVersionTimestamp ${a.latestVersionTimestamp}`)
|
||||
return { ...a, latestVersionTimestamp }
|
||||
})
|
||||
// registry
|
||||
|
||||
async setRegistryRaw (params: RR.SetRegistryReq): Promise<RR.SetRegistryRes> {
|
||||
return this.http.rpcRequest({ method: 'registry.set', params })
|
||||
}
|
||||
|
||||
async getAvailableApp (appId: string): Promise<AppAvailableFull> {
|
||||
return this.authRequest<ReqRes.GetAppAvailableRes>({ method: Method.GET, url: `/apps/${appId}/store` })
|
||||
.then(res => {
|
||||
return {
|
||||
...res,
|
||||
versionViewing: res.versionLatest,
|
||||
}
|
||||
})
|
||||
// notification
|
||||
|
||||
async getNotificationsRaw (params: RR.GetNotificationsReq): Promise<RR.GetNotificationsRes> {
|
||||
return this.http.rpcRequest({ method: 'notifications.list', params })
|
||||
}
|
||||
|
||||
async getInstalledApp (appId: string): Promise<AppInstalledFull> {
|
||||
return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` })
|
||||
.then(app => {
|
||||
return {
|
||||
...app,
|
||||
hasFetchedFull: true,
|
||||
hasUI: this.config.hasUI(app),
|
||||
launchable: this.config.isLaunchable(app),
|
||||
}
|
||||
})
|
||||
async deleteNotification (params: RR.DeleteNotificationReq): Promise<RR.DeleteNotificationRes> {
|
||||
return this.http.rpcRequest({ method: 'notifications.delete', params })
|
||||
}
|
||||
|
||||
async getInstalledApps (): Promise<AppInstalledPreview[]> {
|
||||
return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` })
|
||||
.then(apps => {
|
||||
return apps.map(app => {
|
||||
return {
|
||||
...app,
|
||||
hasUI: this.config.hasUI(app),
|
||||
launchable: this.config.isLaunchable(app),
|
||||
}
|
||||
})
|
||||
})
|
||||
// wifi
|
||||
|
||||
async addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.add', params })
|
||||
}
|
||||
|
||||
async getAppConfig (appId: string): Promise<ReqRes.GetAppConfigRes> {
|
||||
return this.authRequest<ReqRes.GetAppConfigRes>({ method: Method.GET, url: `/apps/${appId}/config` })
|
||||
async connectWifiRaw (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.connect', params })
|
||||
}
|
||||
|
||||
async getAppLogs (appId: string, params: ReqRes.GetAppLogsReq = { }): Promise<string[]> {
|
||||
return this.authRequest<ReqRes.GetAppLogsRes>({ method: Method.GET, url: `/apps/${appId}/logs`, params: params as any })
|
||||
async deleteWifiRaw (params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.delete', params })
|
||||
}
|
||||
|
||||
async getServerLogs (): Promise<string[]> {
|
||||
return this.authRequest<ReqRes.GetServerLogsRes>({ method: Method.GET, url: `/logs` })
|
||||
// ssh
|
||||
|
||||
async getSshKeys (params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.get', params })
|
||||
}
|
||||
|
||||
async getAppMetrics (appId: string): Promise<AppMetrics> {
|
||||
return this.authRequest<ReqRes.GetAppMetricsRes | string>({ method: Method.GET, url: `/apps/${appId}/metrics` })
|
||||
.then(parseMetricsPermissive)
|
||||
async addSshKey (params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.add', params })
|
||||
}
|
||||
|
||||
async installApp (appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
|
||||
const data: ReqRes.PostInstallAppReq = {
|
||||
version,
|
||||
}
|
||||
return this.authRequest<ReqRes.PostInstallAppRes>({ method: Method.POST, url: `/apps/${appId}/install${dryRunParam(dryRun, true)}`, data })
|
||||
.then(app => {
|
||||
return {
|
||||
...app,
|
||||
hasFetchedFull: false,
|
||||
hasUI: this.config.hasUI(app),
|
||||
launchable: this.config.isLaunchable(app),
|
||||
}
|
||||
})
|
||||
async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.delete', params })
|
||||
}
|
||||
|
||||
async uninstallApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
||||
// backup
|
||||
|
||||
async createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.create', params })
|
||||
}
|
||||
|
||||
async startApp (appId: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 60000 })
|
||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING }))
|
||||
.then(() => ({ }))
|
||||
async restoreBackupRaw (params: RR.RestoreBackupReq): Promise<RR.RestoreBackupRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.restore', params })
|
||||
}
|
||||
|
||||
async stopApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||
const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
||||
if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }, modulateTime(new Date(), 5, 'seconds'))
|
||||
return res
|
||||
// disk
|
||||
|
||||
getDisks (params: RR.GetDisksReq): Promise<RR.GetDisksRes> {
|
||||
return this.http.rpcRequest({ method: 'disk.list', params })
|
||||
}
|
||||
|
||||
async restartApp (appId: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 60000 })
|
||||
.then(() => ({ } as any))
|
||||
ejectDisk (params: RR.EjectDisksReq): Promise<RR.EjectDisksRes> {
|
||||
return this.http.rpcRequest({ method: 'disk.eject', params })
|
||||
}
|
||||
|
||||
async createAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
|
||||
const data: ReqRes.PostAppBackupCreateReq = {
|
||||
password: password || undefined,
|
||||
logicalname,
|
||||
}
|
||||
return this.authRequest<ReqRes.PostAppBackupCreateRes>({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 60000 })
|
||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP }))
|
||||
.then(() => ({ }))
|
||||
// package
|
||||
|
||||
async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes<any>['data']> {
|
||||
return this.http.rpcRequest({ method: 'package.properties', params })
|
||||
.then(parsePropertiesPermissive)
|
||||
}
|
||||
|
||||
async stopAppBackup (appId: string): Promise<Unit> {
|
||||
return this.authRequest<ReqRes.PostAppBackupStopRes>({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 60000 })
|
||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED }))
|
||||
.then(() => ({ }))
|
||||
async getPackageLogs (params: RR.GetPackageLogsReq): Promise<RR.GetPackageLogsRes> {
|
||||
return this.http.rpcRequest( { method: 'package.logs', params })
|
||||
}
|
||||
|
||||
async restoreAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
|
||||
const data: ReqRes.PostAppBackupRestoreReq = {
|
||||
password: password || undefined,
|
||||
logicalname,
|
||||
}
|
||||
return this.authRequest<ReqRes.PostAppBackupRestoreRes>({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 60000 })
|
||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP }))
|
||||
.then(() => ({ }))
|
||||
async installPackageRaw (params: RR.InstallPackageReq): Promise<RR.InstallPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.install', params })
|
||||
}
|
||||
|
||||
async patchAppConfig (app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||
const data: ReqRes.PatchAppConfigReq = {
|
||||
config,
|
||||
}
|
||||
return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 60000 })
|
||||
async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise<RR.DryUpdatePackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.update.dry', params })
|
||||
}
|
||||
|
||||
async postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
||||
async getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.get', params })
|
||||
}
|
||||
|
||||
async patchServerConfig (attr: string, value: any): Promise<Unit> {
|
||||
const data: ReqRes.PatchServerConfigReq = {
|
||||
value,
|
||||
}
|
||||
return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 60000 })
|
||||
.then(() => this.serverModel.update({ [attr]: value }))
|
||||
.then(() => ({ }))
|
||||
async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise<RR.DrySetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.set.dry', params })
|
||||
}
|
||||
|
||||
async wipeAppData (app: AppInstalledPreview): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 60000 }).then((res) => {
|
||||
this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG })
|
||||
return res
|
||||
})
|
||||
async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise<RR.SetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.set', params })
|
||||
}
|
||||
|
||||
async toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` })
|
||||
async restorePackageRaw (params: RR.RestorePackageReq): Promise<RR.RestorePackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.restore', params })
|
||||
}
|
||||
|
||||
async addSSHKey (sshKey: string): Promise<Unit> {
|
||||
const data: ReqRes.PostAddSSHKeyReq = {
|
||||
sshKey,
|
||||
}
|
||||
const fingerprint = await this.authRequest<ReqRes.PostAddSSHKeyRes>({ method: Method.POST, url: `/sshKeys`, data })
|
||||
this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] })
|
||||
return { }
|
||||
async executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes> {
|
||||
return this.http.rpcRequest({ method: 'package.action', params })
|
||||
}
|
||||
|
||||
async addWifi (ssid: string, password: string, country: string, connect: boolean): Promise<Unit> {
|
||||
const data: ReqRes.PostAddWifiReq = {
|
||||
ssid,
|
||||
password,
|
||||
country,
|
||||
skipConnect: !connect,
|
||||
}
|
||||
return this.authRequest({ method: Method.POST, url: `/wifi`, data })
|
||||
async startPackageRaw (params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.start', params })
|
||||
}
|
||||
|
||||
async connectWifi (ssid: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) })
|
||||
async dryStopPackage (params: RR.DryStopPackageReq): Promise<RR.DryStopPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.stop.dry', params })
|
||||
}
|
||||
|
||||
async deleteWifi (ssid: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) })
|
||||
async stopPackageRaw (params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.stop', params })
|
||||
}
|
||||
|
||||
async deleteSSHKey (fingerprint: SSHFingerprint): Promise<Unit> {
|
||||
await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` })
|
||||
const ssh = this.serverModel.peek().ssh
|
||||
this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) })
|
||||
return { }
|
||||
async dryRemovePackage (params: RR.DryRemovePackageReq): Promise<RR.DryRemovePackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.remove.dry', params })
|
||||
}
|
||||
|
||||
async restartServer (): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 60000 })
|
||||
async removePackageRaw (params: RR.RemovePackageReq): Promise<RR.RemovePackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.remove', params })
|
||||
}
|
||||
|
||||
async shutdownServer (): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 })
|
||||
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes> {
|
||||
return this.http.rpcRequest({ method: 'package.dependency.configure.dry', params })
|
||||
}
|
||||
|
||||
async serviceAction (appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
|
||||
const data: ReqRes.ServiceActionRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: uuid.v4(),
|
||||
method: s.id,
|
||||
}
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/actions`, data, readTimeout: 300000 })
|
||||
// marketplace
|
||||
|
||||
async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise<RR.GetMarketplaceDataRes> {
|
||||
return this.http.rpcRequest({ method: 'marketplace.data', params })
|
||||
}
|
||||
|
||||
async refreshLAN (): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: '/network/lan/reset' })
|
||||
async getEos (params: RR.GetMarketplaceEOSReq): Promise<RR.GetMarketplaceEOSRes> {
|
||||
return this.http.rpcRequest({ method: 'marketplace.eos', params })
|
||||
}
|
||||
|
||||
async checkV1Status (): Promise<V1Status> {
|
||||
return this.http.request({ method: Method.GET, url: 'https://registry.start9labs.com/sys/status' })
|
||||
async getAvailableList (params: RR.GetAvailableListReq): Promise<RR.GetAvailableListRes> {
|
||||
return this.http.rpcRequest({ method: 'marketplace.available.list', params })
|
||||
}
|
||||
|
||||
private async authRequest<T> (opts: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
|
||||
if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`)
|
||||
|
||||
opts.withCredentials = true
|
||||
return this.http.serverRequest<T>(opts, overrides).catch((e: HttpError) => {
|
||||
console.log(`Got a server error!`, e)
|
||||
if (isUnauthorized(e)) this.received401()
|
||||
throw e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type HttpError = HttpErrorResponse & { error: { code: string, message: string } }
|
||||
|
||||
const dryRunParam = (dryRun: boolean, first: boolean) => {
|
||||
if (!dryRun) return ''
|
||||
return first ? `?dryrun` : `&dryrun`
|
||||
}
|
||||
|
||||
function catchHttpStatusError (error: HttpErrorResponse): Observable<true> {
|
||||
if (error.error instanceof ErrorEvent) {
|
||||
// A client-side or network error occurred. Handle it accordingly.
|
||||
return throwError('Not Connected')
|
||||
} else {
|
||||
return of(true)
|
||||
async getAvailableShow (params: RR.GetAvailableShowReq): Promise<RR.GetAvailableShowRes> {
|
||||
return this.http.rpcRequest({ method: 'marketplace.available', params })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Size Limit [![Cult Of Martians][cult-img]][cult]
|
||||
|
||||
<img src="https://ai.github.io/size-limit/logo.svg" align="right"
|
||||
alt="Size Limit logo by Anton Lovchikov" width="120" height="178">
|
||||
|
||||
Size Limit is a performance budget tool for JavaScript. It checks every commit
|
||||
on CI, calculates the real cost of your JS for end-users and throws an error
|
||||
if the cost exceeds the limit.
|
||||
@@ -159,192 +156,6 @@ interactive elements, using React/Vue/Svelte lib or vanilla JS.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
### Big Libraries
|
||||
|
||||
JS libraries > 10 KB in size.
|
||||
|
||||
This preset includes headless Chrome, and will measure your lib’s execution
|
||||
time. You likely don’t need this overhead for a small 2 KB lib, but for larger
|
||||
ones the execution time is a more accurate and understandable metric that
|
||||
the size in bytes. Library like [React] is a good example for this preset.
|
||||
|
||||
<details><summary><b>Show instructions</b></summary>
|
||||
|
||||
1. Install preset:
|
||||
|
||||
```sh
|
||||
$ npm install --save-dev size-limit @size-limit/preset-big-lib
|
||||
```
|
||||
|
||||
2. Add the `size-limit` section and the `size` script to your `package.json`:
|
||||
|
||||
```diff
|
||||
+ "size-limit": [
|
||||
+ {
|
||||
+ "path": "dist/react.production-*.js"
|
||||
+ }
|
||||
+ ],
|
||||
"scripts": {
|
||||
"build": "webpack ./scripts/rollup/build.js",
|
||||
+ "size": "npm run build && size-limit",
|
||||
"test": "jest && eslint ."
|
||||
}
|
||||
```
|
||||
|
||||
3. If you use ES modules you can test the size after tree-shaking with `import`
|
||||
option:
|
||||
|
||||
```diff
|
||||
"size-limit": [
|
||||
{
|
||||
"path": "dist/react.production-*.js",
|
||||
+ "import": "{ createComponent }"
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
4. Here’s how you can get the size for your current project:
|
||||
|
||||
```sh
|
||||
$ npm run size
|
||||
|
||||
Package size: 30.08 KB with all dependencies, minified and gzipped
|
||||
Loading time: 602 ms on slow 3G
|
||||
Running time: 214 ms on Snapdragon 410
|
||||
Total time: 815 ms
|
||||
```
|
||||
|
||||
5. Now, let’s set the limit. Add 25% to the current total time and use that
|
||||
as the limit in your `package.json`:
|
||||
|
||||
```diff
|
||||
"size-limit": [
|
||||
{
|
||||
+ "limit": "1 s",
|
||||
"path": "dist/react.production-*.js"
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
6. Add a `size` script to your test suite:
|
||||
|
||||
```diff
|
||||
"scripts": {
|
||||
"build": "rollup ./scripts/rollup/build.js",
|
||||
"size": "npm run build && size-limit",
|
||||
- "test": "jest && eslint ."
|
||||
+ "test": "jest && eslint . && npm run size"
|
||||
}
|
||||
```
|
||||
|
||||
7. If you don’t have a continuous integration service running, don’t forget
|
||||
to add one — start with [Travis CI].
|
||||
8. Add the library size to docs, it will help users to choose your project:
|
||||
|
||||
```diff
|
||||
# Project Name
|
||||
|
||||
Short project description
|
||||
|
||||
* **Fast.** 10% faster than competitor.
|
||||
+ * **Small.** 15 KB (minified and gzipped).
|
||||
+ [Size Limit](https://github.com/ai/size-limit) controls the size.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
### Small Libraries
|
||||
|
||||
JS libraries < 10 KB in size.
|
||||
|
||||
This preset will only measure the size, without the execution time, so it’s
|
||||
suitable for small libraries. If your library is larger, you likely want
|
||||
the Big Libraries preset above. [Nano ID] or [Storeon] are good examples
|
||||
for this preset.
|
||||
|
||||
<details><summary><b>Show instructions</b></summary>
|
||||
|
||||
1. First, install `size-limit`:
|
||||
|
||||
```sh
|
||||
$ npm install --save-dev size-limit @size-limit/preset-small-lib
|
||||
```
|
||||
|
||||
2. Add the `size-limit` section and the `size` script to your `package.json`:
|
||||
|
||||
```diff
|
||||
+ "size-limit": [
|
||||
+ {
|
||||
+ "path": "index.js"
|
||||
+ }
|
||||
+ ],
|
||||
"scripts": {
|
||||
+ "size": "size-limit",
|
||||
"test": "jest && eslint ."
|
||||
}
|
||||
```
|
||||
|
||||
3. Here’s how you can get the size for your current project:
|
||||
|
||||
```sh
|
||||
$ npm run size
|
||||
|
||||
Package size: 177 B with all dependencies, minified and gzipped
|
||||
```
|
||||
|
||||
4. If your project size starts to look bloated, run `--why` for analysis:
|
||||
|
||||
```sh
|
||||
$ npm run size -- --why
|
||||
```
|
||||
|
||||
5. Now, let’s set the limit. Determine the current size of your library,
|
||||
add just a little bit (a kilobyte, maybe) and use that as the limit
|
||||
in your `package.json`:
|
||||
|
||||
```diff
|
||||
"size-limit": [
|
||||
{
|
||||
+ "limit": "9 KB",
|
||||
"path": "index.js"
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
6. Add the `size` script to your test suite:
|
||||
|
||||
```diff
|
||||
"scripts": {
|
||||
"size": "size-limit",
|
||||
- "test": "jest && eslint ."
|
||||
+ "test": "jest && eslint . && npm run size"
|
||||
}
|
||||
```
|
||||
|
||||
7. If you don’t have a continuous integration service running, don’t forget
|
||||
to add one — start with [Travis CI].
|
||||
8. Add the library size to docs, it will help users to choose your project:
|
||||
|
||||
```diff
|
||||
# Project Name
|
||||
|
||||
Short project description
|
||||
|
||||
* **Fast.** 10% faster than competitor.
|
||||
+ * **Small.** 500 bytes (minified and gzipped). No dependencies.
|
||||
+ [Size Limit](https://github.com/ai/size-limit) controls the size.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
[Travis CI]: https://github.com/dwyl/learn-travis
|
||||
[Storeon]: https://github.com/ai/storeon/
|
||||
[Nano ID]: https://github.com/ai/nanoid/
|
||||
[React]: https://github.com/facebook/react/
|
||||
|
||||
|
||||
## Reports
|
||||
|
||||
Size Limit has a [GitHub action] that comments and rejects pull requests based
|
||||
@@ -371,99 +182,6 @@ jobs:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
|
||||
## Config
|
||||
|
||||
Size Limits supports three ways to define config.
|
||||
|
||||
1. `size-limit` section in `package.json`:
|
||||
|
||||
```json
|
||||
"size-limit": [
|
||||
{
|
||||
"path": "index.js",
|
||||
"import": "{ createStore }",
|
||||
"limit": "500 ms"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
2. or a separate `.size-limit.json` config file:
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"path": "index.js",
|
||||
"import": "{ createStore }",
|
||||
"limit": "500 ms"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
3. or a more flexible `.size-limit.js` config file:
|
||||
|
||||
```js
|
||||
module.exports = [
|
||||
{
|
||||
path: "index.js",
|
||||
import: "{ createStore }",
|
||||
limit: "500 ms"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Each section in the config can have these options:
|
||||
|
||||
* **path**: relative paths to files. The only mandatory option.
|
||||
It could be a path `"index.js"`, a [pattern] `"dist/app-*.js"`
|
||||
or an array `["index.js", "dist/app-*.js", "!dist/app-exclude.js"]`.
|
||||
* **import**: partial import to test tree-shaking. It could be `"{ lib }"`
|
||||
to test `import { lib } from 'lib'` or `{ "a.js": "{ a }", "b.js": "{ b }" }`
|
||||
to test multiple files.
|
||||
* **limit**: size or time limit for files from the `path` option. It should be
|
||||
a string with a number and unit, separated by a space.
|
||||
Format: `100 B`, `10 KB`, `500 ms`, `1 s`.
|
||||
* **name**: the name of the current section. It will only be useful
|
||||
if you have multiple sections.
|
||||
* **entry**: when using a custom webpack config, a webpack entry could be given.
|
||||
It could be a string or an array of strings.
|
||||
By default, the total size of all entry points will be checked.
|
||||
* **webpack**: with `false` it will disable webpack.
|
||||
* **running**: with `false` it will disable calculating running time.
|
||||
* **gzip**: with `false` it will disable gzip compression.
|
||||
* **brotli**: with `true` it will use brotli compression and disable gzip compression.
|
||||
* **config**: a path to a custom webpack config.
|
||||
* **ignore**: an array of files and dependencies to exclude from
|
||||
the project size calculation.
|
||||
|
||||
If you use Size Limit to track the size of CSS files, make sure to set
|
||||
`webpack: false`. Otherwise, you will get wrong numbers, because webpack
|
||||
inserts `style-loader` runtime (≈2 KB) into the bundle.
|
||||
|
||||
[pattern]: https://github.com/sindresorhus/globby#globbing-patterns
|
||||
|
||||
|
||||
## Plugins and Presets
|
||||
|
||||
Plugins:
|
||||
|
||||
* `@size-limit/file` checks the size of files with Gzip, Brotli
|
||||
or without compression.
|
||||
* `@size-limit/webpack` adds your library to empty webpack project
|
||||
and prepares bundle file for `file` plugin.
|
||||
* `@size-limit/time` uses headless Chrome to track time to execute JS.
|
||||
* `@size-limit/dual-publish` compiles files to ES modules with [`dual-publish`]
|
||||
to check size after tree-shaking.
|
||||
|
||||
Plugin presets:
|
||||
|
||||
* `@size-limit/preset-app` contains `file` and `time` plugins.
|
||||
* `@size-limit/preset-big-lib` contains `webpack`, `file`, and `time` plugins.
|
||||
* `@size-limit/preset-small-lib` contains `webpack` and `file` plugins.
|
||||
|
||||
[`dual-publish`]: https://github.com/ai/dual-publish
|
||||
|
||||
|
||||
## JS API
|
||||
|
||||
```js
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,7 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject, Subscription } from 'rxjs'
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
import { distinctUntilChanged } from 'rxjs/operators'
|
||||
import { ApiService } from './api/api.service'
|
||||
import { chill } from '../util/misc.util'
|
||||
import { isUnauthorized } from '../util/web.util'
|
||||
import { Storage } from '@ionic/storage'
|
||||
import { StorageKeys } from '../models/storage-keys'
|
||||
|
||||
@@ -16,47 +14,41 @@ export enum AuthState {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly $authState$: BehaviorSubject<AuthState> = new BehaviorSubject(AuthState.INITIALIZING)
|
||||
private readonly authState$: BehaviorSubject<AuthState> = new BehaviorSubject(AuthState.INITIALIZING)
|
||||
|
||||
constructor (
|
||||
private readonly api: ApiService,
|
||||
private readonly storage: Storage,
|
||||
) { }
|
||||
|
||||
peek (): AuthState { return this.$authState$.getValue() }
|
||||
listen (callback: Partial<{ [k in AuthState]: () => any }>): Subscription {
|
||||
return this.$authState$.pipe(distinctUntilChanged()).subscribe(s => {
|
||||
return (callback[s] || chill)()
|
||||
})
|
||||
) {
|
||||
this.storage.create()
|
||||
}
|
||||
|
||||
async login (password: string) {
|
||||
try {
|
||||
await this.api.postLogin(password)
|
||||
await this.storage.set(StorageKeys.LOGGED_IN_KEY, true)
|
||||
this.$authState$.next(AuthState.VERIFIED)
|
||||
} catch (e) {
|
||||
if (isUnauthorized(e)) {
|
||||
this.$authState$.next(AuthState.UNVERIFIED)
|
||||
throw { name: 'invalid', message: 'invalid credentials' }
|
||||
}
|
||||
console.error(`Failed login attempt`, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async restoreCache (): Promise<AuthState> {
|
||||
async init (): Promise<AuthState> {
|
||||
const loggedIn = await this.storage.get(StorageKeys.LOGGED_IN_KEY)
|
||||
if (loggedIn) {
|
||||
this.$authState$.next(AuthState.VERIFIED)
|
||||
this.authState$.next(AuthState.VERIFIED)
|
||||
return AuthState.VERIFIED
|
||||
} else {
|
||||
this.$authState$.next(AuthState.UNVERIFIED)
|
||||
this.authState$.next(AuthState.UNVERIFIED)
|
||||
return AuthState.UNVERIFIED
|
||||
}
|
||||
}
|
||||
|
||||
async setAuthStateUnverified (): Promise<void> {
|
||||
this.$authState$.next(AuthState.UNVERIFIED)
|
||||
watch$ (): Observable<AuthState> {
|
||||
return this.authState$.pipe(distinctUntilChanged())
|
||||
}
|
||||
|
||||
async submitPin (pin: string): Promise<void> {
|
||||
await this.api.submitPin({ pin })
|
||||
}
|
||||
|
||||
async submitPassword (password: string): Promise<void> {
|
||||
await this.api.submitPassword({ password })
|
||||
await this.storage.set(StorageKeys.LOGGED_IN_KEY, true)
|
||||
this.authState$.next(AuthState.VERIFIED)
|
||||
}
|
||||
|
||||
async setUnverified (): Promise<void> {
|
||||
this.authState$.next(AuthState.UNVERIFIED)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AppStatus } from '../models/app-model'
|
||||
import { ApiAppInstalledPreview } from './api/api-types'
|
||||
import { InstalledPackageDataEntry, InterfaceDef, Manifest, PackageDataEntry, PackageMainStatus, PackageState } from '../models/patch-db/data-model'
|
||||
|
||||
const { useMocks, mockOver, skipStartupAlerts } = require('../../../use-mocks.json') as UseMocks
|
||||
const { patchDb, maskAs, api, skipStartupAlerts } = require('../../../ui-config.json') as UiConfig
|
||||
|
||||
type UseMocks = {
|
||||
useMocks: boolean
|
||||
mockOver: 'tor' | 'lan'
|
||||
type UiConfig = {
|
||||
patchDb: {
|
||||
// If this is false (the default), poll will be used if in consulate only. If true it will be on regardless of env. This is useful in concert with api mocks.
|
||||
usePollOverride: boolean
|
||||
poll: {
|
||||
cooldown: number /* in ms */
|
||||
}
|
||||
websocket: {
|
||||
type: 'ws'
|
||||
url: string
|
||||
version: number
|
||||
}
|
||||
// Wait this long (ms) before asking BE for a dump when out of order messages are received
|
||||
timeoutForMissingRevision: number
|
||||
}
|
||||
api: {
|
||||
mocks: boolean
|
||||
url: string
|
||||
version: string
|
||||
root: string
|
||||
}
|
||||
maskAs: 'tor' | 'lan' | 'none'
|
||||
skipStartupAlerts: boolean
|
||||
}
|
||||
@Injectable({
|
||||
@@ -16,33 +34,74 @@ export class ConfigService {
|
||||
origin = removePort(removeProtocol(window.origin))
|
||||
version = require('../../../package.json').version
|
||||
|
||||
api = {
|
||||
useMocks,
|
||||
url: '/api',
|
||||
version: '/v0',
|
||||
root: '', // empty will default to same origin
|
||||
}
|
||||
patchDb = patchDb
|
||||
api = api
|
||||
|
||||
skipStartupAlerts = skipStartupAlerts
|
||||
isConsulate = window['platform'] === 'ios'
|
||||
|
||||
isTor () : boolean {
|
||||
return (this.api.useMocks && mockOver === 'tor') || this.origin.endsWith('.onion')
|
||||
return (maskAs === 'tor') || this.origin.endsWith('.onion')
|
||||
}
|
||||
|
||||
hasUI (app: ApiAppInstalledPreview): boolean {
|
||||
return app.lanUi || app.torUi
|
||||
isLan () : boolean {
|
||||
return (maskAs === 'lan') || this.origin.endsWith('.local')
|
||||
}
|
||||
|
||||
isLaunchable (app: ApiAppInstalledPreview): boolean {
|
||||
return !this.isConsulate &&
|
||||
app.status === AppStatus.RUNNING &&
|
||||
(
|
||||
(app.torAddress && app.torUi && this.isTor()) ||
|
||||
(app.lanAddress && app.lanUi && !this.isTor())
|
||||
)
|
||||
isLaunchable (pkg: PackageDataEntry): boolean {
|
||||
if (this.isConsulate || pkg.state !== PackageState.Installed) {
|
||||
return false
|
||||
}
|
||||
|
||||
const installed = pkg.installed
|
||||
|
||||
return installed.status.main.status === PackageMainStatus.Running &&
|
||||
(
|
||||
(hasTorUi(installed.manifest.interfaces) && this.isTor()) ||
|
||||
(hasLanUi(installed.manifest.interfaces) && !this.isTor())
|
||||
)
|
||||
}
|
||||
|
||||
launchableURL (pkg: InstalledPackageDataEntry): string {
|
||||
return this.isTor() ? `http://${torUiAddress(pkg)}` : `https://${lanUiAddress(pkg)}`
|
||||
}
|
||||
}
|
||||
|
||||
export function hasTorUi (interfaces: { [id: string]: InterfaceDef }): boolean {
|
||||
return !!Object.values(interfaces).find(i => i.ui && i['tor-config'])
|
||||
}
|
||||
|
||||
export function hasLanUi (interfaces: { [id: string]: InterfaceDef }): boolean {
|
||||
return !!Object.values(interfaces).find(i => i.ui && i['lan-config'])
|
||||
}
|
||||
|
||||
export function torUiAddress (pkg: InstalledPackageDataEntry): string {
|
||||
const interfaces = pkg.manifest.interfaces
|
||||
const id = Object.keys(interfaces).find(key => {
|
||||
const val = interfaces[key]
|
||||
return val.ui && val['tor-config']
|
||||
})
|
||||
return pkg['interface-info'].addresses[id]['tor-address']
|
||||
}
|
||||
|
||||
export function lanUiAddress (pkg: InstalledPackageDataEntry): string {
|
||||
const interfaces = pkg.manifest.interfaces
|
||||
const id = Object.keys(interfaces).find(key => {
|
||||
const val = interfaces[key]
|
||||
return val.ui && val['lan-config']
|
||||
})
|
||||
return pkg['interface-info'].addresses[id]['lan-address']
|
||||
}
|
||||
|
||||
export function hasUi (interfaces: { [id: string]: InterfaceDef }): boolean {
|
||||
return hasTorUi(interfaces) || hasLanUi(interfaces)
|
||||
}
|
||||
|
||||
export function getManifest (pkg: PackageDataEntry): Manifest {
|
||||
if (pkg.state === PackageState.Installed) {
|
||||
return pkg.installed.manifest
|
||||
}
|
||||
return pkg['temp-manifest']
|
||||
}
|
||||
|
||||
function removeProtocol (str: string): string {
|
||||
|
||||
105
ui/src/app/services/connection.service.ts
Normal file
105
ui/src/app/services/connection.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { fromEvent, Observable, Subject, Subscription, timer } from 'rxjs'
|
||||
import { debounceTime, delay, retryWhen, startWith, switchMap, tap } from 'rxjs/operators'
|
||||
import { ApiService } from './api/api.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConnectionService {
|
||||
private offlineSubscription: Subscription
|
||||
private onlineSubscription: Subscription
|
||||
private httpSubscription: Subscription
|
||||
private readonly currentState: ConnectionState = {
|
||||
network: true,
|
||||
internet: true,
|
||||
}
|
||||
private readonly stateChangeEventEmitter = new Subject<ConnectionState>()
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
) {
|
||||
this.checkNetworkState()
|
||||
this.checkInternetState()
|
||||
}
|
||||
|
||||
ngOnDestroy (): void {
|
||||
try {
|
||||
this.offlineSubscription.unsubscribe()
|
||||
this.onlineSubscription.unsubscribe()
|
||||
this.httpSubscription.unsubscribe()
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor Network & Internet connection status by subscribing to this observer.
|
||||
*/
|
||||
monitor$ (): Observable<ConnectionState> {
|
||||
return this.stateChangeEventEmitter.pipe(
|
||||
debounceTime(300),
|
||||
startWith(this.currentState),
|
||||
)
|
||||
}
|
||||
|
||||
private checkNetworkState (): void {
|
||||
this.onlineSubscription = fromEvent(window, 'online').subscribe(() => {
|
||||
this.currentState.network = true
|
||||
this.checkInternetState()
|
||||
this.emitEvent()
|
||||
})
|
||||
|
||||
this.offlineSubscription = fromEvent(window, 'offline').subscribe(() => {
|
||||
this.currentState.network = true
|
||||
this.checkInternetState()
|
||||
this.emitEvent()
|
||||
})
|
||||
}
|
||||
|
||||
private checkInternetState (): void {
|
||||
|
||||
if (this.httpSubscription) {
|
||||
this.httpSubscription.unsubscribe()
|
||||
}
|
||||
|
||||
// ping server every 10 seconds
|
||||
this.httpSubscription = timer(0, 10000)
|
||||
.pipe(
|
||||
switchMap(() => this.apiService.ping()),
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
tap(val => {
|
||||
console.error('Ping error: ', val)
|
||||
this.currentState.internet = true
|
||||
this.emitEvent()
|
||||
}),
|
||||
// restart after 2 seconds
|
||||
delay(2000),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.currentState.internet = true
|
||||
this.emitEvent()
|
||||
})
|
||||
}
|
||||
|
||||
private emitEvent (): void {
|
||||
this.stateChangeEventEmitter.next(this.currentState)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance of this interface is used to report current connection status.
|
||||
*/
|
||||
export interface ConnectionState {
|
||||
/**
|
||||
* "True" if browser has network connection. Determined by Window objects "online" / "offline" events.
|
||||
*/
|
||||
network: boolean
|
||||
/**
|
||||
* "True" if browser has Internet access. Determined by heartbeat system which periodically makes request to heartbeat Url.
|
||||
*/
|
||||
internet: boolean
|
||||
}
|
||||
@@ -1,32 +1,49 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
|
||||
import { Observable, from, interval, race } from 'rxjs'
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http'
|
||||
import { Observable, from, interval, race, Subject } from 'rxjs'
|
||||
import { map, take } from 'rxjs/operators'
|
||||
import { ConfigService } from './config.service'
|
||||
import { Revision } from 'patch-db-client'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class HttpService {
|
||||
private unauthorizedApiResponse$ = new Subject()
|
||||
authReqEnabled: boolean = false
|
||||
rootUrl: string
|
||||
|
||||
constructor (
|
||||
private readonly http: HttpClient,
|
||||
private readonly config: ConfigService,
|
||||
) { }
|
||||
|
||||
get raw () : HttpClient {
|
||||
return this.http
|
||||
) {
|
||||
const { url, version } = this.config.api
|
||||
this.rootUrl = `${url}/${version}`
|
||||
}
|
||||
|
||||
async serverRequest<T> (options: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
|
||||
options.url = leadingSlash(`${this.config.api.url}${exists(overrides.version) ? overrides.version : this.config.api.version}${options.url}`)
|
||||
if ( this.config.api.root && this.config.api.root !== '' ) {
|
||||
options.url = `${this.config.api.root}${options.url}`
|
||||
watch401$ (): Observable<{ }> {
|
||||
return this.unauthorizedApiResponse$.asObservable()
|
||||
}
|
||||
|
||||
async rpcRequest<T> (rpcOpts: RPCOptions): Promise<T> {
|
||||
rpcOpts.params = rpcOpts.params || { }
|
||||
const httpOpts = {
|
||||
method: Method.POST,
|
||||
data: rpcOpts,
|
||||
url: '',
|
||||
}
|
||||
return this.request<T>(options)
|
||||
|
||||
const res = await this.httpRequest<RPCResponse<T>>(httpOpts)
|
||||
|
||||
if (isRpcError(res)) throw new RpcError(res.error)
|
||||
|
||||
if (isRpcSuccess(res)) return res.result
|
||||
}
|
||||
|
||||
async request<T> (httpOpts: HttpOptions): Promise<T> {
|
||||
const { url, body, timeout, ...rest} = translateOptions(httpOpts)
|
||||
async httpRequest<T> (httpOpts: HttpOptions): Promise<T> {
|
||||
let { url, body, timeout, ...rest} = translateOptions(httpOpts)
|
||||
url = this.rootUrl + url
|
||||
|
||||
let req: Observable<{ body: T }>
|
||||
switch (httpOpts.method){
|
||||
case Method.GET: req = this.http.get(url, rest) as any; break
|
||||
@@ -37,25 +54,42 @@ export class HttpService {
|
||||
}
|
||||
|
||||
return (timeout ? withTimeout(req, timeout) : req)
|
||||
.toPromise()
|
||||
.then(res => res.body)
|
||||
.catch(e => { console.error(e); throw humanReadableErrorMessage(e)})
|
||||
.toPromise()
|
||||
.then(res => res.body)
|
||||
.catch(e => { throw new HttpError(e) })
|
||||
}
|
||||
}
|
||||
|
||||
function humanReadableErrorMessage (e: any): Error {
|
||||
// server up, custom backend error
|
||||
if (e.error && e.error.message) return { ...e, message: e.error.message }
|
||||
if (e.message) return { ...e, message: e.message }
|
||||
if (e.status && e.statusText) return { ...e, message: `${e.status} ${e.statusText}` }
|
||||
return { ...e, message: `Unidentifiable HTTP exception` }
|
||||
function RpcError (e: RPCError['error']): void {
|
||||
const { code, message } = e
|
||||
this.status = code
|
||||
this.message = message
|
||||
if (typeof e.data === 'string') {
|
||||
throw new Error(`unexpected response for RPC Error data: ${e.data}`)
|
||||
}
|
||||
const data = e.data || { message: 'unknown RPC error', revision: null }
|
||||
this.data = { ...data, code }
|
||||
}
|
||||
|
||||
function leadingSlash (url: string): string {
|
||||
let toReturn = url
|
||||
toReturn = toReturn.startsWith('/') ? toReturn : '/' + toReturn
|
||||
toReturn = !toReturn.endsWith('/') ? toReturn : toReturn.slice(0, -1)
|
||||
return toReturn
|
||||
function HttpError (e: HttpErrorResponse): void {
|
||||
const { status, statusText, error } = e
|
||||
this.status = status
|
||||
this.message = statusText
|
||||
this.data = error || { } // error = { code: string, message: string }
|
||||
}
|
||||
|
||||
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 {
|
||||
status: number
|
||||
message: string
|
||||
data: { code: string, message: string, revision: Revision | null }
|
||||
}
|
||||
|
||||
export enum Method {
|
||||
@@ -66,27 +100,64 @@ export enum Method {
|
||||
DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
export interface RPCOptions {
|
||||
method: string
|
||||
// @TODO what are valid params? object, bool?
|
||||
params?: {
|
||||
[param: string]: string | number | boolean | object | string[] | 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?: {
|
||||
message: string
|
||||
revision: Revision | null
|
||||
} | string
|
||||
}
|
||||
}
|
||||
|
||||
export type RPCResponse<T> = RPCSuccess<T> | RPCError
|
||||
|
||||
type HttpError = HttpErrorResponse & { error: { code: string, message: string } }
|
||||
|
||||
export interface HttpOptions {
|
||||
withCredentials?: boolean
|
||||
url: string
|
||||
method: Method
|
||||
params?: {
|
||||
[param: string]: string | string[];
|
||||
[param: string]: string | string[]
|
||||
}
|
||||
data?: any
|
||||
headers?: {
|
||||
[key: string]: string;
|
||||
}
|
||||
url: string
|
||||
readTimeout?: number
|
||||
}
|
||||
|
||||
export interface HttpJsonOptions {
|
||||
headers?: HttpHeaders | {
|
||||
[header: string]: string | string[];
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
observe: 'events'
|
||||
params?: HttpParams | {
|
||||
[param: string]: string | string[];
|
||||
[param: string]: string | string[]
|
||||
}
|
||||
reportProgress?: boolean
|
||||
responseType?: 'json'
|
||||
@@ -116,7 +187,3 @@ function withTimeout<U> (req: Observable<U>, timeout: number): Observable<U> {
|
||||
interval(timeout).pipe(take(1), map(() => { throw new Error('timeout') })),
|
||||
)
|
||||
}
|
||||
|
||||
function exists (str?: string): boolean {
|
||||
return !!str || str === ''
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs'
|
||||
import { catchError, concatMap, distinctUntilChanged, map, take, tap } from 'rxjs/operators'
|
||||
import { ServerModel, ServerStatus } from '../models/server-model'
|
||||
import { ApiService } from './api/api.service'
|
||||
import { Emver } from './emver.service'
|
||||
|
||||
|
||||
// call checkForUpdates in marketplace pages, can subscribe globally however
|
||||
type UpdateAvailable = { versionLatest: string, releaseNotes: string}
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class OsUpdateService {
|
||||
// holds version latest if update available, undefined if not.
|
||||
private readonly $updateAvailable$ = new BehaviorSubject<UpdateAvailable>(undefined)
|
||||
|
||||
watchForUpdateAvailable$ (): Observable<undefined | UpdateAvailable> {
|
||||
return this.$updateAvailable$.asObservable().pipe(distinctUntilChanged())
|
||||
}
|
||||
|
||||
constructor (
|
||||
private readonly emver: Emver,
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
) { }
|
||||
|
||||
// emits the latest version or re-checks to see if there's a new latest version
|
||||
checkWhenNotAvailable$ (): Observable<undefined | UpdateAvailable> {
|
||||
return this.$updateAvailable$.pipe(
|
||||
take(1),
|
||||
concatMap(vl => vl ? of(vl) : this.checkForUpdates$()),
|
||||
)
|
||||
}
|
||||
|
||||
// can sub to this imperatively and take the return value as gospel, or watch the $updateAvailable$ subject for the same info.
|
||||
checkForUpdates$ (): Observable<undefined | UpdateAvailable> {
|
||||
return forkJoin([
|
||||
this.serverModel.watch().versionInstalled.pipe(take(1)),
|
||||
this.apiService.getVersionLatest(),
|
||||
]).pipe(
|
||||
map(([vi, vl]) => this.updateIsAvailable(vi, vl) ? vl : undefined),
|
||||
catchError(e => {
|
||||
console.error(`OsUpdateService Error: ${e}`)
|
||||
return of(undefined)
|
||||
}),
|
||||
// cache the result for components to learn update available without having to have called this method
|
||||
tap(this.$updateAvailable$),
|
||||
)
|
||||
}
|
||||
|
||||
updateIsAvailable (vi: string, vl: UpdateAvailable): boolean {
|
||||
if (!vi || !vl) return false
|
||||
if (this.emver.compare(vi, vl.versionLatest) === -1) {
|
||||
this.$updateAvailable$.next(vl)
|
||||
return true
|
||||
} else {
|
||||
this.$updateAvailable$.next(undefined)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async updateEmbassyOS (versionLatest: string): Promise<void> {
|
||||
await this.apiService.updateAgent(versionLatest)
|
||||
this.serverModel.update({ status: ServerStatus.UPDATING })
|
||||
this.$updateAvailable$.next(undefined)
|
||||
await this.navCtrl.navigateRoot('/embassy')
|
||||
}
|
||||
}
|
||||
58
ui/src/app/services/pkg-status-rendering.service.ts
Normal file
58
ui/src/app/services/pkg-status-rendering.service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { PackageDataEntry, PackageMainStatus, PackageState, Status } from '../models/patch-db/data-model'
|
||||
import { ConnectionState } from './connection.service'
|
||||
|
||||
export function renderPkgStatus (pkg: PackageDataEntry, connection: ConnectionState): PkgStatusRendering {
|
||||
if (!connection.network || !connection.internet) {
|
||||
return { display: 'Connecting', color: 'medium', showDots: true, feStatus: FEStatus.Connecting }
|
||||
}
|
||||
|
||||
switch (pkg.state) {
|
||||
case PackageState.Installing: return { display: 'Installing', color: 'primary', showDots: true, feStatus: FEStatus.Installing }
|
||||
case PackageState.Updating: return { display: 'Updating', color: 'primary', showDots: true, feStatus: FEStatus.Updating }
|
||||
case PackageState.Removing: return { display: 'Removing', color: 'warning', showDots: true, feStatus: FEStatus.Removing }
|
||||
case PackageState.Installed: return handleInstalledState(pkg.installed.status)
|
||||
}
|
||||
}
|
||||
|
||||
function handleInstalledState (status: Status): PkgStatusRendering {
|
||||
if (!status.configured) {
|
||||
return { display: 'Needs Config', color: 'warning', showDots: false, feStatus: FEStatus.NeedsConfig }
|
||||
}
|
||||
|
||||
if (Object.values(status['dependency-errors']).length) {
|
||||
return { display: 'Dependency Issue', color: 'warning', showDots: false, feStatus: FEStatus.DependencyIssue }
|
||||
}
|
||||
|
||||
switch (status.main.status) {
|
||||
case PackageMainStatus.Running: return { display: 'Running', color: 'success', showDots: false, feStatus: FEStatus.Running }
|
||||
case PackageMainStatus.Stopping: return { display: 'Stopping', color: 'dark', showDots: true, feStatus: FEStatus.Stopping }
|
||||
case PackageMainStatus.Stopped: return { display: 'Stopped', color: 'medium', showDots: false, feStatus: FEStatus.Stopped }
|
||||
case PackageMainStatus.BackingUp: return { display: 'Backing Up', color: 'warning', showDots: true, feStatus: FEStatus.BackingUp }
|
||||
case PackageMainStatus.Restoring: return { display: 'Restoring', color: 'primary', showDots: true, feStatus: FEStatus.Restoring }
|
||||
}
|
||||
}
|
||||
|
||||
export interface PkgStatusRendering {
|
||||
feStatus: FEStatus
|
||||
display: string
|
||||
color: string
|
||||
showDots: boolean
|
||||
}
|
||||
|
||||
// aggregate of all pkg statuses, except for Installed, which implies a "main" or "FE" status
|
||||
export enum FEStatus {
|
||||
// pkg
|
||||
Installing = 'installing',
|
||||
Updating = 'updating',
|
||||
Removing = 'removing',
|
||||
// main
|
||||
Running = 'running',
|
||||
Stopping = 'stopping',
|
||||
Stopped = 'stopped',
|
||||
BackingUp = 'backing-up',
|
||||
Restoring = 'restoring',
|
||||
// FE
|
||||
Connecting = 'connecting',
|
||||
DependencyIssue = 'dependency-issue',
|
||||
NeedsConfig = 'needs-config',
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Router } from '@angular/router'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PwaBackService {
|
||||
constructor (
|
||||
private readonly router: Router,
|
||||
private readonly nav: NavController,
|
||||
) { }
|
||||
|
||||
// this will strip an entry from the path on navigation
|
||||
back () {
|
||||
return this.nav.back()
|
||||
// this.router.navigate()
|
||||
// const path = this.router.url.split('/').filter(a => a !== '')
|
||||
// path.pop()
|
||||
// this.router.navigate(['/', ...path], { replaceUrl: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,124 +2,108 @@ import { Injectable } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { AppConfigValuePage } from '../modals/app-config-value/app-config-value.page'
|
||||
import { ApiService } from './api/api.service'
|
||||
import { PropertySubject } from '../util/property-subject.util'
|
||||
import { S9Server, ServerModel } from '../models/server-model'
|
||||
import { ValueSpec } from '../app-config/config-types'
|
||||
import { ConfigSpec } from '../pkg-config/config-types'
|
||||
import { ConfigCursor } from '../pkg-config/config-cursor'
|
||||
import { SSHService } from '../pages/server-routes/developer-routes/dev-ssh-keys/ssh.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ServerConfigService {
|
||||
server: PropertySubject<S9Server>
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly serverModel: ServerModel,
|
||||
) {
|
||||
this.server = this.serverModel.watch()
|
||||
}
|
||||
private readonly sshService: SSHService,
|
||||
) { }
|
||||
|
||||
async presentModalValueEdit (key: string, current?: string) {
|
||||
const cursor = new ConfigCursor(serverConfig, { [key]: current }).seekNext(key)
|
||||
|
||||
async presentModalValueEdit (key: string, add = false) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
backdropDismiss: false,
|
||||
component: AppConfigValuePage,
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
componentProps: {
|
||||
...this.getConfigSpec(key),
|
||||
value: add ? '' : this.server[key].getValue(),
|
||||
cursor,
|
||||
saveFn: this.saveFns[key],
|
||||
},
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private getConfigSpec (key: string): SpecAndSaveFn {
|
||||
const configSpec: { [key: string]: SpecAndSaveFn } = {
|
||||
name: {
|
||||
spec: {
|
||||
type: 'string',
|
||||
name: 'Device Name',
|
||||
description: 'A unique label for this device.',
|
||||
nullable: false,
|
||||
// @TODO determine regex
|
||||
// pattern: '',
|
||||
patternDescription: 'Must be less than 40 characters',
|
||||
masked: false,
|
||||
copyable: true,
|
||||
},
|
||||
saveFn: (val: string) => {
|
||||
return this.apiService.patchServerConfig('name', val).then(() => this.serverModel.update({ name: val }))
|
||||
},
|
||||
},
|
||||
autoCheckUpdates: {
|
||||
spec: {
|
||||
type: 'boolean',
|
||||
name: 'Auto Check for Updates',
|
||||
description: 'On launch, EmabssyOS will automatically check for updates of itself and your installed services. Updating still requires user approval and action. No updates will ever be performed automatically.',
|
||||
default: true,
|
||||
},
|
||||
saveFn: (val: boolean) => {
|
||||
return this.apiService.patchServerConfig('autoCheckUpdates', val).then(() => this.serverModel.update({ autoCheckUpdates: val }))
|
||||
},
|
||||
},
|
||||
// password: {
|
||||
// spec: {
|
||||
// type: 'string',
|
||||
// name: 'Change Password',
|
||||
// description: 'The master password for your Embassy. Must contain at least 128 bits of entropy.',
|
||||
// nullable: false,
|
||||
// // @TODO figure out how to confirm min entropy
|
||||
// // pattern: '^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[*.!@#$%^&*\]).{12,32}$',
|
||||
// patternDescription: 'Password too simple. Password must contain at least 128 bits of entroy.',
|
||||
// changeWarning: 'Changing your password will have no affect on old backups. In order to restore old backups, you must provide the password that was used to create them.',
|
||||
// masked: true,
|
||||
// copyable: true,
|
||||
// },
|
||||
// saveFn: (val: string) => {
|
||||
// return this.apiService.patchServerConfig('password', val)
|
||||
// },
|
||||
// },
|
||||
// alternativeRegistryUrl: {
|
||||
// spec: {
|
||||
// type: 'string',
|
||||
// name: 'Marketplace URL',
|
||||
// description: 'Used for connecting to an alternative service marketplace.',
|
||||
// nullable: true,
|
||||
// // @TODO regex for URL
|
||||
// // pattern: '',
|
||||
// patternDescription: 'Must be a valid URL',
|
||||
// changeWarning: 'Downloading services from an alternative marketplace could result in malicious or harmful code being installed on your device.',
|
||||
// masked: false,
|
||||
// copyable: true,
|
||||
// },
|
||||
// saveFn: (val: string) => {
|
||||
// return this.apiService.patchServerConfig('alternativeRegistryUrl', val).then(() => this.serverModel.update({ alternativeRegistryUrl: val }))
|
||||
// },
|
||||
// },
|
||||
ssh: {
|
||||
spec: {
|
||||
type: 'string',
|
||||
name: 'SSH Key',
|
||||
description: 'Add SSH keys to your Embassy to gain root access from the command line.',
|
||||
nullable: false,
|
||||
// @TODO regex for SSH Key
|
||||
// pattern: '',
|
||||
patternDescription: 'Must be a valid SSH key',
|
||||
masked: true,
|
||||
copyable: true,
|
||||
},
|
||||
saveFn: (val: string) => {
|
||||
return this.apiService.addSSHKey(val)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return configSpec[key]
|
||||
saveFns: { [key: string]: (val: any) => Promise<any> } = {
|
||||
name: async (value: string) => {
|
||||
return this.apiService.setDbValue({ pointer: 'ui/name', value })
|
||||
},
|
||||
autoCheckUpdates: async (value: boolean) => {
|
||||
return this.apiService.setDbValue({ pointer: 'ui/auto-check-updates', value })
|
||||
},
|
||||
ssh: async (pubkey: string) => {
|
||||
return this.sshService.add(pubkey)
|
||||
},
|
||||
registry: async (url: string) => {
|
||||
return this.apiService.setRegistry({ url })
|
||||
},
|
||||
// password: async (password: string) => {
|
||||
// return this.apiService.updatePassword({ password })
|
||||
// },
|
||||
}
|
||||
}
|
||||
|
||||
interface SpecAndSaveFn {
|
||||
spec: ValueSpec
|
||||
saveFn: (val: any) => Promise<any>
|
||||
const serverConfig: ConfigSpec = {
|
||||
name: {
|
||||
type: 'string',
|
||||
name: 'Device Name',
|
||||
description: 'A unique label for this device.',
|
||||
nullable: false,
|
||||
// @TODO determine regex
|
||||
// pattern: '',
|
||||
patternDescription: 'Must be less than 40 characters',
|
||||
masked: false,
|
||||
copyable: false,
|
||||
},
|
||||
autoCheckUpdates: {
|
||||
type: 'boolean',
|
||||
name: 'Auto Check for Updates',
|
||||
description: 'On launch, EmabssyOS will automatically check for updates of itself and your installed services. Updating still requires user approval and action. No updates will ever be performed automatically.',
|
||||
default: true,
|
||||
},
|
||||
ssh: {
|
||||
type: 'string',
|
||||
name: 'SSH Key',
|
||||
description: 'Add SSH keys to your Embassy to gain root access from the command line.',
|
||||
nullable: false,
|
||||
// @TODO regex for SSH Key
|
||||
// pattern: '',
|
||||
patternDescription: 'Must be a valid SSH key',
|
||||
masked: false,
|
||||
copyable: false,
|
||||
},
|
||||
registry: {
|
||||
type: 'string',
|
||||
name: 'Marketplace URL',
|
||||
description: 'The URL of the service marketplace. By default, your Embassy connects to the official Start9 Embassy Marketplace.',
|
||||
nullable: true,
|
||||
// @TODO regex for URL
|
||||
// pattern: '',
|
||||
patternDescription: 'Must be a valid URL',
|
||||
changeWarning: 'Downloading services from an alternative marketplace can result in malicious or harmful code being installed on your device.',
|
||||
default: 'https://registry.start9.com',
|
||||
masked: false,
|
||||
copyable: false,
|
||||
},
|
||||
// password: {
|
||||
// type: 'string',
|
||||
// name: 'Change Password',
|
||||
// description: 'The master password for your Embassy. Must contain at least 128 bits of entropy.',
|
||||
// nullable: false,
|
||||
// // @TODO figure out how to confirm min entropy
|
||||
// // pattern: '^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[*.!@#$%^&*\]).{12,32}$',
|
||||
// patternDescription: 'Password too simple. Password must contain at least 128 bits of entroy.',
|
||||
// changeWarning: 'Changing your password will have no affect on old backups. In order to restore old backups, you must provide the password that was used to create them.',
|
||||
// masked: true,
|
||||
// copyable: true,
|
||||
// },
|
||||
}
|
||||
|
||||
@@ -5,6 +5,5 @@ import { Injectable } from '@angular/core'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SplitPaneTracker {
|
||||
$menuFixedOpenOnLeft$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||
constructor () { }
|
||||
menuFixedOpenOnLeft$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||
}
|
||||
@@ -3,15 +3,14 @@ import { AlertController, IonicSafeString, ModalController, NavController } from
|
||||
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 { S9Server } from '../models/server-model'
|
||||
import { displayEmver } from '../pipes/emver.pipe'
|
||||
import { V1Status } from './api/api-types'
|
||||
import { ApiService, ReqRes } from './api/api.service'
|
||||
import { ApiService } from './api/api.service'
|
||||
import { RR } from './api/api-types'
|
||||
import { ConfigService } from './config.service'
|
||||
import { Emver } from './emver.service'
|
||||
import { OsUpdateService } from './os-update.service'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({providedIn: 'root' })
|
||||
export class StartupAlertsNotifier {
|
||||
constructor (
|
||||
private readonly alertCtrl: AlertController,
|
||||
@@ -37,13 +36,6 @@ export class StartupAlertsNotifier {
|
||||
display: vl => this.displayOsUpdateCheck(vl),
|
||||
hasRun: this.config.skipStartupAlerts,
|
||||
}
|
||||
const v1StatusUpdate: Check<V1Status> = {
|
||||
name: 'v1Status',
|
||||
shouldRun: s => this.shouldRunOsUpdateCheck(s),
|
||||
check: () => this.v1StatusCheck(),
|
||||
display: s => this.displayV1Check(s),
|
||||
hasRun: this.config.skipStartupAlerts,
|
||||
}
|
||||
const apps: Check<boolean> = {
|
||||
name: 'apps',
|
||||
shouldRun: s => this.shouldRunAppsCheck(s),
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ToastController, NavController } from '@ionic/angular'
|
||||
import { ServerModel, S9Server } from '../models/server-model'
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SyncNotifier {
|
||||
displayedWelcomeMessage = false
|
||||
checkedForUpdates = false
|
||||
|
||||
constructor (
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly serverModel: ServerModel,
|
||||
) { }
|
||||
|
||||
async handleSpecial (server: Readonly<S9Server>): Promise<void> {
|
||||
this.handleNotifications(server)
|
||||
}
|
||||
|
||||
private async handleNotifications (server: Readonly<S9Server>) {
|
||||
const count = server.notifications.length
|
||||
|
||||
if (!count) { return }
|
||||
|
||||
let updates = { } as Partial<S9Server>
|
||||
updates.badge = server.badge + count
|
||||
updates.notifications = []
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: 'Embassy',
|
||||
message: `${count} new notification${count === 1 ? '' : 's'}`,
|
||||
position: 'bottom',
|
||||
duration: 4000,
|
||||
cssClass: 'notification-toast',
|
||||
buttons: [
|
||||
{
|
||||
side: 'start',
|
||||
icon: 'close',
|
||||
handler: () => {
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
side: 'end',
|
||||
text: 'View',
|
||||
handler: () => {
|
||||
this.navCtrl.navigateForward(['/notifications'])
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await toast.present()
|
||||
this.serverModel.update(updates)
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ServerModel } from '../models/server-model'
|
||||
import { ApiService } from './api/api.service'
|
||||
import { tryAll, pauseFor } from '../util/misc.util'
|
||||
import { AppModel } from '../models/app-model'
|
||||
import { SyncNotifier } from './sync.notifier'
|
||||
import { BehaviorSubject, Observable, of, from, Subject, EMPTY } from 'rxjs'
|
||||
import { switchMap, concatMap, catchError, delay, tap } from 'rxjs/operators'
|
||||
import { StartupAlertsNotifier } from './startup-alerts.notifier'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SyncDaemon {
|
||||
private readonly syncInterval = 5000
|
||||
private readonly $sync$ = new BehaviorSubject(false)
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly syncNotifier: SyncNotifier,
|
||||
private readonly startupAlertsNotifier: StartupAlertsNotifier,
|
||||
) {
|
||||
this.$sync$.pipe(
|
||||
switchMap(go => go
|
||||
? this.sync().pipe(delay(this.syncInterval), tap(() => this.$sync$.next(true)))
|
||||
: EMPTY,
|
||||
),
|
||||
).subscribe()
|
||||
}
|
||||
|
||||
start () { this.$sync$.next(true) }
|
||||
stop () { this.$sync$.next(false) }
|
||||
sync (): Observable<void> {
|
||||
return from(this.getServerAndApps()).pipe(
|
||||
concatMap(() => this.syncNotifier.handleSpecial(this.serverModel.peek())),
|
||||
concatMap(() => this.startupAlertsNotifier.runChecks(this.serverModel.peek())),
|
||||
catchError(e => of(console.error(`Exception in sync service`, e))),
|
||||
)
|
||||
}
|
||||
|
||||
private async getServerAndApps (): Promise<void> {
|
||||
const now = new Date()
|
||||
const [serverRes, appsRes] = await tryAll([
|
||||
this.apiService.getServer(),
|
||||
pauseFor(250).then(() => this.apiService.getInstalledApps()),
|
||||
])
|
||||
|
||||
switch (serverRes.result) {
|
||||
case 'resolve': {
|
||||
this.serverModel.update(serverRes.value, now)
|
||||
break
|
||||
}
|
||||
case 'reject': {
|
||||
console.error(`get server request rejected with`, serverRes.value)
|
||||
this.serverModel.markUnreachable()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch (appsRes.result) {
|
||||
case 'resolve': {
|
||||
this.appModel.syncCache(appsRes.value, now)
|
||||
break
|
||||
}
|
||||
case 'reject': {
|
||||
console.error(`get apps request rejected with`, appsRes.value)
|
||||
this.appModel.markAppsUnreachable()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { ModalController } from '@ionic/angular'
|
||||
import { ModalOptions } from '@ionic/core'
|
||||
import { APP_CONFIG_COMPONENT_MAPPING } from '../modals/app-config-injectable/modal-injectable-token'
|
||||
import { AppConfigComponentMapping } from '../modals/app-config-injectable/modal-injectable-type'
|
||||
import { ValueSpec } from '../app-config/config-types'
|
||||
import { ValueSpec } from '../pkg-config/config-types'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -43,7 +43,6 @@ export class TrackingModalController {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
dismiss (val?: any): Promise<boolean> {
|
||||
return this.modalCtrl.dismiss(val)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user