show available marketplace updates in menu (#1613)

* show service updates in menu
This commit is contained in:
Lucy C
2022-07-05 13:02:32 -06:00
committed by GitHub
parent e2d58c2959
commit 88afb756f5
17 changed files with 162 additions and 107 deletions

View File

@@ -7,5 +7,7 @@
(click)="switchCategory(cat)" (click)="switchCategory(cat)"
> >
{{ cat }} {{ cat }}
<span *ngIf="cat === 'updates'"> &nbsp; ({{ updatesAvailable }}) </span> <span *ngIf="cat === 'updates' && updatesAvailable">
&nbsp; ({{ updatesAvailable }})
</span>
</ion-button> </ion-button>

View File

@@ -23,7 +23,7 @@ export class CategoriesComponent {
category = '' category = ''
@Input() @Input()
updatesAvailable? = 0 updatesAvailable = 0
@Output() @Output()
readonly categoryChange = new EventEmitter<string>() readonly categoryChange = new EventEmitter<string>()

View File

@@ -7,7 +7,7 @@ export abstract class AbstractMarketplaceService {
abstract getReleaseNotes(id: string): Observable<Record<string, string>> abstract getReleaseNotes(id: string): Observable<Record<string, string>>
abstract getCategories(): Observable<string[]> abstract getCategories(): Observable<Set<string>>
abstract getPackages(): Observable<MarketplacePkg[]> abstract getPackages(): Observable<MarketplacePkg[]>

View File

@@ -14,10 +14,12 @@ import { GlobalErrorHandler } from './services/global-error-handler.service'
import { AuthService } from './services/auth.service' import { AuthService } from './services/auth.service'
import { LocalStorageService } from './services/local-storage.service' import { LocalStorageService } from './services/local-storage.service'
import { DataModel } from './services/patch-db/data-model' import { DataModel } from './services/patch-db/data-model'
import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig const { useMocks } = require('../../../../config.json') as WorkspaceConfig
export const APP_PROVIDERS: Provider[] = [ export const APP_PROVIDERS: Provider[] = [
FilterPackagesPipe,
FormBuilder, FormBuilder,
IonNav, IonNav,
{ {

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@angular/core' import { Inject, Injectable } from '@angular/core'
import { ModalController } from '@ionic/angular' import { ModalController } from '@ionic/angular'
import { Observable, of } from 'rxjs' import { Observable, of } from 'rxjs'
import { filter, share, switchMap, take, tap } from 'rxjs/operators' import { filter, share, switchMap, take, tap } from 'rxjs/operators'
@@ -11,6 +11,8 @@ import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchMonitorService } from './patch-monitor.service' import { PatchMonitorService } from './patch-monitor.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { AbstractMarketplaceService } from '../../../../../../marketplace/src/services/marketplace.service'
// Get data from PatchDb after is starts and act upon it // Get data from PatchDb after is starts and act upon it
@Injectable({ @Injectable({
@@ -24,8 +26,8 @@ export class PatchDataService extends Observable<DataModel | null> {
filter(obj => !isEmptyObject(obj)), filter(obj => !isEmptyObject(obj)),
take(1), take(1),
tap(({ ui }) => { tap(({ ui }) => {
// check for updates to EOS // check for updates to EOS and services
this.checkForEosUpdate(ui) this.checkForUpdates(ui)
// show eos welcome message // show eos welcome message
this.showEosWelcome(ui['ack-welcome']) this.showEosWelcome(ui['ack-welcome'])
}), }),
@@ -42,13 +44,17 @@ export class PatchDataService extends Observable<DataModel | null> {
private readonly config: ConfigService, private readonly config: ConfigService,
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
) { ) {
super(subscriber => this.stream$.subscribe(subscriber)) super(subscriber => this.stream$.subscribe(subscriber))
} }
private checkForEosUpdate(ui: UIData): void { private checkForUpdates(ui: UIData): void {
if (ui['auto-check-updates'] !== false) { if (ui['auto-check-updates'] !== false) {
this.eosService.getEOS() this.eosService.getEOS()
this.marketplaceService.getPackages().pipe(take(1)).subscribe()
this.marketplaceService.getCategories().pipe(take(1)).subscribe()
} }
} }

View File

@@ -1,19 +1,11 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { import { ToastController, ToastOptions } from '@ionic/angular'
LoadingController, import { EMPTY, Observable, ObservableInput } from 'rxjs'
ToastController,
ToastOptions,
} from '@ionic/angular'
import { EMPTY, merge, Observable, ObservableInput } from 'rxjs'
import { filter, pairwise, switchMap, tap } from 'rxjs/operators' import { filter, pairwise, switchMap, tap } from 'rxjs/operators'
import { ErrorToastService } from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ConfigService } from 'src/app/services/config.service'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDataService } from './patch-data.service' import { PatchDataService } from './patch-data.service'
import { DataModel, ServerInfo } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
// Watch unread notification count to display toast // Watch unread notification count to display toast
@Injectable() @Injectable()
@@ -64,8 +56,6 @@ export class UnreadToastService extends Observable<unknown> {
private readonly router: Router, private readonly router: Router,
private readonly patchData: PatchDataService, private readonly patchData: PatchDataService,
private readonly patch: PatchDbService, private readonly patch: PatchDbService,
private readonly config: ConfigService,
private readonly embassyApi: ApiService,
private readonly toastCtrl: ToastController, private readonly toastCtrl: ToastController,
) { ) {
super(subscriber => this.stream$.subscribe(subscriber)) super(subscriber => this.stream$.subscribe(subscriber))

View File

@@ -5,9 +5,7 @@
<ion-item-group class="menu"> <ion-item-group class="menu">
<ion-menu-toggle *ngFor="let page of pages; let i = index" auto-hide="false"> <ion-menu-toggle *ngFor="let page of pages; let i = index" auto-hide="false">
<ion-item <ion-item
*ngIf=" *ngIf="page.url !== '/developer' || (showDevTools$ | async)"
page.url !== '/developer' || (localStorageService.showDevTools$ | async)
"
button button
class="link" class="link"
color="transparent" color="transparent"
@@ -26,19 +24,27 @@
{{ page.title }} {{ page.title }}
</ion-label> </ion-label>
<ion-icon <ion-icon
*ngIf="page.url === '/embassy' && (eosService.showUpdate$ | async)" *ngIf="page.url === '/embassy' && (showEOSUpdate$ | async)"
color="success" color="success"
size="small" size="small"
name="rocket-outline" name="rocket-outline"
></ion-icon> ></ion-icon>
<ion-badge <ion-badge
*ngIf=" *ngIf="
page.url === '/notifications' && (notification$ | async) as count page.url === '/marketplace' && (updateCount$ | async) as updateCount
"
color="success"
>
{{ updateCount }}
</ion-badge>
<ion-badge
*ngIf="
page.url === '/notifications' &&
(notificationCount$ | async) as notificaitonCount
" "
color="danger" color="danger"
class="badge"
> >
{{ count }} {{ notificaitonCount }}
</ion-badge> </ion-badge>
</ion-item> </ion-item>
</ion-menu-toggle> </ion-menu-toggle>

View File

@@ -25,10 +25,6 @@
} }
} }
.badge {
margin-right: 3%;
}
.snek { .snek {
position: absolute; position: absolute;
bottom: 90px; bottom: 90px;

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component } from '@angular/core' import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { AlertController } from '@ionic/angular' import { AlertController } from '@ionic/angular'
import { ConfigService } from '../../services/config.service' import { ConfigService } from '../../services/config.service'
import { LocalStorageService } from '../../services/local-storage.service' import { LocalStorageService } from '../../services/local-storage.service'
@@ -6,6 +6,10 @@ import { EOSService } from '../../services/eos.service'
import { ApiService } from '../../services/api/embassy-api.service' import { ApiService } from '../../services/api/embassy-api.service'
import { AuthService } from '../../services/auth.service' import { AuthService } from '../../services/auth.service'
import { PatchDbService } from '../../services/patch-db/patch-db.service' import { PatchDbService } from '../../services/patch-db/patch-db.service'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@Component({ @Component({
selector: 'app-menu', selector: 'app-menu',
@@ -42,19 +46,29 @@ export class MenuComponent {
}, },
] ]
readonly notification$ = this.patch.watch$( readonly notificationCount$ = this.patch.watch$(
'server-info', 'server-info',
'unread-notification-count', 'unread-notification-count',
) )
readonly showEOSUpdate$ = this.eosService.showUpdate$
readonly showDevTools$ = this.localStorageService.showDevTools$
readonly updateCount$: Observable<number> = this.marketplaceService
.getUpdates()
.pipe(map(pkgs => pkgs.length))
constructor( constructor(
private readonly config: ConfigService, private readonly config: ConfigService,
private readonly alertCtrl: AlertController, private readonly alertCtrl: AlertController,
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly patch: PatchDbService, private readonly patch: PatchDbService,
public readonly localStorageService: LocalStorageService, private readonly localStorageService: LocalStorageService,
public readonly eosService: EOSService, private readonly eosService: EOSService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
) {} ) {}
get href(): string { get href(): string {

View File

@@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { MenuComponent } from './menu.component' import { MenuComponent } from './menu.component'
import { SnekModule } from '../snek/snek.module' import { SnekModule } from '../snek/snek.module'

View File

@@ -24,11 +24,7 @@ export class MarketplaceListPage {
startWith({}), startWith({}),
) )
readonly categories$ = this.marketplaceService readonly categories$ = this.marketplaceService.getCategories()
.getCategories()
.pipe(
map(categories => new Set(['featured', 'updates', ...categories, 'all'])),
)
readonly pkgs$: Observable<MarketplacePkg[]> = this.patch readonly pkgs$: Observable<MarketplacePkg[]> = this.patch
.watch$('server-info') .watch$('server-info')

View File

@@ -7,23 +7,25 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content *ngIf="ui$ | async as ui" class="ion-padding-top"> <ion-content class="ion-padding-top">
<ion-item-group *ngIf="server$ | async as server"> <ng-container *ngIf="ui$ | async as ui">
<ion-item-divider>General</ion-item-divider> <ion-item-group *ngIf="server$ | async as server">
<ion-item button (click)="presentModalName('Embassy-' + server.id)"> <ion-item-divider>General</ion-item-divider>
<ion-label>Device Name</ion-label> <ion-item button (click)="presentModalName('Embassy-' + server.id)">
<ion-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note> <ion-label>Device Name</ion-label>
</ion-item> <ion-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note>
</ion-item>
<ion-item-divider>Marketplace</ion-item-divider> <ion-item-divider>Marketplace</ion-item-divider>
<ion-item <ion-item
button button
(click)="serverConfig.presentAlert('auto-check-updates', ui['auto-check-updates'] !== false)" (click)="serverConfig.presentAlert('auto-check-updates', ui['auto-check-updates'] !== false)"
> >
<ion-label>Auto Check for Updates</ion-label> <ion-label>Auto Check for Updates</ion-label>
<ion-note slot="end"> <ion-note slot="end">
{{ ui['auto-check-updates'] !== false ? 'Enabled' : 'Disabled' }} {{ ui['auto-check-updates'] !== false ? 'Enabled' : 'Disabled' }}
</ion-note> </ion-note>
</ion-item> </ion-item>
</ion-item-group> </ion-item-group>
</ng-container>
</ion-content> </ion-content>

View File

@@ -1,7 +1,6 @@
import { Component, ViewChild } from '@angular/core' import { Component, ViewChild } from '@angular/core'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { import {
IonContent,
LoadingController, LoadingController,
ModalController, ModalController,
ToastController, ToastController,
@@ -20,7 +19,6 @@ import { LocalStorageService } from '../../../services/local-storage.service'
styleUrls: ['./preferences.page.scss'], styleUrls: ['./preferences.page.scss'],
}) })
export class PreferencesPage { export class PreferencesPage {
@ViewChild(IonContent) content: IonContent
clicks = 0 clicks = 0
readonly ui$ = this.patch.watch$('ui') readonly ui$ = this.patch.watch$('ui')
@@ -36,10 +34,6 @@ export class PreferencesPage {
readonly serverConfig: ServerConfigService, readonly serverConfig: ServerConfigService,
) {} ) {}
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
async presentModalName(placeholder: string): Promise<void> { async presentModalName(placeholder: string): Promise<void> {
const options: GenericInputOptions = { const options: GenericInputOptions = {
title: 'Edit Device Name', title: 'Edit Device Name',

View File

@@ -223,7 +223,7 @@ export class MockApiService extends ApiService {
if (path === '/package/v0/info') { if (path === '/package/v0/info') {
return { return {
name: 'Dark9', name: 'Dark69',
categories: [ categories: [
'featured', 'featured',
'bitcoin', 'bitcoin',

View File

@@ -14,6 +14,15 @@ export const mockPatchData: DataModel = {
'auto-check-updates': true, 'auto-check-updates': true,
'pkg-order': [], 'pkg-order': [],
'ack-welcome': '1.0.0', 'ack-welcome': '1.0.0',
marketplace: {
'selected-id': '1234',
'known-hosts': {
'1234': {
name: 'Dark9',
url: 'https://test-marketplace.com',
},
},
},
}, },
'server-info': { 'server-info': {
id: 'abcdefgh', id: 'abcdefgh',

View File

@@ -1,12 +1,13 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { LoadingController } from '@ionic/angular'
import { Emver, ErrorToastService } from '@start9labs/shared' import { Emver, ErrorToastService } from '@start9labs/shared'
import { import {
MarketplacePkg, MarketplacePkg,
AbstractMarketplaceService, AbstractMarketplaceService,
Marketplace, Marketplace,
FilterPackagesPipe,
MarketplaceData,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { from, Observable, of } from 'rxjs' import { from, Observable, of, Subject } from 'rxjs'
import { RR } from 'src/app/services/api/api.types' import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
@@ -29,12 +30,13 @@ import {
@Injectable() @Injectable()
export class MarketplaceService extends AbstractMarketplaceService { export class MarketplaceService extends AbstractMarketplaceService {
private readonly notes = new Map<string, Record<string, string>>() private readonly notes = new Map<string, Record<string, string>>()
private readonly hasPackages$ = new Subject<boolean>()
private readonly altMarketplaceData$: Observable< private readonly uiMarketplaceData$: Observable<
UIMarketplaceData | undefined UIMarketplaceData | undefined
> = this.patch.watch$('ui', 'marketplace').pipe(shareReplay(1)) > = this.patch.watch$('ui', 'marketplace').pipe(shareReplay(1))
private readonly marketplace$ = this.altMarketplaceData$.pipe( private readonly marketplace$ = this.uiMarketplaceData$.pipe(
map(data => this.toMarketplace(data)), map(data => this.toMarketplace(data)),
) )
@@ -42,48 +44,80 @@ export class MarketplaceService extends AbstractMarketplaceService {
.watch$('server-info') .watch$('server-info')
.pipe(take(1), shareReplay()) .pipe(take(1), shareReplay())
private readonly categories$: Observable<string[]> = this.marketplace$.pipe( private readonly registryData$: Observable<MarketplaceData> =
switchMap(({ url }) => this.uiMarketplaceData$.pipe(
this.serverInfo$.pipe( switchMap(uiMarketplaceData =>
switchMap(({ id }) =>
from(this.getMarketplaceData({ 'server-id': id }, url)),
),
),
),
map(({ categories }) => categories),
shareReplay(1),
)
private readonly pkg$: Observable<MarketplacePkg[]> =
this.altMarketplaceData$.pipe(
switchMap(data =>
this.serverInfo$.pipe( this.serverInfo$.pipe(
switchMap(info => switchMap(({ id }) =>
from( from(
this.getMarketplacePkgs( this.getMarketplaceData(
{ page: 1, 'per-page': 100 }, { 'server-id': id },
this.toMarketplace(data).url, this.toMarketplace(uiMarketplaceData).url,
info['eos-version-compat'],
), ),
).pipe(tap(() => this.onPackages(data))), ).pipe(tap(({ name }) => this.updateName(uiMarketplaceData, name))),
), ),
), ),
), ),
catchError(e => {
this.errToast.present(e)
return of([])
}),
shareReplay(1), shareReplay(1),
) )
private readonly categories$: Observable<Set<string>> =
this.registryData$.pipe(
map(
({ categories }) =>
new Set(['featured', 'updates', ...categories, 'all']),
),
)
private readonly pkgs$: Observable<MarketplacePkg[]> = this.marketplace$.pipe(
switchMap(({ url }) =>
this.serverInfo$.pipe(
switchMap(info =>
from(
this.getMarketplacePkgs(
{ page: 1, 'per-page': 100 },
url,
info['eos-version-compat'],
),
).pipe(tap(() => this.hasPackages$.next(true))),
),
),
),
catchError(e => {
this.errToast.present(e)
return of([])
}),
shareReplay(1),
)
private readonly updates$: Observable<MarketplacePkg[]> =
this.hasPackages$.pipe(
switchMap(() =>
this.patch.watch$('package-data').pipe(
switchMap(localPkgs =>
this.pkgs$.pipe(
map(pkgs => {
return this.filterPkgsPipe.transform(
pkgs,
'',
'updates',
localPkgs,
)
}),
),
),
),
),
)
constructor( constructor(
private readonly api: ApiService, private readonly api: ApiService,
private readonly patch: PatchDbService, private readonly patch: PatchDbService,
private readonly config: ConfigService, private readonly config: ConfigService,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
private readonly emver: Emver, private readonly emver: Emver,
private readonly filterPkgsPipe: FilterPackagesPipe,
) { ) {
super() super()
} }
@@ -93,15 +127,15 @@ export class MarketplaceService extends AbstractMarketplaceService {
} }
getAltMarketplace(): Observable<UIMarketplaceData | undefined> { getAltMarketplace(): Observable<UIMarketplaceData | undefined> {
return this.altMarketplaceData$ return this.uiMarketplaceData$
} }
getCategories(): Observable<string[]> { getCategories(): Observable<Set<string>> {
return this.categories$ return this.categories$
} }
getPackages(): Observable<MarketplacePkg[]> { getPackages(): Observable<MarketplacePkg[]> {
return this.pkg$ return this.pkgs$
} }
getPackage(id: string, version: string): Observable<MarketplacePkg | null> { getPackage(id: string, version: string): Observable<MarketplacePkg | null> {
@@ -133,6 +167,10 @@ export class MarketplaceService extends AbstractMarketplaceService {
) )
} }
getUpdates(): Observable<MarketplacePkg[]> {
return this.updates$
}
getReleaseNotes(id: string): Observable<Record<string, string>> { getReleaseNotes(id: string): Observable<Record<string, string>> {
if (this.notes.has(id)) { if (this.notes.has(id)) {
return of(this.notes.get(id) || {}) return of(this.notes.get(id) || {})
@@ -216,15 +254,16 @@ export class MarketplaceService extends AbstractMarketplaceService {
) )
} }
private onPackages(data?: UIMarketplaceData) { private updateName(
const { name } = this.toMarketplace(data) uiMarketplaceData: UIMarketplaceData | undefined,
name: string,
if (!data?.['selected-id']) { ) {
if (!uiMarketplaceData?.['selected-id']) {
return return
} }
const selectedId = data['selected-id'] const selectedId = uiMarketplaceData['selected-id']
const knownHosts = data['known-hosts'] const knownHosts = uiMarketplaceData['known-hosts']
if (knownHosts[selectedId].name !== name) { if (knownHosts[selectedId].name !== name) {
this.api.setDbValue({ this.api.setDbValue({

View File

@@ -113,7 +113,7 @@ export const serverConfig: ConfigSpec = {
type: 'boolean', type: 'boolean',
name: 'Auto Check for Updates', name: 'Auto Check for Updates',
description: description:
'If enabled, EmbassyOS will automatically check for updates of itself. Updating will still require your approval and action. Updates will never be performed automatically.', 'If enabled, EmbassyOS will automatically check for updates of itself and installed services. Updating will still require your approval and action. Updates will never be performed automatically.',
default: true, default: true,
}, },
} }