mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
show available marketplace updates in menu (#1613)
* show service updates in menu
This commit is contained in:
@@ -7,5 +7,7 @@
|
||||
(click)="switchCategory(cat)"
|
||||
>
|
||||
{{ cat }}
|
||||
<span *ngIf="cat === 'updates'"> ({{ updatesAvailable }}) </span>
|
||||
<span *ngIf="cat === 'updates' && updatesAvailable">
|
||||
({{ updatesAvailable }})
|
||||
</span>
|
||||
</ion-button>
|
||||
|
||||
@@ -23,7 +23,7 @@ export class CategoriesComponent {
|
||||
category = ''
|
||||
|
||||
@Input()
|
||||
updatesAvailable? = 0
|
||||
updatesAvailable = 0
|
||||
|
||||
@Output()
|
||||
readonly categoryChange = new EventEmitter<string>()
|
||||
|
||||
@@ -7,7 +7,7 @@ export abstract class AbstractMarketplaceService {
|
||||
|
||||
abstract getReleaseNotes(id: string): Observable<Record<string, string>>
|
||||
|
||||
abstract getCategories(): Observable<string[]>
|
||||
abstract getCategories(): Observable<Set<string>>
|
||||
|
||||
abstract getPackages(): Observable<MarketplacePkg[]>
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@ import { GlobalErrorHandler } from './services/global-error-handler.service'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { LocalStorageService } from './services/local-storage.service'
|
||||
import { DataModel } from './services/patch-db/data-model'
|
||||
import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe'
|
||||
|
||||
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
export const APP_PROVIDERS: Provider[] = [
|
||||
FilterPackagesPipe,
|
||||
FormBuilder,
|
||||
IonNav,
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { Observable, of } from 'rxjs'
|
||||
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 { ApiService } from 'src/app/services/api/embassy-api.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
|
||||
@Injectable({
|
||||
@@ -24,8 +26,8 @@ export class PatchDataService extends Observable<DataModel | null> {
|
||||
filter(obj => !isEmptyObject(obj)),
|
||||
take(1),
|
||||
tap(({ ui }) => {
|
||||
// check for updates to EOS
|
||||
this.checkForEosUpdate(ui)
|
||||
// check for updates to EOS and services
|
||||
this.checkForUpdates(ui)
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(ui['ack-welcome'])
|
||||
}),
|
||||
@@ -42,13 +44,17 @@ export class PatchDataService extends Observable<DataModel | null> {
|
||||
private readonly config: ConfigService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly embassyApi: ApiService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
private checkForEosUpdate(ui: UIData): void {
|
||||
private checkForUpdates(ui: UIData): void {
|
||||
if (ui['auto-check-updates'] !== false) {
|
||||
this.eosService.getEOS()
|
||||
this.marketplaceService.getPackages().pipe(take(1)).subscribe()
|
||||
this.marketplaceService.getCategories().pipe(take(1)).subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
LoadingController,
|
||||
ToastController,
|
||||
ToastOptions,
|
||||
} from '@ionic/angular'
|
||||
import { EMPTY, merge, Observable, ObservableInput } from 'rxjs'
|
||||
import { ToastController, ToastOptions } from '@ionic/angular'
|
||||
import { EMPTY, Observable, ObservableInput } from 'rxjs'
|
||||
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 { ConfigService } from 'src/app/services/config.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.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
|
||||
@Injectable()
|
||||
@@ -64,8 +56,6 @@ export class UnreadToastService extends Observable<unknown> {
|
||||
private readonly router: Router,
|
||||
private readonly patchData: PatchDataService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
<ion-item-group class="menu">
|
||||
<ion-menu-toggle *ngFor="let page of pages; let i = index" auto-hide="false">
|
||||
<ion-item
|
||||
*ngIf="
|
||||
page.url !== '/developer' || (localStorageService.showDevTools$ | async)
|
||||
"
|
||||
*ngIf="page.url !== '/developer' || (showDevTools$ | async)"
|
||||
button
|
||||
class="link"
|
||||
color="transparent"
|
||||
@@ -26,19 +24,27 @@
|
||||
{{ page.title }}
|
||||
</ion-label>
|
||||
<ion-icon
|
||||
*ngIf="page.url === '/embassy' && (eosService.showUpdate$ | async)"
|
||||
*ngIf="page.url === '/embassy' && (showEOSUpdate$ | async)"
|
||||
color="success"
|
||||
size="small"
|
||||
name="rocket-outline"
|
||||
></ion-icon>
|
||||
<ion-badge
|
||||
*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"
|
||||
class="badge"
|
||||
>
|
||||
{{ count }}
|
||||
{{ notificaitonCount }}
|
||||
</ion-badge>
|
||||
</ion-item>
|
||||
</ion-menu-toggle>
|
||||
|
||||
@@ -25,10 +25,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-right: 3%;
|
||||
}
|
||||
|
||||
.snek {
|
||||
position: absolute;
|
||||
bottom: 90px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { ConfigService } from '../../services/config.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 { AuthService } from '../../services/auth.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({
|
||||
selector: 'app-menu',
|
||||
@@ -42,19 +46,29 @@ export class MenuComponent {
|
||||
},
|
||||
]
|
||||
|
||||
readonly notification$ = this.patch.watch$(
|
||||
readonly notificationCount$ = this.patch.watch$(
|
||||
'server-info',
|
||||
'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(
|
||||
private readonly config: ConfigService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly patch: PatchDbService,
|
||||
public readonly localStorageService: LocalStorageService,
|
||||
public readonly eosService: EOSService,
|
||||
private readonly localStorageService: LocalStorageService,
|
||||
private readonly eosService: EOSService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
) {}
|
||||
|
||||
get href(): string {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { MenuComponent } from './menu.component'
|
||||
import { SnekModule } from '../snek/snek.module'
|
||||
|
||||
|
||||
@@ -24,11 +24,7 @@ export class MarketplaceListPage {
|
||||
startWith({}),
|
||||
)
|
||||
|
||||
readonly categories$ = this.marketplaceService
|
||||
.getCategories()
|
||||
.pipe(
|
||||
map(categories => new Set(['featured', 'updates', ...categories, 'all'])),
|
||||
)
|
||||
readonly categories$ = this.marketplaceService.getCategories()
|
||||
|
||||
readonly pkgs$: Observable<MarketplacePkg[]> = this.patch
|
||||
.watch$('server-info')
|
||||
|
||||
@@ -7,23 +7,25 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content *ngIf="ui$ | async as ui" class="ion-padding-top">
|
||||
<ion-item-group *ngIf="server$ | async as server">
|
||||
<ion-item-divider>General</ion-item-divider>
|
||||
<ion-item button (click)="presentModalName('Embassy-' + server.id)">
|
||||
<ion-label>Device Name</ion-label>
|
||||
<ion-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-content class="ion-padding-top">
|
||||
<ng-container *ngIf="ui$ | async as ui">
|
||||
<ion-item-group *ngIf="server$ | async as server">
|
||||
<ion-item-divider>General</ion-item-divider>
|
||||
<ion-item button (click)="presentModalName('Embassy-' + server.id)">
|
||||
<ion-label>Device Name</ion-label>
|
||||
<ion-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Marketplace</ion-item-divider>
|
||||
<ion-item
|
||||
button
|
||||
(click)="serverConfig.presentAlert('auto-check-updates', ui['auto-check-updates'] !== false)"
|
||||
>
|
||||
<ion-label>Auto Check for Updates</ion-label>
|
||||
<ion-note slot="end">
|
||||
{{ ui['auto-check-updates'] !== false ? 'Enabled' : 'Disabled' }}
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
<ion-item-divider>Marketplace</ion-item-divider>
|
||||
<ion-item
|
||||
button
|
||||
(click)="serverConfig.presentAlert('auto-check-updates', ui['auto-check-updates'] !== false)"
|
||||
>
|
||||
<ion-label>Auto Check for Updates</ion-label>
|
||||
<ion-note slot="end">
|
||||
{{ ui['auto-check-updates'] !== false ? 'Enabled' : 'Disabled' }}
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import {
|
||||
IonContent,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
ToastController,
|
||||
@@ -20,7 +19,6 @@ import { LocalStorageService } from '../../../services/local-storage.service'
|
||||
styleUrls: ['./preferences.page.scss'],
|
||||
})
|
||||
export class PreferencesPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
clicks = 0
|
||||
|
||||
readonly ui$ = this.patch.watch$('ui')
|
||||
@@ -36,10 +34,6 @@ export class PreferencesPage {
|
||||
readonly serverConfig: ServerConfigService,
|
||||
) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
async presentModalName(placeholder: string): Promise<void> {
|
||||
const options: GenericInputOptions = {
|
||||
title: 'Edit Device Name',
|
||||
|
||||
@@ -223,7 +223,7 @@ export class MockApiService extends ApiService {
|
||||
|
||||
if (path === '/package/v0/info') {
|
||||
return {
|
||||
name: 'Dark9',
|
||||
name: 'Dark69',
|
||||
categories: [
|
||||
'featured',
|
||||
'bitcoin',
|
||||
|
||||
@@ -14,6 +14,15 @@ export const mockPatchData: DataModel = {
|
||||
'auto-check-updates': true,
|
||||
'pkg-order': [],
|
||||
'ack-welcome': '1.0.0',
|
||||
marketplace: {
|
||||
'selected-id': '1234',
|
||||
'known-hosts': {
|
||||
'1234': {
|
||||
name: 'Dark9',
|
||||
url: 'https://test-marketplace.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'server-info': {
|
||||
id: 'abcdefgh',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { LoadingController } from '@ionic/angular'
|
||||
import { Emver, ErrorToastService } from '@start9labs/shared'
|
||||
import {
|
||||
MarketplacePkg,
|
||||
AbstractMarketplaceService,
|
||||
Marketplace,
|
||||
FilterPackagesPipe,
|
||||
MarketplaceData,
|
||||
} 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 { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
@@ -29,12 +30,13 @@ import {
|
||||
@Injectable()
|
||||
export class MarketplaceService extends AbstractMarketplaceService {
|
||||
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
|
||||
> = 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)),
|
||||
)
|
||||
|
||||
@@ -42,48 +44,80 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
.watch$('server-info')
|
||||
.pipe(take(1), shareReplay())
|
||||
|
||||
private readonly categories$: Observable<string[]> = this.marketplace$.pipe(
|
||||
switchMap(({ url }) =>
|
||||
this.serverInfo$.pipe(
|
||||
switchMap(({ id }) =>
|
||||
from(this.getMarketplaceData({ 'server-id': id }, url)),
|
||||
),
|
||||
),
|
||||
),
|
||||
map(({ categories }) => categories),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly pkg$: Observable<MarketplacePkg[]> =
|
||||
this.altMarketplaceData$.pipe(
|
||||
switchMap(data =>
|
||||
private readonly registryData$: Observable<MarketplaceData> =
|
||||
this.uiMarketplaceData$.pipe(
|
||||
switchMap(uiMarketplaceData =>
|
||||
this.serverInfo$.pipe(
|
||||
switchMap(info =>
|
||||
switchMap(({ id }) =>
|
||||
from(
|
||||
this.getMarketplacePkgs(
|
||||
{ page: 1, 'per-page': 100 },
|
||||
this.toMarketplace(data).url,
|
||||
info['eos-version-compat'],
|
||||
this.getMarketplaceData(
|
||||
{ 'server-id': id },
|
||||
this.toMarketplace(uiMarketplaceData).url,
|
||||
),
|
||||
).pipe(tap(() => this.onPackages(data))),
|
||||
).pipe(tap(({ name }) => this.updateName(uiMarketplaceData, name))),
|
||||
),
|
||||
),
|
||||
),
|
||||
catchError(e => {
|
||||
this.errToast.present(e)
|
||||
|
||||
return of([])
|
||||
}),
|
||||
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(
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly emver: Emver,
|
||||
private readonly filterPkgsPipe: FilterPackagesPipe,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -93,15 +127,15 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
}
|
||||
|
||||
getAltMarketplace(): Observable<UIMarketplaceData | undefined> {
|
||||
return this.altMarketplaceData$
|
||||
return this.uiMarketplaceData$
|
||||
}
|
||||
|
||||
getCategories(): Observable<string[]> {
|
||||
getCategories(): Observable<Set<string>> {
|
||||
return this.categories$
|
||||
}
|
||||
|
||||
getPackages(): Observable<MarketplacePkg[]> {
|
||||
return this.pkg$
|
||||
return this.pkgs$
|
||||
}
|
||||
|
||||
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>> {
|
||||
if (this.notes.has(id)) {
|
||||
return of(this.notes.get(id) || {})
|
||||
@@ -216,15 +254,16 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
)
|
||||
}
|
||||
|
||||
private onPackages(data?: UIMarketplaceData) {
|
||||
const { name } = this.toMarketplace(data)
|
||||
|
||||
if (!data?.['selected-id']) {
|
||||
private updateName(
|
||||
uiMarketplaceData: UIMarketplaceData | undefined,
|
||||
name: string,
|
||||
) {
|
||||
if (!uiMarketplaceData?.['selected-id']) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedId = data['selected-id']
|
||||
const knownHosts = data['known-hosts']
|
||||
const selectedId = uiMarketplaceData['selected-id']
|
||||
const knownHosts = uiMarketplaceData['known-hosts']
|
||||
|
||||
if (knownHosts[selectedId].name !== name) {
|
||||
this.api.setDbValue({
|
||||
|
||||
@@ -113,7 +113,7 @@ export const serverConfig: ConfigSpec = {
|
||||
type: 'boolean',
|
||||
name: 'Auto Check for Updates',
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user