mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
ui: factors auto checks
This commit is contained in:
committed by
Aiden McClelland
parent
b717853759
commit
b7821576bb
@@ -3,6 +3,7 @@ import { ServerModel, ServerStatus } from './models/server-model'
|
||||
import { Storage } from '@ionic/storage'
|
||||
import { SyncDaemon } from './services/sync.service'
|
||||
import { AuthService, AuthState } from './services/auth.service'
|
||||
import { GlobalAlertsNotifier } from './services/startup-alerts.notifier'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { Router } from '@angular/router'
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
@@ -65,6 +66,7 @@ export class AppComponent {
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly emver: Emver,
|
||||
private readonly globalAlertsNotifier: GlobalAlertsNotifier,
|
||||
readonly splitPane: SplitPaneTracker,
|
||||
) {
|
||||
// set dark theme
|
||||
@@ -79,6 +81,7 @@ export class AppComponent {
|
||||
await this.storage.ready()
|
||||
await this.authService.restoreCache()
|
||||
await this.emver.init()
|
||||
this.globalAlertsNotifier.init()
|
||||
|
||||
this.authService.listen({
|
||||
[AuthState.VERIFIED]: async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||
import { exists } from 'src/app/util/misc.util'
|
||||
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
|
||||
import { ApiService } from '../../services/api/api.service'
|
||||
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
|
||||
@@ -171,5 +172,5 @@ function validate<T> (t: T, test: (t: T) => Boolean, desc: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const exists = t => !!t
|
||||
|
||||
const defaultUninstallationWarning = serviceName => `Uninstalling ${ serviceName } will result in the deletion of its data.`
|
||||
@@ -16,7 +16,7 @@ export class UpdateOsBannerComponent {
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loader: LoaderService,
|
||||
) {
|
||||
this.updateAvailable$ = this.osUpdateService.watchForUpdateAvailable()
|
||||
this.updateAvailable$ = this.osUpdateService.watchForUpdateAvailable$()
|
||||
}
|
||||
|
||||
ngOnInit () { }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs'
|
||||
import { catchError, map, take, tap } from 'rxjs/operators'
|
||||
import { BehaviorSubject, combineLatest, forkJoin, interval, NextObserver, Observable, Observer, of } from 'rxjs'
|
||||
import { catchError, concatMap, distinctUntilChanged, filter, map, take, tap } from 'rxjs/operators'
|
||||
import { ServerModel, ServerStatus } from '../models/server-model'
|
||||
import { ApiService } from './api/api.service'
|
||||
import { Emver } from './emver.service'
|
||||
@@ -10,26 +10,52 @@ import { Emver } from './emver.service'
|
||||
// call checkForUpdates in marketplace pages, can subscribe globally however
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class OsUpdateService {
|
||||
private readonly $updateAvailable$ = new BehaviorSubject(undefined)
|
||||
// holds version latest if update available, undefined if not.
|
||||
private readonly $updateAvailable$ = new BehaviorSubject<string>(undefined)
|
||||
|
||||
// same as above, but we only update this as a result of auto check
|
||||
// this is because we only pop update alert when it comes from an auto check, not the checkForUpdates() call
|
||||
private readonly $updateAvailableFromAutoCheck$ = new BehaviorSubject<string>(undefined)
|
||||
|
||||
watchForUpdateAvailable$ (): Observable<undefined | string> {
|
||||
return this.$updateAvailable$.asObservable().pipe(distinctUntilChanged())
|
||||
}
|
||||
|
||||
watchForAutoCheckUpdateAvailable$ (): Observable<undefined | string> {
|
||||
return this.$updateAvailableFromAutoCheck$.asObservable().pipe(distinctUntilChanged())
|
||||
}
|
||||
|
||||
constructor (
|
||||
private readonly emver: Emver,
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
) { }
|
||||
) {
|
||||
// watch auto check flag and versionLatest for possible update
|
||||
this.autoCheck$().subscribe(this.$updateAvailableFromAutoCheck$)
|
||||
|
||||
watchForUpdateAvailable (): Observable<undefined | string> {
|
||||
return this.$updateAvailable$.asObservable()
|
||||
// if update is available from auto check, then it's available (not vice versa)
|
||||
this.$updateAvailableFromAutoCheck$.subscribe(this.$updateAvailable$)
|
||||
}
|
||||
|
||||
// undefined when no update available, string for the versionLatest if there is
|
||||
|
||||
private autoCheck$ (): Observable<string> {
|
||||
const { autoCheckUpdates } = this.serverModel.watch()
|
||||
return combineLatest([autoCheckUpdates, interval(5000)]).pipe(
|
||||
filter( ([check, _]) => check),
|
||||
concatMap(() => this.apiService.getVersionLatest()),
|
||||
filter( ({ canUpdate }) => canUpdate),
|
||||
map(({ versionLatest }) => versionLatest),
|
||||
)
|
||||
}
|
||||
|
||||
// can call this imperatively and take the return value as gospel, or watch the $updateAvailable$ subject for the same info.
|
||||
checkForUpdates (): Promise<undefined | string> {
|
||||
return forkJoin([
|
||||
this.apiService.getVersionLatest(),
|
||||
this.serverModel.watch().versionInstalled.pipe(take(1)),
|
||||
this.apiService.getVersionLatest(),
|
||||
]).pipe(
|
||||
map(([vl, vi]) => this.emver.compare(vi, vl.versionLatest) === -1 ? vl.versionLatest : undefined),
|
||||
map(([vi, vl]) => updateIsAvailable(this.emver, vi, vl.versionLatest)),
|
||||
catchError(e => {
|
||||
console.error(`OsUpdateService Error: ${e}`)
|
||||
return of(undefined)
|
||||
@@ -39,10 +65,20 @@ export class OsUpdateService {
|
||||
).toPromise()
|
||||
}
|
||||
|
||||
async checkForAppsUpdate (): Promise<boolean> {
|
||||
const availableApps = await this.apiService.getAvailableApps()
|
||||
return !!availableApps.find(app => this.emver.compare(app.versionInstalled, app.versionLatest) === -1)
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
function updateIsAvailable (e: Emver, vi: string, vl: string): string | undefined {
|
||||
if (!vi || !vl) return undefined
|
||||
return e.compare(vi, vl) === -1 ? vl : undefined
|
||||
}
|
||||
129
ui/src/app/services/startup-alerts.notifier.ts
Normal file
129
ui/src/app/services/startup-alerts.notifier.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AlertController, ModalController, NavController } from '@ionic/angular'
|
||||
import { combineLatest, EMPTY, iif, Observable, of } from 'rxjs'
|
||||
import { concatMap, filter, take } from 'rxjs/operators'
|
||||
import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
|
||||
import { ServerModel } from '../models/server-model'
|
||||
import { exists } from '../util/misc.util'
|
||||
import { ApiService } from './api/api.service'
|
||||
import { ConfigService } from './config.service'
|
||||
import { LoaderService } from './loader.service'
|
||||
import { OsUpdateService } from './os-update.service'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class GlobalAlertsNotifier {
|
||||
constructor (
|
||||
private readonly osUpdateService: OsUpdateService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly server: ServerModel,
|
||||
private readonly apiService: ApiService,
|
||||
) {
|
||||
}
|
||||
|
||||
init () {
|
||||
of({ }).pipe(
|
||||
concatMap(() => this.osWelcome$()),
|
||||
concatMap(() => this.autoUpdateCheck$()),
|
||||
).subscribe()
|
||||
}
|
||||
|
||||
private osWelcome$ (): Observable<void> {
|
||||
const { welcomeAck, versionInstalled } = this.server.watch()
|
||||
|
||||
return combineLatest([ welcomeAck, versionInstalled ]).pipe(
|
||||
filter( ([wa, vi]) => !!vi && !wa),
|
||||
take(1), // we will check and show welcome message at most once per app instance
|
||||
concatMap(([_, vi]) => iif(
|
||||
() => vi === this.config.version,
|
||||
this.presentOsWelcome(vi),
|
||||
EMPTY,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
private autoUpdateCheck$ (): Observable<void> {
|
||||
// this emits iff autoCheck is on and update available
|
||||
return this.osUpdateService.watchForAutoCheckUpdateAvailable$().pipe(
|
||||
filter(exists),
|
||||
concatMap(async (vl) => {
|
||||
const { update } = await this.presentAlertNewOS(vl)
|
||||
if (update) {
|
||||
return this.loader.displayDuringP(
|
||||
this.osUpdateService.updateEmbassyOS(vl),
|
||||
).catch(e => alert(e))
|
||||
}
|
||||
|
||||
try {
|
||||
const newApps = await this.osUpdateService.checkForAppsUpdate()
|
||||
if (newApps) {
|
||||
return this.presentAlertNewApps()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Exception checking for new apps: `, e)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private async presentOsWelcome (vi: string): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
backdropDismiss: false,
|
||||
component: OSWelcomePage,
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
componentProps: { version: vi },
|
||||
})
|
||||
//kick this off async
|
||||
this.apiService.acknowledgeOSWelcome(this.config.version).catch(e => {
|
||||
console.error(`Unable to acknowledge OS welcome`, e)
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async presentAlertNewApps () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: true,
|
||||
header: 'Updates Available!',
|
||||
message: 'New service updates are available in the Marketplace.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'View in Marketplace',
|
||||
handler: () => {
|
||||
return this.navCtrl.navigateForward('/services/marketplace')
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async presentAlertNewOS (versionLatest: string): Promise<{ cancel?: true, update?: true }> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: true,
|
||||
header: 'New EmbassyOS Version!',
|
||||
message: `Update EmbassyOS to version ${versionLatest}?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Not now',
|
||||
role: 'cancel',
|
||||
handler: () => resolve({ cancel: true }),
|
||||
},
|
||||
{
|
||||
text: 'Update',
|
||||
handler: () => resolve({ update: true }),
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
|
||||
import { ApiService } from './api/api.service'
|
||||
import { Emver } from './emver.service'
|
||||
import { LoaderService } from './loader.service'
|
||||
import { OsUpdateService } from './os-update.service'
|
||||
import { filter, take, tap } from 'rxjs/operators'
|
||||
import { exists } from '../util/misc.util'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -24,6 +27,7 @@ export class SyncNotifier {
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly emver: Emver,
|
||||
private readonly osUpdateService: OsUpdateService,
|
||||
) { }
|
||||
|
||||
async handleSpecial (server: Readonly<S9Server>): Promise<void> {
|
||||
@@ -88,88 +92,6 @@ export class SyncNotifier {
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async handleUpdateCheck (server: Readonly<S9Server>) {
|
||||
debugSync('handleUpdateCheck', server)
|
||||
if (!server.autoCheckUpdates || this.checkedForUpdates) return
|
||||
|
||||
this.checkedForUpdates = true
|
||||
debugSync('handleUpdateCheck', 'checkedForUpdates=true')
|
||||
if (server.versionLatest && this.emver.compare(server.versionInstalled, server.versionLatest) === -1) {
|
||||
debugSync('handleUpdateCheck', 'OS Update')
|
||||
// if cancel selected, move on to newApps
|
||||
const { update } = await this.presentAlertNewOS(server.versionLatest)
|
||||
debugSync('handleUpdateCheck', 'OS Update', 'response', update)
|
||||
if (update) {
|
||||
return this.updateEmbassyOS(server.versionLatest).catch(e => alert(e))
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
debugSync('handleUpdateCheck', 'Apps Check')
|
||||
|
||||
const availableApps = await this.apiService.getAvailableApps()
|
||||
if (!!availableApps.find(app => this.emver.compare(app.versionInstalled, app.versionLatest) === -1)) {
|
||||
debugSync('handleUpdateCheck', 'Apps Check', 'new apps found')
|
||||
return this.presentAlertNewApps()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Exception checking for new apps: `, e)
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertNewApps () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: true,
|
||||
header: 'Updates Available!',
|
||||
message: 'New service updates are available in the Marketplace.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'View in Marketplace',
|
||||
handler: () => {
|
||||
return this.navCtrl.navigateForward('/services/marketplace')
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async presentAlertNewOS (versionLatest: string): Promise<{ cancel?: true, update?: true }> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: true,
|
||||
header: 'New EmbassyOS Version!',
|
||||
message: `Update EmbassyOS to version ${versionLatest}?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Not now',
|
||||
role: 'cancel',
|
||||
handler: () => resolve({ cancel: true }),
|
||||
},
|
||||
{
|
||||
text: 'Update',
|
||||
handler: () => resolve({ update: true }),
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
})
|
||||
}
|
||||
|
||||
private async updateEmbassyOS (versionLatest: string) {
|
||||
this.loader
|
||||
.displayDuringAsync(async () => {
|
||||
await this.apiService.updateAgent(versionLatest)
|
||||
this.serverModel.update({ status: ServerStatus.UPDATING })
|
||||
})
|
||||
.catch(e => alert(e))
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: remove
|
||||
|
||||
@@ -158,4 +158,6 @@ export function uniqueBy<T> (ts: T[], uniqueBy: (t: T) => string, prioritize: (t
|
||||
|
||||
export function capitalizeFirstLetter (string: string): string {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
export const exists = t => !!t
|
||||
Reference in New Issue
Block a user