mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Feat/update tab (#1865)
* implement updates tab for viewing all updates from all marketplaces in one place * remove auto-check-updates * feat: implement updates page (#1888) * feat: implement updates page * chore: comments * better styling in update tab * rework marketplace service (#1891) * rework marketplace service * remove unneeded ? * fix: refactor marketplace to cache requests Co-authored-by: waterplea <alexander@inkin.ru> Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
committed by
Aiden McClelland
parent
d380cc31fa
commit
26c37ba824
25
frontend/package-lock.json
generated
25
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "embassy-os",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "embassy-os",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.2.1",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^14.1.0",
|
||||
"@angular/common": "^14.1.0",
|
||||
@@ -20,6 +20,7 @@
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"angular-svg-round-progressbar": "^9.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||
@@ -4127,6 +4128,18 @@
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/angular-svg-round-progressbar": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/angular-svg-round-progressbar/-/angular-svg-round-progressbar-9.0.0.tgz",
|
||||
"integrity": "sha512-q8d2AEG9u+GMAMrZY40NgejN5fHwR4iK+rRxtJ7NnMEvvuAMqt9UEtKe0SqVQHvZYE6W16L5J9yaO+TEtfRjpw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^14.0.0",
|
||||
"@angular/core": "^14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
@@ -17628,6 +17641,14 @@
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"angular-svg-round-progressbar": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/angular-svg-round-progressbar/-/angular-svg-round-progressbar-9.0.0.tgz",
|
||||
"integrity": "sha512-q8d2AEG9u+GMAMrZY40NgejN5fHwR4iK+rRxtJ7NnMEvvuAMqt9UEtKe0SqVQHvZYE6W16L5J9yaO+TEtfRjpw==",
|
||||
"requires": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"angular-svg-round-progressbar": "^9.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": null,
|
||||
"auto-check-updates": true,
|
||||
"ack-welcome": "0.3.2.1",
|
||||
"marketplace": {
|
||||
"selected-url": "https://registry.start9.com/",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
|
||||
import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-item',
|
||||
|
||||
@@ -14,7 +14,7 @@ export class ReleaseNotesComponent {
|
||||
|
||||
private selected: string | null = null
|
||||
|
||||
readonly notes$ = this.marketplaceService.fetchReleaseNotes(this.pkgId)
|
||||
readonly notes$ = this.marketplaceService.fetchReleaseNotes$(this.pkgId)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
|
||||
import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-about',
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
} from '@angular/core'
|
||||
import { AlertController, ModalController } from '@ionic/angular'
|
||||
import { displayEmver, Emver, MarkdownComponent } from '@start9labs/shared'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||
import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-additional',
|
||||
@@ -57,7 +57,7 @@ export class AdditionalComponent {
|
||||
}
|
||||
|
||||
async presentModalMd(title: string) {
|
||||
const content = this.marketplaceService.fetchPackageMarkdown(
|
||||
const content = this.marketplaceService.fetchStatic$(
|
||||
this.pkg.manifest.id,
|
||||
title,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
|
||||
import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-dependencies',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
|
||||
import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-package',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NgModule, Pipe, PipeTransform } from '@angular/core'
|
||||
import { MarketplacePkg } from '../types/marketplace-pkg'
|
||||
import { MarketplaceManifest } from '../types/marketplace-manifest'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { MarketplaceManifest, MarketplacePkg } from '../types'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
@Pipe({
|
||||
|
||||
@@ -25,7 +25,4 @@ export * from './pipes/filter-packages.pipe'
|
||||
|
||||
export * from './services/marketplace.service'
|
||||
|
||||
export * from './types/dependency'
|
||||
export * from './types/marketplace-info'
|
||||
export * from './types/marketplace-manifest'
|
||||
export * from './types/marketplace-pkg'
|
||||
export * from './types'
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { MarketplaceInfo } from '../types/marketplace-info'
|
||||
import { MarketplacePkg } from '../types/marketplace-pkg'
|
||||
import {
|
||||
MarketplacePkg,
|
||||
Marketplace,
|
||||
MarketplaceURL,
|
||||
MarketplaceName,
|
||||
StoreData,
|
||||
} from '../types'
|
||||
|
||||
export abstract class AbstractMarketplaceService {
|
||||
abstract getMarketplaceInfo$(): Observable<MarketplaceInfo>
|
||||
abstract getKnownHosts$(): Observable<Record<MarketplaceURL, MarketplaceName>>
|
||||
|
||||
abstract getPackages$(): Observable<MarketplacePkg[]>
|
||||
abstract getSelectedHost$(): Observable<{ url: string; name: string }>
|
||||
|
||||
abstract getPackage(
|
||||
abstract getMarketplace$(): Observable<Marketplace>
|
||||
|
||||
abstract getSelectedStore$(): Observable<StoreData | null>
|
||||
|
||||
abstract getPackage$(
|
||||
id: string,
|
||||
version: string,
|
||||
url?: string,
|
||||
): Observable<MarketplacePkg | undefined>
|
||||
|
||||
abstract fetchReleaseNotes(
|
||||
abstract fetchReleaseNotes$(
|
||||
id: string,
|
||||
url?: string,
|
||||
): Observable<Record<string, string>>
|
||||
|
||||
abstract fetchPackageMarkdown(
|
||||
abstract fetchStatic$(
|
||||
id: string,
|
||||
type: string,
|
||||
url?: string,
|
||||
|
||||
76
frontend/projects/marketplace/src/types.ts
Normal file
76
frontend/projects/marketplace/src/types.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Url } from '@start9labs/shared'
|
||||
|
||||
export type MarketplaceURL = string
|
||||
|
||||
export type MarketplaceName = string
|
||||
|
||||
export type Marketplace = Record<MarketplaceURL, StoreData | null>
|
||||
|
||||
export interface StoreData {
|
||||
info: StoreInfo
|
||||
packages: MarketplacePkg[]
|
||||
}
|
||||
|
||||
export interface StoreInfo {
|
||||
name: MarketplaceName
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
export interface MarketplacePkg {
|
||||
icon: Url
|
||||
license: Url
|
||||
instructions: Url
|
||||
manifest: MarketplaceManifest
|
||||
categories: string[]
|
||||
versions: string[]
|
||||
'dependency-metadata': {
|
||||
[id: string]: {
|
||||
title: string
|
||||
icon: Url
|
||||
}
|
||||
}
|
||||
'published-at': string
|
||||
}
|
||||
|
||||
export interface MarketplaceManifest<T = unknown> {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
description: {
|
||||
short: string
|
||||
long: string
|
||||
}
|
||||
'release-notes': string
|
||||
license: string // type of license
|
||||
'wrapper-repo': Url
|
||||
'upstream-repo': Url
|
||||
'support-site': Url
|
||||
'marketing-site': Url
|
||||
'donation-url': Url | null
|
||||
alerts: {
|
||||
install: string | null
|
||||
uninstall: string | null
|
||||
restore: string | null
|
||||
start: string | null
|
||||
stop: string | null
|
||||
}
|
||||
dependencies: Record<string, Dependency<T>>
|
||||
}
|
||||
|
||||
export interface Dependency<T> {
|
||||
version: string
|
||||
requirement:
|
||||
| {
|
||||
type: 'opt-in'
|
||||
how: string
|
||||
}
|
||||
| {
|
||||
type: 'opt-out'
|
||||
how: string
|
||||
}
|
||||
| {
|
||||
type: 'required'
|
||||
}
|
||||
description: string | null
|
||||
config: T
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export interface Dependency<T> {
|
||||
version: string
|
||||
requirement:
|
||||
| {
|
||||
type: 'opt-in'
|
||||
how: string
|
||||
}
|
||||
| {
|
||||
type: 'opt-out'
|
||||
how: string
|
||||
}
|
||||
| {
|
||||
type: 'required'
|
||||
}
|
||||
description: string | null
|
||||
config: T
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface MarketplaceInfo {
|
||||
name: string
|
||||
categories: string[]
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Url } from '@start9labs/shared'
|
||||
import { Dependency } from './dependency'
|
||||
|
||||
export interface MarketplaceManifest<T = unknown> {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
description: {
|
||||
short: string
|
||||
long: string
|
||||
}
|
||||
'release-notes': string
|
||||
license: string // type of license
|
||||
'wrapper-repo': Url
|
||||
'upstream-repo': Url
|
||||
'support-site': Url
|
||||
'marketing-site': Url
|
||||
'donation-url': Url | null
|
||||
alerts: {
|
||||
install: string | null
|
||||
uninstall: string | null
|
||||
restore: string | null
|
||||
start: string | null
|
||||
stop: string | null
|
||||
}
|
||||
dependencies: Record<string, Dependency<T>>
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Url } from '@start9labs/shared'
|
||||
import { MarketplaceManifest } from './marketplace-manifest'
|
||||
|
||||
export interface MarketplacePkg {
|
||||
icon: Url
|
||||
license: Url
|
||||
instructions: Url
|
||||
manifest: MarketplaceManifest
|
||||
categories: string[]
|
||||
versions: string[]
|
||||
'dependency-metadata': {
|
||||
[id: string]: {
|
||||
title: string
|
||||
icon: Url
|
||||
}
|
||||
}
|
||||
'published-at': string
|
||||
}
|
||||
@@ -47,8 +47,9 @@ export * from './types/workspace-config'
|
||||
|
||||
export * from './tokens/relative-url'
|
||||
|
||||
export * from './util/copy-to-clipboard'
|
||||
export * from './util/base-64'
|
||||
export * from './util/copy-to-clipboard'
|
||||
export * from './util/get-new-entries'
|
||||
export * from './util/get-pkg-id'
|
||||
export * from './util/misc.util'
|
||||
export * from './util/rpc.util'
|
||||
|
||||
12
frontend/projects/shared/src/util/get-new-entries.ts
Normal file
12
frontend/projects/shared/src/util/get-new-entries.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function getNewEntries<T extends Record<any, any>>(prev: T, curr: T): T {
|
||||
return Object.entries(curr).reduce(
|
||||
(result, [key, value]) =>
|
||||
prev[key]
|
||||
? result
|
||||
: {
|
||||
...result,
|
||||
[key]: value,
|
||||
},
|
||||
{} as T,
|
||||
)
|
||||
}
|
||||
@@ -24,6 +24,13 @@ const routes: Routes = [
|
||||
m => m.ServerRoutingModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'updates',
|
||||
canActivate: [AuthGuard],
|
||||
canActivateChild: [AuthGuard],
|
||||
loadChildren: () =>
|
||||
import('./pages/updates/updates.module').then(m => m.UpdatesPageModule),
|
||||
},
|
||||
{
|
||||
path: 'marketplace',
|
||||
canActivate: [AuthGuard],
|
||||
|
||||
@@ -29,9 +29,7 @@
|
||||
name="rocket-outline"
|
||||
></ion-icon>
|
||||
<ion-badge
|
||||
*ngIf="
|
||||
page.url === '/marketplace' && (updateCount$ | async) as updateCount
|
||||
"
|
||||
*ngIf="page.url === '/updates' && (updateCount$ | async) as updateCount"
|
||||
color="success"
|
||||
>
|
||||
{{ updateCount }}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { EOSService } from '../../services/eos.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { iif, Observable } from 'rxjs'
|
||||
import { filter, map, switchMap } from 'rxjs/operators'
|
||||
import { combineLatest, map, Observable, of, startWith } from 'rxjs'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { marketplaceSame, versionLower } from '../../pages/updates/updates.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
@@ -26,6 +27,11 @@ export class MenuComponent {
|
||||
url: '/embassy',
|
||||
icon: 'cube-outline',
|
||||
},
|
||||
{
|
||||
title: 'Updates',
|
||||
url: '/updates',
|
||||
icon: 'globe-outline',
|
||||
},
|
||||
{
|
||||
title: 'Marketplace',
|
||||
url: '/marketplace',
|
||||
@@ -47,21 +53,24 @@ export class MenuComponent {
|
||||
|
||||
readonly showEOSUpdate$ = this.eosService.showUpdate$
|
||||
|
||||
readonly updateCount$: Observable<number> = this.patch
|
||||
.watch$('ui', 'auto-check-updates')
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() =>
|
||||
this.marketplaceService.getUpdates$().pipe(
|
||||
map(arr => {
|
||||
return arr.reduce(
|
||||
(acc, marketplace) => acc + marketplace.pkgs.length,
|
||||
0,
|
||||
)
|
||||
}),
|
||||
),
|
||||
readonly updateCount$: Observable<number> = combineLatest([
|
||||
this.marketplaceService.getMarketplace$(),
|
||||
this.patch.watch$('package-data'),
|
||||
]).pipe(
|
||||
map(([marketplace, local]) =>
|
||||
Object.entries(marketplace).reduce(
|
||||
(length, [url, store]) =>
|
||||
length +
|
||||
(store?.packages.filter(
|
||||
({ manifest }) =>
|
||||
marketplaceSame(manifest, local, url) &&
|
||||
versionLower(manifest, local, this.emver),
|
||||
).length || 0),
|
||||
0,
|
||||
),
|
||||
)
|
||||
),
|
||||
startWith(0),
|
||||
)
|
||||
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
|
||||
@@ -71,5 +80,6 @@ export class MenuComponent {
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly emver: Emver,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ const ICONS = [
|
||||
'cloud-offline-outline',
|
||||
'cloud-upload-outline',
|
||||
'code-outline',
|
||||
'cog-outline',
|
||||
'color-wand-outline',
|
||||
'construct-outline',
|
||||
'copy-outline',
|
||||
@@ -56,12 +55,12 @@ const ICONS = [
|
||||
'newspaper-outline',
|
||||
'notifications-outline',
|
||||
'open-outline',
|
||||
'options-outline',
|
||||
'pencil',
|
||||
'phone-portrait-outline',
|
||||
'play-circle-outline',
|
||||
'play-outline',
|
||||
'power',
|
||||
'pricetag-outline',
|
||||
'pulse',
|
||||
'push-outline',
|
||||
'qr-code-outline',
|
||||
|
||||
@@ -2,14 +2,26 @@
|
||||
<ion-item-group>
|
||||
<ng-container *ngFor="let g of groupsArr">
|
||||
<ion-item-divider>
|
||||
<ion-skeleton-text animated style="width: 120px; height: 16px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 120px; height: 16px"
|
||||
></ion-skeleton-text>
|
||||
</ion-item-divider>
|
||||
<ion-item *ngFor="let r of rowsArr">
|
||||
<ion-avatar *ngIf="showAvatar" slot="start">
|
||||
<ion-skeleton-text [animated]="true"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text animated style="width: 200px; height: 14px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 200px; height: 14px"
|
||||
></ion-skeleton-text>
|
||||
</ion-label>
|
||||
<ion-note slot="end">
|
||||
<ion-skeleton-text animated style="width: 80px; height: 14px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 80px; height: 14px"
|
||||
></ion-skeleton-text>
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
@@ -19,12 +31,21 @@
|
||||
<ng-container *ngIf="!groups">
|
||||
<ion-item-group>
|
||||
<ion-item *ngFor="let r of rowsArr">
|
||||
<ion-avatar *ngIf="showAvatar" slot="start">
|
||||
<ion-skeleton-text [animated]="true"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text animated style="width: 200px; height: 14px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 200px; height: 14px"
|
||||
></ion-skeleton-text>
|
||||
</ion-label>
|
||||
<ion-note slot="end">
|
||||
<ion-skeleton-text animated style="width: 80px; height: 14px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 80px; height: 14px"
|
||||
></ion-skeleton-text>
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Component, Input, OnChanges } from '@angular/core'
|
||||
import { Component, Input } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'skeleton-list',
|
||||
templateUrl: './skeleton-list.component.html',
|
||||
styleUrls: ['./skeleton-list.component.scss'],
|
||||
})
|
||||
export class SkeletonListComponent implements OnChanges {
|
||||
export class SkeletonListComponent {
|
||||
@Input() groups = 0
|
||||
@Input() rows = 3
|
||||
@Input() showAvatar = false
|
||||
groupsArr: number[] = []
|
||||
rowsArr: number[] = []
|
||||
|
||||
ngOnChanges() {
|
||||
ngOnInit() {
|
||||
this.groupsArr = Array(this.groups).fill(0)
|
||||
this.rowsArr = Array(this.rows).fill(0)
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ export class AppListPage {
|
||||
const length = next.length
|
||||
return !length || prev.length !== length
|
||||
}),
|
||||
map(([_, pkgs]) => {
|
||||
return pkgs.sort((a, b) => (b.manifest.title > a.manifest.title ? -1 : 1))
|
||||
}),
|
||||
map(([_, pkgs]) =>
|
||||
pkgs.sort((a, b) => (b.manifest.title > a.manifest.title ? -1 : 1)),
|
||||
),
|
||||
)
|
||||
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<skeleton-list *ngIf="loading" [rows]="3"></skeleton-list>
|
||||
<skeleton-list *ngIf="loading"></skeleton-list>
|
||||
<ion-item-group *ngIf="!loading">
|
||||
<ion-item *ngFor="let metric of metrics | keyvalue : asIsOrder">
|
||||
<ion-label>{{ metric.key }}</ion-label>
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
</ion-row>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col size="12">
|
||||
<ng-container *ngIf="marketplace$ | async as marketplace; else loading">
|
||||
<ng-container *ngIf="store$ | async as store; else loading">
|
||||
<ng-container *ngIf="localPkgs$ | async as localPkgs">
|
||||
<marketplace-categories
|
||||
[categories]="marketplace.categories"
|
||||
[categories]="store.categories"
|
||||
[category]="category"
|
||||
[updatesAvailable]="
|
||||
(marketplace.pkgs | filterPackages: '':'updates':localPkgs).length
|
||||
(store.packages | filterPackages: '':'updates':localPkgs).length
|
||||
"
|
||||
(categoryChange)="onCategoryChange($event)"
|
||||
></marketplace-categories>
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="divider"></div>
|
||||
|
||||
<ion-grid
|
||||
*ngIf="marketplace.pkgs | filterPackages: query:category:localPkgs as filtered"
|
||||
*ngIf="store.packages | filterPackages: query:category:localPkgs as filtered"
|
||||
>
|
||||
<div
|
||||
*ngIf="!filtered.length && category === 'updates'"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map } from 'rxjs'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@@ -12,30 +12,22 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceListPage {
|
||||
private readonly pkgs$ = this.marketplaceService.getPackages$()
|
||||
readonly store$ = this.marketplaceService.getSelectedStore$().pipe(
|
||||
filter(Boolean),
|
||||
map(({ info, packages }) => {
|
||||
const categories = new Set<string>()
|
||||
if (info.categories.includes('featured')) categories.add('featured')
|
||||
categories.add('updates')
|
||||
info.categories.forEach(c => categories.add(c))
|
||||
categories.add('all')
|
||||
|
||||
private readonly categories$ = this.marketplaceService
|
||||
.getMarketplaceInfo$()
|
||||
.pipe(
|
||||
map(({ categories }) => {
|
||||
const set = new Set<string>()
|
||||
if (categories.includes('featured')) set.add('featured')
|
||||
set.add('updates')
|
||||
categories.forEach(c => set.add(c))
|
||||
set.add('all')
|
||||
return set
|
||||
}),
|
||||
)
|
||||
|
||||
readonly marketplace$ = combineLatest([this.pkgs$, this.categories$]).pipe(
|
||||
map(arr => {
|
||||
return { pkgs: arr[0], categories: arr[1] }
|
||||
return { categories, packages }
|
||||
}),
|
||||
)
|
||||
|
||||
readonly localPkgs$ = this.patch.watch$('package-data')
|
||||
|
||||
readonly details$ = this.marketplaceService.getUiMarketplace$().pipe(
|
||||
readonly details$ = this.marketplaceService.getSelectedHost$().pipe(
|
||||
map(({ url, name }) => {
|
||||
let color: string
|
||||
let description: string
|
||||
|
||||
@@ -62,7 +62,7 @@ export class MarketplaceShowControlsComponent {
|
||||
|
||||
async tryInstall() {
|
||||
const currentMarketplace = await firstValueFrom(
|
||||
this.marketplaceService.getUiMarketplace$(),
|
||||
this.marketplaceService.getSelectedHost$(),
|
||||
)
|
||||
const url = this.url || currentMarketplace.url
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export class MarketplaceShowPage {
|
||||
|
||||
readonly pkg$ = this.loadVersion$.pipe(
|
||||
switchMap(version =>
|
||||
this.marketplaceService.getPackage(this.pkgId, version, this.url),
|
||||
this.marketplaceService.getPackage$(this.pkgId, version, this.url),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ export class MarketplacesPage {
|
||||
loader.message = 'Validating marketplace...'
|
||||
await loader.present()
|
||||
|
||||
const name = await this.marketplaceService.validateMarketplace(url)
|
||||
const name = await firstValueFrom(this.marketplaceService.fetchInfo$(url))
|
||||
|
||||
// Save
|
||||
loader.message = 'Saving...'
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { PreferencesPage } from './preferences.page'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PreferencesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharedPipesModule,
|
||||
],
|
||||
declarations: [PreferencesPage],
|
||||
})
|
||||
export class PreferencesPageModule {}
|
||||
@@ -1,30 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="embassy"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title (click)="addClick()">Preferences</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-item-group *ngIf="name$ | async as name">
|
||||
<ion-item-divider>General</ion-item-divider>
|
||||
<ion-item button (click)="presentModalName(name)">
|
||||
<ion-label>Device Name</ion-label>
|
||||
<ion-note slot="end">{{ name.current }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Marketplace</ion-item-divider>
|
||||
<ion-item
|
||||
*ngIf="ui$ | async as ui"
|
||||
button
|
||||
(click)="serverConfig.presentAlert('auto-check-updates', ui['auto-check-updates'])"
|
||||
>
|
||||
<ion-label>Auto Check for Updates</ion-label>
|
||||
<ion-note slot="end">
|
||||
{{ ui['auto-check-updates'] ? 'Enabled' : 'Disabled' }}
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,99 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
LoadingController,
|
||||
ModalController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import {
|
||||
GenericInputComponent,
|
||||
GenericInputOptions,
|
||||
} from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ServerConfigService } from 'src/app/services/server-config.service'
|
||||
import { ClientStorageService } from '../../../services/client-storage.service'
|
||||
import {
|
||||
ServerNameInfo,
|
||||
ServerNameService,
|
||||
} from 'src/app/services/server-name.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'preferences',
|
||||
templateUrl: './preferences.page.html',
|
||||
styleUrls: ['./preferences.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PreferencesPage {
|
||||
clicks = 0
|
||||
|
||||
readonly ui$ = this.patch.watch$('ui')
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
readonly name$ = this.serverNameService.name$
|
||||
|
||||
constructor(
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly api: ApiService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly ClientStorageService: ClientStorageService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly serverNameService: ServerNameService,
|
||||
readonly serverConfig: ServerConfigService,
|
||||
) {}
|
||||
|
||||
async presentModalName(name: ServerNameInfo): Promise<void> {
|
||||
const options: GenericInputOptions = {
|
||||
title: 'Edit Device Name',
|
||||
message: 'This is for your reference only.',
|
||||
label: 'Device Name',
|
||||
useMask: false,
|
||||
placeholder: name.default,
|
||||
nullable: true,
|
||||
initialValue: name.current,
|
||||
buttonText: 'Save',
|
||||
submitFn: (value: string) =>
|
||||
this.setDbValue('name', value || name.default),
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: { options },
|
||||
cssClass: 'alertlike-modal',
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
component: GenericInputComponent,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async setDbValue(key: string, value: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Saving...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.setDbValue([key], value)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
async addClick() {
|
||||
this.clicks++
|
||||
if (this.clicks >= 5) {
|
||||
this.clicks = 0
|
||||
const newVal = this.ClientStorageService.toggleShowDevTools()
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: newVal ? 'Dev tools unlocked' : 'Dev tools hidden',
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
|
||||
await toast.present()
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.clicks = Math.max(this.clicks - 1, 0)
|
||||
}, 10000)
|
||||
}
|
||||
}
|
||||
@@ -48,13 +48,6 @@ const routes: Routes = [
|
||||
m => m.ServerMetricsPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'preferences',
|
||||
loadChildren: () =>
|
||||
import('./preferences/preferences.module').then(
|
||||
m => m.PreferencesPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'restore',
|
||||
loadChildren: () =>
|
||||
|
||||
@@ -24,10 +24,7 @@
|
||||
<ion-item-group *ngIf="server$ | async as server; else loading">
|
||||
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
||||
<ion-item-divider>
|
||||
<ion-text color="dark" *ngIf="cat.key !== 'Power'">
|
||||
{{ cat.key }}
|
||||
</ion-text>
|
||||
<ion-text color="dark" *ngIf="cat.key === 'Power'" (click)="addClick()">
|
||||
<ion-text color="dark" (click)="addClick(cat.key)">
|
||||
{{ cat.key }}
|
||||
</ion-text>
|
||||
</ion-item-divider>
|
||||
|
||||
@@ -4,12 +4,13 @@ import {
|
||||
LoadingController,
|
||||
NavController,
|
||||
ModalController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { ServerNameService } from 'src/app/services/server-name.service'
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { firstValueFrom, Observable, of } from 'rxjs'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||
@@ -17,6 +18,10 @@ import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
|
||||
import { getAllPackages } from '../../../util/get-package-data'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
GenericInputComponent,
|
||||
GenericInputOptions,
|
||||
} from 'src/app/modals/generic-input/generic-input.component'
|
||||
|
||||
@Component({
|
||||
selector: 'server-show',
|
||||
@@ -24,7 +29,8 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
styleUrls: ['server-show.page.scss'],
|
||||
})
|
||||
export class ServerShowPage {
|
||||
clicks = 0
|
||||
settingsClicks = 0
|
||||
powerClicks = 0
|
||||
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
readonly name$ = this.serverNameService.name$
|
||||
@@ -44,8 +50,35 @@ export class ServerShowPage {
|
||||
private readonly ClientStorageService: ClientStorageService,
|
||||
private readonly serverNameService: ServerNameService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) {}
|
||||
|
||||
async presentModalName(): Promise<void> {
|
||||
const name = await firstValueFrom(this.name$)
|
||||
|
||||
const options: GenericInputOptions = {
|
||||
title: 'Edit Device Name',
|
||||
message: 'This is for your reference only.',
|
||||
label: 'Device Name',
|
||||
useMask: false,
|
||||
placeholder: name.default,
|
||||
nullable: true,
|
||||
initialValue: name.current,
|
||||
buttonText: 'Save',
|
||||
submitFn: (value: string) =>
|
||||
this.setDbValue('name', value || name.default),
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: { options },
|
||||
cssClass: 'alertlike-modal',
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
component: GenericInputComponent,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async updateEos(): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: OSUpdatePage,
|
||||
@@ -170,6 +203,32 @@ export class ServerShowPage {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
addClick(title: string) {
|
||||
switch (title) {
|
||||
case 'Settings':
|
||||
this.addSettingsClick()
|
||||
break
|
||||
case 'Power':
|
||||
this.addPowerClick()
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private async setDbValue(key: string, value: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Saving...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.setDbValue([key], value)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// should wipe cache independent of actual BE logout
|
||||
private logout() {
|
||||
this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e))
|
||||
@@ -311,7 +370,7 @@ export class ServerShowPage {
|
||||
{
|
||||
title: 'Software Update',
|
||||
description: 'Get the latest version of embassyOS',
|
||||
icon: 'cog-outline',
|
||||
icon: 'cloud-download-outline',
|
||||
action: () =>
|
||||
this.eosService.updateAvailable$.getValue()
|
||||
? this.updateEos()
|
||||
@@ -320,14 +379,11 @@ export class ServerShowPage {
|
||||
disabled$: this.eosService.updatingOrBackingUp$,
|
||||
},
|
||||
{
|
||||
title: 'Preferences',
|
||||
description: 'Device name, background tasks',
|
||||
icon: 'options-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['preferences'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
title: 'Device Name',
|
||||
description: 'Edit the local display name of your Embassy',
|
||||
icon: 'pricetag-outline',
|
||||
action: () => this.presentModalName(),
|
||||
detail: false,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
@@ -504,19 +560,31 @@ export class ServerShowPage {
|
||||
],
|
||||
}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
private async addSettingsClick() {
|
||||
this.settingsClicks++
|
||||
if (this.settingsClicks === 5) {
|
||||
this.settingsClicks = 0
|
||||
const newVal = this.ClientStorageService.toggleShowDevTools()
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: newVal ? 'Dev tools unlocked' : 'Dev tools hidden',
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
|
||||
await toast.present()
|
||||
}
|
||||
}
|
||||
|
||||
addClick() {
|
||||
this.clicks++
|
||||
if (this.clicks >= 5) {
|
||||
this.clicks = 0
|
||||
private addPowerClick() {
|
||||
this.powerClicks++
|
||||
if (this.powerClicks === 5) {
|
||||
this.powerClicks = 0
|
||||
this.ClientStorageService.toggleShowDiskRepair()
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.clicks = Math.max(this.clicks - 1, 0)
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
frontend/projects/ui/src/app/pages/updates/updates.module.ts
Normal file
33
frontend/projects/ui/src/app/pages/updates/updates.module.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { FilterUpdatesPipe, UpdatesPage } from './updates.page'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { MarkdownPipeModule, SharedPipesModule } from '@start9labs/shared'
|
||||
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
||||
import { RoundProgressModule } from 'angular-svg-round-progressbar'
|
||||
import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: UpdatesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
BadgeMenuComponentModule,
|
||||
SharedPipesModule,
|
||||
SkeletonListComponentModule,
|
||||
MarkdownPipeModule,
|
||||
RoundProgressModule,
|
||||
InstallProgressPipeModule,
|
||||
],
|
||||
declarations: [UpdatesPage, FilterUpdatesPipe],
|
||||
})
|
||||
export class UpdatesPageModule {}
|
||||
84
frontend/projects/ui/src/app/pages/updates/updates.page.html
Normal file
84
frontend/projects/ui/src/app/pages/updates/updates.page.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Updates</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item-group *ngIf="data$ | async as data">
|
||||
<ng-container *ngFor="let host of data.hosts | keyvalue">
|
||||
<ion-item-divider> {{ host.value }} </ion-item-divider>
|
||||
|
||||
<div class="ion-padding-start ion-padding-bottom">
|
||||
<ion-item *ngIf="data.errors.includes(host.key)">
|
||||
<ion-text color="danger">Request Failed</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ng-container
|
||||
*ngIf="data.marketplace[host.key]?.packages as packages else loading"
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="packages | filterUpdates : data.localPkgs : host.key as updates"
|
||||
>
|
||||
<ion-item *ngFor="let pkg of updates">
|
||||
<ng-container *ngIf="data.localPkgs[pkg.manifest.id] as local">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="'data:image/png;base64,' + pkg.icon | trustUrl" />
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h1>{{ pkg.manifest.title }}</h1>
|
||||
<h2 class="inline">
|
||||
<span>{{ local.manifest.version }}</span>
|
||||
<ion-icon name="arrow-forward"></ion-icon>
|
||||
<ion-text color="success">
|
||||
{{ pkg.manifest.version }}
|
||||
</ion-text>
|
||||
</h2>
|
||||
<p [innerHTML]="pkg.manifest['release-notes'] | markdown"></p>
|
||||
</ion-label>
|
||||
|
||||
<div slot="end">
|
||||
<round-progress
|
||||
*ngIf="local.state === PackageState.Installing else notInstalling"
|
||||
[current]="local['install-progress'] | installProgress"
|
||||
[max]="100"
|
||||
[radius]="24"
|
||||
[stroke]="4"
|
||||
[rounded]="true"
|
||||
color="var(--ion-color-primary)"
|
||||
></round-progress>
|
||||
<ng-template #notInstalling>
|
||||
<ion-spinner
|
||||
*ngIf="queued[pkg.manifest.id] else updateBtn"
|
||||
color="dark"
|
||||
></ion-spinner>
|
||||
<ng-template #updateBtn>
|
||||
<ion-button
|
||||
(click)="update(pkg.manifest.id, host.key)"
|
||||
color="dark"
|
||||
strong
|
||||
>
|
||||
Update
|
||||
</ion-button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="!updates.length">
|
||||
<p>All services are up to date!</p>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #loading>
|
||||
<skeleton-list [showAvatar]="true" [rows]="2"></skeleton-list>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
12
frontend/projects/ui/src/app/pages/updates/updates.page.scss
Normal file
12
frontend/projects/ui/src/app/pages/updates/updates.page.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
ion-avatar {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
ion-label {
|
||||
margin-left: 64px;
|
||||
}
|
||||
|
||||
.name:only-child {
|
||||
display: none;
|
||||
}
|
||||
95
frontend/projects/ui/src/app/pages/updates/updates.page.ts
Normal file
95
frontend/projects/ui/src/app/pages/updates/updates.page.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import {
|
||||
AbstractMarketplaceService,
|
||||
Marketplace,
|
||||
MarketplaceManifest,
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { combineLatest, Observable } from 'rxjs'
|
||||
import { PrimaryRendering } from '../../services/pkg-status-rendering.service'
|
||||
|
||||
interface UpdatesData {
|
||||
hosts: Record<string, string>
|
||||
marketplace: Marketplace
|
||||
localPkgs: Record<string, PackageDataEntry>
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'updates',
|
||||
templateUrl: 'updates.page.html',
|
||||
styleUrls: ['updates.page.scss'],
|
||||
})
|
||||
export class UpdatesPage {
|
||||
queued: Record<string, boolean> = {}
|
||||
|
||||
readonly data$: Observable<UpdatesData> = combineLatest({
|
||||
hosts: this.marketplaceService.getKnownHosts$(),
|
||||
marketplace: this.marketplaceService.getMarketplace$(),
|
||||
localPkgs: this.patch.watch$('package-data'),
|
||||
errors: this.marketplaceService.getRequestErrors$(),
|
||||
})
|
||||
|
||||
readonly PackageState = PackageState
|
||||
readonly rendering = PrimaryRendering[PackageState.Installing]
|
||||
|
||||
constructor(
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
async update(id: string, url: string): Promise<void> {
|
||||
this.queued[id] = true
|
||||
this.api.installPackage({ id, 'marketplace-url': url })
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'filterUpdates',
|
||||
})
|
||||
export class FilterUpdatesPipe implements PipeTransform {
|
||||
constructor(private readonly emver: Emver) {}
|
||||
|
||||
transform(
|
||||
pkgs: MarketplacePkg[],
|
||||
local: Record<string, PackageDataEntry> = {},
|
||||
url: string,
|
||||
): MarketplacePkg[] {
|
||||
return pkgs.filter(
|
||||
({ manifest }) =>
|
||||
marketplaceSame(manifest, local, url) &&
|
||||
versionLower(manifest, local, this.emver),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function marketplaceSame(
|
||||
{ id }: MarketplaceManifest,
|
||||
local: Record<string, PackageDataEntry>,
|
||||
url: string,
|
||||
): boolean {
|
||||
return local[id]?.installed?.['marketplace-url'] === url
|
||||
}
|
||||
|
||||
export function versionLower(
|
||||
{ version, id }: MarketplaceManifest,
|
||||
local: Record<string, PackageDataEntry>,
|
||||
emver: Emver,
|
||||
): boolean {
|
||||
return (
|
||||
local[id].state === PackageState.Installing ||
|
||||
emver.compare(version, local[id].installed?.manifest.version || '') === 1
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { InstallProgressPipe } from './install-progress.pipe'
|
||||
import {
|
||||
InstallProgressDisplayPipe,
|
||||
InstallProgressPipe,
|
||||
} from './install-progress.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [InstallProgressPipe],
|
||||
exports: [InstallProgressPipe],
|
||||
declarations: [InstallProgressPipe, InstallProgressDisplayPipe],
|
||||
exports: [InstallProgressPipe, InstallProgressDisplayPipe],
|
||||
})
|
||||
export class InstallProgressPipeModule {}
|
||||
|
||||
@@ -3,11 +3,21 @@ import { InstallProgress } from 'src/app/services/patch-db/data-model'
|
||||
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
|
||||
|
||||
@Pipe({
|
||||
name: 'installProgressDisplay',
|
||||
name: 'installProgress',
|
||||
})
|
||||
export class InstallProgressPipe implements PipeTransform {
|
||||
transform(installProgress?: InstallProgress): number {
|
||||
return packageLoadingProgress(installProgress)?.totalProgress || 0
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'installProgressDisplay',
|
||||
})
|
||||
export class InstallProgressDisplayPipe implements PipeTransform {
|
||||
transform(installProgress?: InstallProgress): string {
|
||||
const totalProgress = packageLoadingProgress(installProgress)?.totalProgress || 0
|
||||
const totalProgress =
|
||||
packageLoadingProgress(installProgress)?.totalProgress || 0
|
||||
|
||||
return totalProgress < 99 ? totalProgress + '%' : 'finalizing'
|
||||
}
|
||||
|
||||
@@ -1787,7 +1787,7 @@ export module Mock {
|
||||
},
|
||||
'current-dependencies': {},
|
||||
'dependency-info': {},
|
||||
'marketplace-url': 'https://marketplace-url.com',
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
},
|
||||
'install-progress': undefined,
|
||||
@@ -1836,7 +1836,7 @@ export module Mock {
|
||||
icon: 'assets/img/service-icons/bitcoind.png',
|
||||
},
|
||||
},
|
||||
'marketplace-url': 'https://marketplace-url.com',
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
},
|
||||
'install-progress': undefined,
|
||||
@@ -1896,7 +1896,7 @@ export module Mock {
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
},
|
||||
},
|
||||
'marketplace-url': 'https://marketplace-url.com',
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
},
|
||||
'install-progress': undefined,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Dump, Revision } from 'patch-db-client'
|
||||
import { MarketplaceInfo, MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace'
|
||||
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import {
|
||||
@@ -241,8 +241,8 @@ export module RR {
|
||||
|
||||
// marketplace
|
||||
|
||||
export type GetMarketplaceDataReq = { 'server-id': string }
|
||||
export type GetMarketplaceDataRes = MarketplaceInfo
|
||||
export type GetMarketplaceInfoReq = { 'server-id': string }
|
||||
export type GetMarketplaceInfoRes = StoreInfo
|
||||
|
||||
export type GetMarketplaceEOSReq = {
|
||||
'server-id': string
|
||||
|
||||
@@ -567,7 +567,6 @@ export class MockApiService extends ApiService {
|
||||
...Mock.LocalPkgs[params.id],
|
||||
state: PackageState.Installing,
|
||||
'install-progress': { ...PROGRESS },
|
||||
installed: undefined,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
PackageMainStatus,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { Mock } from './api.fixures'
|
||||
|
||||
export const mockPatchData: DataModel = {
|
||||
ui: {
|
||||
name: `Matt's Embassy`,
|
||||
'auto-check-updates': true,
|
||||
'pkg-order': [],
|
||||
'ack-welcome': '1.0.0',
|
||||
marketplace: {
|
||||
@@ -382,7 +382,10 @@ export const mockPatchData: DataModel = {
|
||||
dependencies: {},
|
||||
},
|
||||
installed: {
|
||||
manifest: {} as Manifest,
|
||||
manifest: {
|
||||
...Mock.MockManifestBitcoind,
|
||||
version: '0.20.0',
|
||||
},
|
||||
'last-backup': null,
|
||||
status: {
|
||||
configured: true,
|
||||
@@ -434,7 +437,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
'current-dependencies': {},
|
||||
'dependency-info': {},
|
||||
'marketplace-url': 'https://marketplace-url.com',
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
},
|
||||
},
|
||||
@@ -648,7 +651,7 @@ export const mockPatchData: DataModel = {
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
},
|
||||
},
|
||||
'marketplace-url': 'https://marketplace-url.com',
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,163 +1,131 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import {
|
||||
MarketplacePkg,
|
||||
AbstractMarketplaceService,
|
||||
MarketplaceInfo,
|
||||
StoreData,
|
||||
Marketplace,
|
||||
StoreInfo,
|
||||
} from '@start9labs/marketplace'
|
||||
import { combineLatest, from, Observable, of } from 'rxjs'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
distinctUntilKeyChanged,
|
||||
from,
|
||||
mergeMap,
|
||||
Observable,
|
||||
of,
|
||||
scan,
|
||||
} from 'rxjs'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
DataModel,
|
||||
Manifest,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
catchError,
|
||||
filter,
|
||||
map,
|
||||
pairwise,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { getServerInfo } from '../util/get-server-info'
|
||||
|
||||
type MarketplaceURL = string
|
||||
interface MarketplaceData {
|
||||
info: MarketplaceInfo | null
|
||||
packages: MarketplacePkg[]
|
||||
}
|
||||
type MasterCache = Map<MarketplaceURL, MarketplaceData>
|
||||
import { getNewEntries } from '@start9labs/shared'
|
||||
|
||||
@Injectable()
|
||||
export class MarketplaceService implements AbstractMarketplaceService {
|
||||
private readonly cache: MasterCache = new Map()
|
||||
private readonly knownHosts$ = this.patch.watch$(
|
||||
'ui',
|
||||
'marketplace',
|
||||
'known-hosts',
|
||||
)
|
||||
|
||||
private readonly uiMarketplace$: Observable<{ url: string; name: string }> =
|
||||
this.patch.watch$('ui', 'marketplace').pipe(
|
||||
distinctUntilChanged(
|
||||
(prev, curr) => prev['selected-url'] === curr['selected-url'],
|
||||
),
|
||||
map(data => {
|
||||
const url = data['selected-url']
|
||||
return {
|
||||
url,
|
||||
name: data['known-hosts'][url],
|
||||
}
|
||||
}),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly marketplaceData$: Observable<MarketplaceData> =
|
||||
this.uiMarketplace$.pipe(
|
||||
switchMap(({ url, name }) =>
|
||||
from(this.loadMarketplace(url)).pipe(
|
||||
tap(data => {
|
||||
this.updateName(url, name, data.info!.name)
|
||||
}),
|
||||
),
|
||||
),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly marketplaceInfo$: Observable<MarketplaceInfo> =
|
||||
this.marketplaceData$.pipe(map(data => data.info!))
|
||||
|
||||
private readonly marketplacePkgs$: Observable<MarketplacePkg[]> =
|
||||
this.marketplaceData$.pipe(map(data => data.packages))
|
||||
|
||||
private readonly updates$: Observable<
|
||||
{ url: string; pkgs: MarketplacePkg[] }[]
|
||||
> = this.patch.watch$('package-data').pipe(
|
||||
take(1), // check once per app instance
|
||||
map(localPkgs => {
|
||||
return Object.values(localPkgs)
|
||||
.filter(localPkg => localPkg.state === PackageState.Installed)
|
||||
.reduce((localPkgMap, pkg) => {
|
||||
const url = pkg.installed!['marketplace-url'] || '' // side-laoded services will not have marketplace-url
|
||||
const cached = this.cache
|
||||
.get(url)
|
||||
?.packages.find(p => p.manifest.id === pkg.manifest.id)
|
||||
if (url && !cached) {
|
||||
const arr = localPkgMap.get(url) || []
|
||||
localPkgMap.set(url, arr.concat(pkg.manifest))
|
||||
}
|
||||
return localPkgMap
|
||||
}, new Map<string, Manifest[]>())
|
||||
}),
|
||||
switchMap(localPkgMap => {
|
||||
const urls = Array.from(localPkgMap.keys())
|
||||
const requests = urls.map(url => {
|
||||
const ids = localPkgMap.get(url)?.map(({ id }) => {
|
||||
return { id, version: '*' }
|
||||
})
|
||||
return from(this.loadPackages({ ids }, url)).pipe(
|
||||
map(pkgs => {
|
||||
const manifests = localPkgMap.get(url)!
|
||||
const filtered = pkgs.filter(pkg => {
|
||||
const localVersion = manifests.find(
|
||||
m => m.id === pkg.manifest.id,
|
||||
)?.version
|
||||
return (
|
||||
localVersion &&
|
||||
this.emver.compare(pkg.manifest.version, localVersion) === 1
|
||||
)
|
||||
})
|
||||
return { url, pkgs: filtered }
|
||||
}),
|
||||
startWith({ url, pkgs: [] }), // needed for combineLatest to emit right away
|
||||
)
|
||||
})
|
||||
return combineLatest(requests)
|
||||
}),
|
||||
private readonly selectedHost$ = this.patch.watch$('ui', 'marketplace').pipe(
|
||||
distinctUntilKeyChanged('selected-url'),
|
||||
map(data => ({
|
||||
url: data['selected-url'],
|
||||
name: data['known-hosts'][data['selected-url']],
|
||||
})),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly marketplace$ = this.knownHosts$.pipe(
|
||||
startWith<Record<string, string>>({}),
|
||||
pairwise(),
|
||||
mergeMap(([prev, curr]) => from(Object.entries(getNewEntries(prev, curr)))),
|
||||
mergeMap(([url, name]) =>
|
||||
this.fetchStore$(url).pipe(
|
||||
map<StoreData, [string, StoreData | null]>(data => {
|
||||
if (data.info) this.updateName(url, name, data.info.name)
|
||||
|
||||
return [url, data]
|
||||
}),
|
||||
startWith<[string, StoreData | null]>([url, null]),
|
||||
),
|
||||
),
|
||||
scan<[string, StoreData | null], Record<string, StoreData | null>>(
|
||||
(requests, [url, store]) => {
|
||||
requests[url] = store
|
||||
|
||||
return requests
|
||||
},
|
||||
{},
|
||||
),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly selectedStore$ = this.selectedHost$.pipe(
|
||||
switchMap(({ url }) => this.marketplace$.pipe(map(m => m[url]))),
|
||||
)
|
||||
|
||||
private readonly requestErrors$ = new BehaviorSubject<string[]>([])
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly emver: Emver,
|
||||
) {}
|
||||
|
||||
getUiMarketplace$(): Observable<{ url: string; name: string }> {
|
||||
return this.uiMarketplace$
|
||||
getKnownHosts$(): Observable<Record<string, string>> {
|
||||
return this.knownHosts$
|
||||
}
|
||||
|
||||
getMarketplaceInfo$(): Observable<MarketplaceInfo> {
|
||||
return this.marketplaceInfo$
|
||||
getSelectedHost$(): Observable<{ url: string; name: string }> {
|
||||
return this.selectedHost$
|
||||
}
|
||||
|
||||
getPackages$(): Observable<MarketplacePkg[]> {
|
||||
return this.marketplacePkgs$
|
||||
getMarketplace$(): Observable<Marketplace> {
|
||||
return this.marketplace$
|
||||
}
|
||||
|
||||
getPackage(
|
||||
getSelectedStore$(): Observable<StoreData | null> {
|
||||
return this.selectedStore$
|
||||
}
|
||||
|
||||
getPackage$(
|
||||
id: string,
|
||||
version: string,
|
||||
url?: string,
|
||||
optionalUrl?: string,
|
||||
): Observable<MarketplacePkg | undefined> {
|
||||
return this.uiMarketplace$.pipe(
|
||||
switchMap(m => {
|
||||
url = url || m.url
|
||||
if (this.cache.has(url)) {
|
||||
const pkg = this.getPkgFromCache(id, version, url)
|
||||
if (pkg) return of(pkg)
|
||||
return this.patch.watch$('ui', 'marketplace').pipe(
|
||||
switchMap(marketplace => {
|
||||
const url = optionalUrl || marketplace['selected-url']
|
||||
|
||||
if (version !== '*' || !marketplace['known-hosts'][url]) {
|
||||
return this.fetchPackage$(id, version, url)
|
||||
}
|
||||
|
||||
if (version === '*') {
|
||||
return from(this.loadPackage(id, url))
|
||||
} else {
|
||||
return from(this.fetchPackage(id, version, url))
|
||||
}
|
||||
return this.selectedStore$.pipe(
|
||||
filter(Boolean),
|
||||
map(s => s.packages.find(p => p.manifest.id === id)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
getUpdates$(): Observable<{ url: string; pkgs: MarketplacePkg[] }[]> {
|
||||
return this.updates$
|
||||
// UI only
|
||||
|
||||
getRequestErrors$(): Observable<string[]> {
|
||||
return this.requestErrors$
|
||||
}
|
||||
|
||||
async installPackage(
|
||||
@@ -174,16 +142,71 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
await this.api.installPackage(params)
|
||||
}
|
||||
|
||||
async validateMarketplace(url: string): Promise<string> {
|
||||
await this.loadInfo(url)
|
||||
return this.cache.get(url)!.info!.name
|
||||
fetchInfo$(url: string): Observable<StoreInfo> {
|
||||
return this.patch
|
||||
.watch$('server-info', 'id')
|
||||
.pipe(
|
||||
switchMap(id =>
|
||||
this.api.marketplaceProxy<RR.GetMarketplaceInfoRes>(
|
||||
'/package/v0/info',
|
||||
{ 'server-d': id },
|
||||
url,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fetchReleaseNotes(
|
||||
private fetchStore$(url: string): Observable<StoreData> {
|
||||
return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe(
|
||||
map(([info, packages]) => ({ info, packages })),
|
||||
catchError(e => {
|
||||
this.requestErrors$.next(this.requestErrors$.value.concat(url))
|
||||
return of(e)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private fetchPackages$(
|
||||
url: string,
|
||||
params: Omit<
|
||||
RR.GetMarketplacePackagesReq,
|
||||
'eos-version-compat' | 'page' | 'per-page'
|
||||
> = {},
|
||||
): Observable<MarketplacePkg[]> {
|
||||
return this.patch.watch$('server-info', 'eos-version-compat').pipe(
|
||||
switchMap(versionCompat => {
|
||||
const qp: RR.GetMarketplacePackagesReq = {
|
||||
...params,
|
||||
'eos-version-compat': versionCompat,
|
||||
page: 1,
|
||||
'per-page': 100,
|
||||
}
|
||||
if (qp.ids) qp.ids = JSON.stringify(qp.ids)
|
||||
|
||||
return this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>(
|
||||
'/package/v0/index',
|
||||
qp,
|
||||
url,
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fetchPackage$(
|
||||
id: string,
|
||||
version: string,
|
||||
url: string,
|
||||
): Observable<MarketplacePkg | undefined> {
|
||||
return this.fetchPackages$(url, { ids: [{ id, version }] }).pipe(
|
||||
map(pkgs => pkgs[0]),
|
||||
)
|
||||
}
|
||||
|
||||
fetchReleaseNotes$(
|
||||
id: string,
|
||||
url?: string,
|
||||
): Observable<Record<string, string>> {
|
||||
return this.uiMarketplace$.pipe(
|
||||
return this.selectedHost$.pipe(
|
||||
switchMap(m => {
|
||||
return from(
|
||||
this.api.marketplaceProxy<Record<string, string>>(
|
||||
@@ -196,12 +219,8 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
)
|
||||
}
|
||||
|
||||
fetchPackageMarkdown(
|
||||
id: string,
|
||||
type: string,
|
||||
url?: string,
|
||||
): Observable<string> {
|
||||
return this.uiMarketplace$.pipe(
|
||||
fetchStatic$(id: string, type: string, url?: string): Observable<string> {
|
||||
return this.selectedHost$.pipe(
|
||||
switchMap(m => {
|
||||
return from(
|
||||
this.api.marketplaceProxy<string>(
|
||||
@@ -214,88 +233,6 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
)
|
||||
}
|
||||
|
||||
private async loadMarketplace(url: string): Promise<MarketplaceData> {
|
||||
const cachedInfo = this.cache.get(url)?.info
|
||||
const [info, packages] = await Promise.all([
|
||||
cachedInfo || this.loadInfo(url),
|
||||
this.loadPackages({}, url),
|
||||
])
|
||||
return { info, packages }
|
||||
}
|
||||
|
||||
private async loadInfo(url: string): Promise<MarketplaceInfo> {
|
||||
const info = await this.fetchInfo(url)
|
||||
this.updateCache(url, info)
|
||||
return info
|
||||
}
|
||||
|
||||
private async loadPackage(
|
||||
id: string,
|
||||
url: string,
|
||||
): Promise<MarketplacePkg | undefined> {
|
||||
const pkgs = await this.loadPackages({ ids: [{ id, version: '*' }] }, url)
|
||||
return pkgs[0]
|
||||
}
|
||||
|
||||
private async loadPackages(
|
||||
params: Omit<
|
||||
RR.GetMarketplacePackagesReq,
|
||||
'eos-version-compat' | 'page' | 'per-page'
|
||||
>,
|
||||
url: string,
|
||||
): Promise<MarketplacePkg[]> {
|
||||
const pkgs = await this.fetchPackages(params, url)
|
||||
this.updateCache(url, undefined, pkgs)
|
||||
return pkgs
|
||||
}
|
||||
|
||||
private async fetchInfo(url: string): Promise<RR.GetMarketplaceDataRes> {
|
||||
const { id } = await getServerInfo(this.patch)
|
||||
|
||||
const params: RR.GetMarketplaceDataReq = {
|
||||
'server-id': id,
|
||||
}
|
||||
|
||||
return this.api.marketplaceProxy<RR.GetMarketplaceDataRes>(
|
||||
'/package/v0/info',
|
||||
params,
|
||||
url,
|
||||
)
|
||||
}
|
||||
|
||||
private async fetchPackage(
|
||||
id: string,
|
||||
version: string,
|
||||
url: string,
|
||||
): Promise<MarketplacePkg | undefined> {
|
||||
const pkgs = await this.fetchPackages({ ids: [{ id, version }] }, url)
|
||||
return pkgs[0]
|
||||
}
|
||||
|
||||
private async fetchPackages(
|
||||
params: Omit<
|
||||
RR.GetMarketplacePackagesReq,
|
||||
'eos-version-compat' | 'page' | 'per-page'
|
||||
>,
|
||||
url: string,
|
||||
): Promise<RR.GetMarketplacePackagesRes> {
|
||||
const qp: RR.GetMarketplacePackagesReq = {
|
||||
...params,
|
||||
'eos-version-compat': (await getServerInfo(this.patch))[
|
||||
'eos-version-compat'
|
||||
],
|
||||
page: 1,
|
||||
'per-page': 100,
|
||||
}
|
||||
if (qp.ids) qp.ids = JSON.stringify(qp.ids)
|
||||
|
||||
return this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>(
|
||||
'/package/v0/index',
|
||||
qp,
|
||||
url,
|
||||
)
|
||||
}
|
||||
|
||||
private async updateName(
|
||||
url: string,
|
||||
name: string,
|
||||
@@ -305,39 +242,4 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
this.api.setDbValue(['marketplace', 'known-hosts', url], newName)
|
||||
}
|
||||
}
|
||||
|
||||
private getPkgFromCache(
|
||||
id: string,
|
||||
version: string,
|
||||
url: string,
|
||||
): MarketplacePkg | undefined {
|
||||
return this.cache.get(url)?.packages.find(p => {
|
||||
const versionIsSame =
|
||||
version === '*' || this.emver.compare(p.manifest.version, version) === 0
|
||||
|
||||
return p.manifest.id === id && versionIsSame
|
||||
})
|
||||
}
|
||||
|
||||
private updateCache(
|
||||
url: string,
|
||||
info?: MarketplaceInfo,
|
||||
pkgs?: MarketplacePkg[],
|
||||
): void {
|
||||
const cache = this.cache.get(url)
|
||||
|
||||
let packages = cache?.packages || []
|
||||
if (pkgs) {
|
||||
const filtered = packages.filter(
|
||||
cachedPkg =>
|
||||
!pkgs.find(pkg => pkg.manifest.id === cachedPkg.manifest.id),
|
||||
)
|
||||
packages = filtered.concat(pkgs)
|
||||
}
|
||||
|
||||
this.cache.set(url, {
|
||||
info: info || cache?.info || null,
|
||||
packages,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class PatchDataService extends Observable<DataModel> {
|
||||
take(1),
|
||||
tap(({ ui }) => {
|
||||
// check for updates to EOS and services
|
||||
this.checkForUpdates(ui)
|
||||
this.checkForUpdates()
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(ui['ack-welcome'])
|
||||
}),
|
||||
@@ -43,12 +43,9 @@ export class PatchDataService extends Observable<DataModel> {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
private checkForUpdates(ui: UIData): void {
|
||||
if (ui['auto-check-updates'] !== false) {
|
||||
this.eosService.getEOS()
|
||||
this.marketplaceService.getMarketplaceInfo$().pipe(take(1)).subscribe()
|
||||
this.marketplaceService.getUpdates$().pipe(take(1)).subscribe()
|
||||
}
|
||||
private checkForUpdates(): void {
|
||||
this.eosService.getEOS()
|
||||
this.marketplaceService.getMarketplace$().pipe(take(1)).subscribe()
|
||||
}
|
||||
|
||||
private async showEosWelcome(ackVersion: string): Promise<void> {
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface DataModel {
|
||||
|
||||
export interface UIData {
|
||||
name: string | null
|
||||
'auto-check-updates': boolean
|
||||
'pkg-order': string[]
|
||||
'ack-welcome': string // EOS emver
|
||||
marketplace: UIMarketplaceData
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AlertInput, AlertButton } from '@ionic/core'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { AlertController, LoadingController } from '@ionic/angular'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ServerConfigService {
|
||||
constructor(
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly embassyApi: ApiService,
|
||||
) {}
|
||||
|
||||
async presentAlert(
|
||||
key: string,
|
||||
current?: any,
|
||||
): Promise<HTMLIonAlertElement | null> {
|
||||
const spec = serverConfig[key]
|
||||
|
||||
let inputs: AlertInput[]
|
||||
let buttons: AlertButton[] = [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async (data: any) => {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Saving...',
|
||||
})
|
||||
loader.present()
|
||||
|
||||
try {
|
||||
await this.saveFns[key](data)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
]
|
||||
|
||||
switch (spec.type) {
|
||||
case 'boolean':
|
||||
inputs = [
|
||||
{
|
||||
name: 'enabled',
|
||||
type: 'radio',
|
||||
label: 'Enabled',
|
||||
value: true,
|
||||
checked: current,
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
type: 'radio',
|
||||
label: 'Disabled',
|
||||
value: false,
|
||||
checked: !current,
|
||||
},
|
||||
]
|
||||
break
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: spec.name,
|
||||
message: spec.description,
|
||||
inputs,
|
||||
buttons,
|
||||
})
|
||||
await alert.present()
|
||||
return alert
|
||||
}
|
||||
|
||||
// async presentModalForm (key: string) {
|
||||
// const modal = await this.modalCtrl.create({
|
||||
// component: AppActionInputPage,
|
||||
// componentProps: {
|
||||
// title: serverConfig[key].name,
|
||||
// spec: (serverConfig[key] as ValueSpecObject).spec,
|
||||
// },
|
||||
// })
|
||||
|
||||
// modal.onWillDismiss().then(res => {
|
||||
// if (!res.data) return
|
||||
// this.saveFns[key](res.data)
|
||||
// })
|
||||
|
||||
// await modal.present()
|
||||
// }
|
||||
|
||||
saveFns: { [key: string]: (val: any) => Promise<any> } = {
|
||||
'auto-check-updates': async (enabled: boolean) => {
|
||||
return this.embassyApi.setDbValue(['auto-check-updates'], enabled)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const serverConfig: ConfigSpec = {
|
||||
'auto-check-updates': {
|
||||
type: 'boolean',
|
||||
name: 'Auto Check for Updates',
|
||||
description:
|
||||
'If enabled, embassyOS will automatically check for updates of itself and 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