mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 04:53:40 +00:00
refactor: refactor updates page to get rid of ionic (#2459)
This commit is contained in:
@@ -7,7 +7,7 @@ import * as emver from '@start9labs/emver'
|
||||
export class Emver {
|
||||
constructor() {}
|
||||
|
||||
compare(lhs: string, rhs: string): number | null {
|
||||
compare(lhs?: string, rhs?: string): number | null {
|
||||
if (!lhs || !rhs) return null
|
||||
return emver.compare(lhs, rhs)
|
||||
}
|
||||
|
||||
@@ -97,9 +97,7 @@ export class MenuComponent {
|
||||
map(([marketplace, local]) =>
|
||||
Object.entries(marketplace).reduce((list, [_, store]) => {
|
||||
store?.packages.forEach(({ manifest: { id, version } }) => {
|
||||
if (
|
||||
this.emver.compare(version, local[id]?.manifest.version || '') === 1
|
||||
)
|
||||
if (this.emver.compare(version, local[id]?.manifest.version) === 1)
|
||||
list.add(id)
|
||||
})
|
||||
return list
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<span class="link">
|
||||
<tui-svg
|
||||
*ngIf="icon.startsWith('tuiIcon'); else url"
|
||||
class="icon"
|
||||
[src]="icon"
|
||||
></tui-svg>
|
||||
<ng-template #url>
|
||||
<img alt="" class="icon" [src]="icon" />
|
||||
</ng-template>
|
||||
<tui-badged-content [style.--tui-radius.rem]="1.5">
|
||||
<tui-badge-alert *ngIf="badge" size="m" tuiSlot="top">
|
||||
{{ badge }}
|
||||
</tui-badge-alert>
|
||||
<tui-svg
|
||||
*ngIf="icon.startsWith('tuiIcon'); else url"
|
||||
class="icon"
|
||||
[src]="icon"
|
||||
></tui-svg>
|
||||
<ng-template #url>
|
||||
<img alt="" class="icon" [src]="icon" />
|
||||
</ng-template>
|
||||
</tui-badged-content>
|
||||
<label ticker class="title">{{ title }}</label>
|
||||
</span>
|
||||
<span *ngIf="isService" class="side">
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 100%;
|
||||
color: var(--tui-text-01-night);
|
||||
}
|
||||
|
||||
tui-svg.icon {
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
TuiBadgeAlertModule,
|
||||
TuiBadgedContentModule,
|
||||
} from '@taiga-ui/experimental'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { TickerModule } from '@start9labs/shared'
|
||||
import {
|
||||
@@ -32,6 +36,8 @@ import { toRouterLink } from '../../utils/to-router-link'
|
||||
TuiDataListModule,
|
||||
TuiSvgModule,
|
||||
TickerModule,
|
||||
TuiBadgedContentModule,
|
||||
TuiBadgeAlertModule,
|
||||
ActionsComponent,
|
||||
],
|
||||
})
|
||||
@@ -50,6 +56,9 @@ export class CardComponent {
|
||||
@Input()
|
||||
actions: Record<string, readonly Action[]> = {}
|
||||
|
||||
@Input()
|
||||
badge: number | null = null
|
||||
|
||||
get isService(): boolean {
|
||||
return !this.id.includes('/')
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
empty: empty
|
||||
"
|
||||
appCard
|
||||
[badge]="item.key | toNotifications | async"
|
||||
[drawerItem]="item.key"
|
||||
[id]="item.key"
|
||||
[title]="item.value.title"
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ServicesService } from '../../services/services.service'
|
||||
import { toRouterLink } from '../../utils/to-router-link'
|
||||
import { DrawerItemDirective } from './drawer-item.directive'
|
||||
import { SYSTEM_UTILITIES } from '../../constants/system-utilities'
|
||||
import { ToNotificationsPipe } from '../../pipes/to-notifications'
|
||||
|
||||
@Component({
|
||||
selector: 'app-drawer',
|
||||
@@ -34,6 +35,7 @@ import { SYSTEM_UTILITIES } from '../../constants/system-utilities'
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterLink,
|
||||
TuiSvgModule,
|
||||
TuiScrollbarModule,
|
||||
TuiActiveZoneModule,
|
||||
@@ -43,7 +45,7 @@ import { SYSTEM_UTILITIES } from '../../constants/system-utilities'
|
||||
TuiFilterPipeModule,
|
||||
CardComponent,
|
||||
DrawerItemDirective,
|
||||
RouterLink,
|
||||
ToNotificationsPipe,
|
||||
],
|
||||
})
|
||||
export class DrawerComponent {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { TuiRepeatTimesModule } from '@taiga-ui/cdk'
|
||||
|
||||
@Component({
|
||||
selector: 'skeleton-list',
|
||||
template: `
|
||||
<div *tuiRepeatTimes="let index of rows" class="g-action">
|
||||
<div
|
||||
class="tui-skeleton"
|
||||
style="--tui-skeleton-radius: 100%; width: 2.5rem; height: 2.5rem"
|
||||
[hidden]="!showAvatar"
|
||||
></div>
|
||||
<div class="tui-skeleton" style="width: 12rem; height: 0.75rem"></div>
|
||||
<div
|
||||
class="tui-skeleton"
|
||||
style="width: 5rem; height: 0.75rem; margin-left: auto"
|
||||
></div>
|
||||
</div>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [TuiRepeatTimesModule],
|
||||
})
|
||||
export class SkeletonListComponent {
|
||||
@Input() rows = 3
|
||||
@Input() showAvatar = false
|
||||
}
|
||||
@@ -4,6 +4,10 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
|
||||
icon: 'tuiIconSaveLarge',
|
||||
title: 'Backups',
|
||||
},
|
||||
'/portal/system/updates': {
|
||||
icon: 'tuiIconGlobeLarge',
|
||||
title: 'Updates',
|
||||
},
|
||||
'/portal/system/devices': {
|
||||
icon: 'assets/img/icon_transparent.png',
|
||||
title: 'Devices',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { NotificationsService } from '../services/notifications.service'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
@Pipe({
|
||||
name: 'toNotifications',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToNotificationsPipe implements PipeTransform {
|
||||
readonly notifications = inject(NotificationsService)
|
||||
|
||||
transform(id: string): Observable<number> {
|
||||
return this.notifications.getNotifications(id)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import { DesktopService } from '../../services/desktop.service'
|
||||
})
|
||||
export class DektopLoadingService extends Observable<boolean> {
|
||||
private readonly desktop = inject(DesktopService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly loading = this.patch.watch$('ui', 'desktop').pipe(
|
||||
take(1),
|
||||
tap(items => (this.desktop.items = items.filter(Boolean))),
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
appCard
|
||||
@tuiFadeIn
|
||||
[id]="item"
|
||||
[badge]="item | toNotifications | async"
|
||||
[title]="desktopItem.title"
|
||||
[icon]="desktopItem.icon"
|
||||
[routerLink]="desktopItem.routerLink"
|
||||
|
||||
@@ -10,12 +10,8 @@ import { EMPTY_QUERY, TUI_PARENT_STOP } from '@taiga-ui/cdk'
|
||||
import { tuiFadeIn, tuiScaleIn } from '@taiga-ui/core'
|
||||
import { TuiTileComponent, TuiTilesComponent } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { DesktopService } from '../../services/desktop.service'
|
||||
import { Observable } from 'rxjs'
|
||||
import { DektopLoadingService } from './dektop-loading.service'
|
||||
|
||||
@Component({
|
||||
@@ -29,8 +25,7 @@ export class DesktopComponent {
|
||||
|
||||
readonly desktop = inject(DesktopService)
|
||||
readonly loading$ = inject(DektopLoadingService)
|
||||
readonly packages$: Observable<Record<string, PackageDataEntry>> =
|
||||
inject<PatchDB<DataModel>>(PatchDB).watch$('package-data')
|
||||
readonly packages$ = inject(PatchDB<DataModel>).watch$('package-data')
|
||||
|
||||
@ViewChild(TuiTilesComponent)
|
||||
readonly tile?: TuiTilesComponent
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TuiTilesModule } from '@taiga-ui/kit'
|
||||
import { DesktopComponent } from './desktop.component'
|
||||
import { CardComponent } from '../../components/card/card.component'
|
||||
import { ToDesktopItemPipe } from '../../pipes/to-desktop-item'
|
||||
import { ToNotificationsPipe } from '../../pipes/to-notifications'
|
||||
import { DesktopItemDirective } from './desktop-item.directive'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
@@ -29,6 +30,7 @@ const ROUTES: Routes = [
|
||||
RouterModule.forChild(ROUTES),
|
||||
TuiFadeModule,
|
||||
DragScrollerDirective,
|
||||
ToNotificationsPipe,
|
||||
],
|
||||
declarations: [DesktopComponent],
|
||||
exports: [DesktopComponent],
|
||||
|
||||
@@ -2,9 +2,8 @@ import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Observable } from 'rxjs'
|
||||
import { InterfaceAddressesComponentModule } from 'src/app/common/interface-addresses/interface-addresses.module'
|
||||
import { InterfaceInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
interface Context {
|
||||
packageId: string
|
||||
@@ -27,7 +26,7 @@ interface Context {
|
||||
export class ServiceInterfaceModal {
|
||||
readonly context = inject<{ data: Context }>(POLYMORPHEUS_CONTEXT).data
|
||||
|
||||
readonly interfaceInfo$: Observable<InterfaceInfo> = inject(PatchDB).watch$(
|
||||
readonly interfaceInfo$ = inject(PatchDB<DataModel>).watch$(
|
||||
'package-data',
|
||||
this.context.packageId,
|
||||
'installed',
|
||||
|
||||
@@ -36,7 +36,7 @@ export class ServiceComponent {
|
||||
private readonly route = inject(ActivatedRoute)
|
||||
private readonly router = inject(Router)
|
||||
private readonly navigation = inject(NavigationService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { TuiForModule } from '@taiga-ui/cdk'
|
||||
import { TuiSvgModule } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { from, map, Observable } from 'rxjs'
|
||||
import { from, map } from 'rxjs'
|
||||
import { CronJob } from 'cron'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { BackupJob } from 'src/app/services/api/api.types'
|
||||
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
||||
|
||||
@Component({
|
||||
@@ -56,7 +55,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
||||
imports: [CommonModule, TuiForModule, TuiSvgModule, GetBackupIconPipe],
|
||||
})
|
||||
export class BackupsUpcomingComponent {
|
||||
readonly current$: Observable<BackupJob> = inject<PatchDB<DataModel>>(PatchDB)
|
||||
readonly current$ = inject(PatchDB<DataModel>)
|
||||
.watch$('server-info', 'status-info', 'current-backup', 'job')
|
||||
.pipe(map(job => job || {}))
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ interface Package {
|
||||
],
|
||||
})
|
||||
export class BackupsBackupModal {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
readonly context =
|
||||
inject<TuiDialogContext<string[], { btnText: string }>>(
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
|
||||
@@ -75,7 +75,7 @@ export class BackupsRecoverModal {
|
||||
private readonly context =
|
||||
inject<TuiDialogContext<void, RecoverData>>(POLYMORPHEUS_CONTEXT)
|
||||
|
||||
readonly packageData$ = inject<PatchDB<DataModel>>(PatchDB)
|
||||
readonly packageData$ = inject(PatchDB<DataModel>)
|
||||
.watch$('package-data')
|
||||
.pipe(take(1))
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@ const ROUTES: Routes = [
|
||||
import('./backups/backups.component').then(m => m.BackupsComponent),
|
||||
data: toDesktopItem('/portal/system/backups'),
|
||||
},
|
||||
{
|
||||
title: systemTabResolver,
|
||||
path: 'updates',
|
||||
loadComponent: () =>
|
||||
import('./updates/updates.component').then(m => m.UpdatesComponent),
|
||||
data: toDesktopItem('/portal/system/updates'),
|
||||
},
|
||||
{
|
||||
title: systemTabResolver,
|
||||
path: 'snek',
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { NgIf } from '@angular/common'
|
||||
import { Component, inject, Input } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import {
|
||||
AbstractMarketplaceService,
|
||||
MarketplacePkg,
|
||||
MimeTypePipeModule,
|
||||
} from '@start9labs/marketplace'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
isEmptyObject,
|
||||
LoadingService,
|
||||
MarkdownPipeModule,
|
||||
SafeLinksDirective,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
TuiButtonModule,
|
||||
TuiDialogService,
|
||||
TuiLinkModule,
|
||||
TuiLoaderModule,
|
||||
TuiSvgModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiAvatarModule } from '@taiga-ui/experimental'
|
||||
import {
|
||||
TUI_PROMPT,
|
||||
TuiAccordionModule,
|
||||
TuiProgressModule,
|
||||
} from '@taiga-ui/kit'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||
import { InstallProgressPipe } from '../pipes/install-progress.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'updates-item',
|
||||
template: `
|
||||
<tui-accordion-item borders="top-bottom">
|
||||
<div class="g-action">
|
||||
<tui-avatar size="s" [src]="pkg | mimeType | trustUrl" />
|
||||
<div [style.flex]="1">
|
||||
<strong>{{ pkg.manifest.title }}</strong>
|
||||
<div>
|
||||
<!-- @TODO left side should be local['old-manifest'] (or whatever), not manifest. -->
|
||||
{{ local.manifest.version || '' | displayEmver }}
|
||||
<tui-svg src="tuiIconArrowRight"></tui-svg>
|
||||
<span [style.color]="'var(--tui-positive)'">
|
||||
{{ pkg.manifest.version | displayEmver }}
|
||||
</span>
|
||||
</div>
|
||||
<div [style.color]="'var(--tui-negative)'">
|
||||
{{ errors }}
|
||||
</div>
|
||||
</div>
|
||||
<tui-progress-circle
|
||||
*ngIf="local.state === 'updating'; else button"
|
||||
style="color: var(--tui-positive)"
|
||||
[max]="100"
|
||||
[value]="local['install-progress'] | installProgress"
|
||||
></tui-progress-circle>
|
||||
<ng-template #button>
|
||||
<button
|
||||
*ngIf="ready; else queued"
|
||||
tuiButton
|
||||
size="s"
|
||||
[appearance]="errors ? 'secondary-destructive' : 'primary'"
|
||||
(click.stop)="onClick()"
|
||||
>
|
||||
{{ errors ? 'Retry' : 'Update' }}
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #queued>
|
||||
<tui-loader [style.width.rem]="2" [inheritColor]="true"></tui-loader>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-template tuiAccordionItemContent>
|
||||
<strong>What's new</strong>
|
||||
<p
|
||||
safeLinks
|
||||
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
|
||||
></p>
|
||||
<a
|
||||
tuiLink
|
||||
iconAlign="right"
|
||||
icon="tuiIconExternalLink"
|
||||
[routerLink]="'/marketplace/' + pkg.manifest.id"
|
||||
[queryParams]="{ url: url }"
|
||||
>
|
||||
View listing
|
||||
</a>
|
||||
</ng-template>
|
||||
</tui-accordion-item>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: block;
|
||||
--tui-base-03: transparent;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--tui-clear);
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgIf,
|
||||
RouterLink,
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
MimeTypePipeModule,
|
||||
NgDompurifyModule,
|
||||
SafeLinksDirective,
|
||||
SharedPipesModule,
|
||||
TuiProgressModule,
|
||||
TuiAccordionModule,
|
||||
TuiAvatarModule,
|
||||
TuiSvgModule,
|
||||
TuiButtonModule,
|
||||
TuiLinkModule,
|
||||
TuiLoaderModule,
|
||||
InstallProgressPipe,
|
||||
],
|
||||
})
|
||||
export class UpdatesItemComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly marketplace = inject(
|
||||
AbstractMarketplaceService,
|
||||
) as MarketplaceService
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
@Input({ required: true })
|
||||
local!: PackageDataEntry
|
||||
|
||||
@Input({ required: true })
|
||||
url = ''
|
||||
|
||||
get errors(): string {
|
||||
return this.marketplace.updateErrors[this.pkg.manifest.id]
|
||||
}
|
||||
|
||||
get ready(): boolean {
|
||||
return !this.marketplace.updateQueue[this.pkg.manifest.id]
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
delete this.marketplace.updateErrors[id]
|
||||
this.marketplace.updateQueue[id] = true
|
||||
|
||||
if (await hasCurrentDeps(this.patch, this.local.manifest.id)) {
|
||||
await this.dry()
|
||||
} else {
|
||||
await this.update()
|
||||
}
|
||||
}
|
||||
|
||||
private async dry() {
|
||||
const { id, version } = this.pkg.manifest
|
||||
const loader = this.loader
|
||||
.open('Checking dependent services...')
|
||||
.subscribe()
|
||||
|
||||
try {
|
||||
const breakages = await this.api.dryUpdatePackage({
|
||||
id,
|
||||
version,
|
||||
})
|
||||
loader.unsubscribe()
|
||||
|
||||
if (isEmptyObject(breakages)) {
|
||||
await this.update()
|
||||
} else {
|
||||
const proceed = await this.alert(breakages)
|
||||
|
||||
if (proceed) {
|
||||
await this.update()
|
||||
} else {
|
||||
delete this.marketplace.updateQueue[id]
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
delete this.marketplace.updateQueue[id]
|
||||
this.marketplace.updateErrors[id] = e.message
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async update() {
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
try {
|
||||
await this.marketplace.installPackage(id, version, this.url)
|
||||
delete this.marketplace.updateQueue[id]
|
||||
} catch (e: any) {
|
||||
delete this.marketplace.updateQueue[id]
|
||||
this.marketplace.updateErrors[id] = e.message
|
||||
}
|
||||
}
|
||||
|
||||
private async alert(breakages: Breakages): Promise<boolean> {
|
||||
const content: string = `As a result of updating ${this.pkg.manifest.title}, the following services will no longer work properly and may crash:<ul>`
|
||||
const local = await getAllPackages(this.patch)
|
||||
const bullets = Object.keys(breakages)
|
||||
.map(id => `<li><b>${local[id].manifest.title}</b></li>`)
|
||||
.join('')
|
||||
|
||||
return new Promise(async resolve => {
|
||||
this.dialogs
|
||||
.open<boolean>(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content: `${content}${bullets}</ul>`,
|
||||
yes: 'Continue',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.subscribe(response => resolve(response))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Pipe({
|
||||
name: 'filterUpdates',
|
||||
standalone: true,
|
||||
})
|
||||
export class FilterUpdatesPipe implements PipeTransform {
|
||||
private readonly emver = inject(Emver)
|
||||
|
||||
transform(
|
||||
pkgs?: MarketplacePkg[],
|
||||
local?: Record<string, PackageDataEntry | undefined>,
|
||||
): MarketplacePkg[] | null {
|
||||
return (
|
||||
pkgs?.filter(
|
||||
({ manifest }) =>
|
||||
this.emver.compare(
|
||||
manifest.version,
|
||||
local?.[manifest.id]?.manifest.version, // @TODO this won't work, need old version
|
||||
) === 1,
|
||||
) || null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { InstallProgress } from 'src/app/services/patch-db/data-model'
|
||||
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
|
||||
|
||||
@Pipe({
|
||||
name: 'installProgress',
|
||||
standalone: true,
|
||||
})
|
||||
export class InstallProgressPipe implements PipeTransform {
|
||||
transform(installProgress?: InstallProgress): number {
|
||||
return packageLoadingProgress(installProgress)?.totalProgress || 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import {
|
||||
AbstractMarketplaceService,
|
||||
StoreIconComponentModule,
|
||||
} from '@start9labs/marketplace'
|
||||
import { TuiForModule } from '@taiga-ui/cdk'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest } from 'rxjs'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { FilterUpdatesPipe } from './pipes/filter-updates.pipe'
|
||||
import { UpdatesItemComponent } from './components/item.component'
|
||||
import { SkeletonListComponent } from '../../../components/skeleton-list.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
<section *ngFor="let host of data.hosts">
|
||||
<h3 class="g-title">
|
||||
<store-icon
|
||||
[url]="host.url"
|
||||
[marketplace]="config.marketplace"
|
||||
size="26px"
|
||||
></store-icon>
|
||||
{{ host.name }}
|
||||
</h3>
|
||||
<p
|
||||
*ngIf="data.errors.includes(host.url)"
|
||||
[style.color]="'var(--tui-negative)'"
|
||||
>
|
||||
Request Failed
|
||||
</p>
|
||||
<updates-item
|
||||
*ngFor="
|
||||
let pkg of data.mp[host.url]?.packages | filterUpdates : data.local;
|
||||
else: loading;
|
||||
empty: blank
|
||||
"
|
||||
[pkg]="pkg"
|
||||
[local]="data.local[pkg.manifest.id]"
|
||||
[url]="host.url"
|
||||
></updates-item>
|
||||
</section>
|
||||
</ng-container>
|
||||
<ng-template #blank><p>All services are up to date!</p></ng-template>
|
||||
<ng-template #loading>
|
||||
<skeleton-list [showAvatar]="true"></skeleton-list>
|
||||
</ng-template>
|
||||
`,
|
||||
host: { class: 'g-page' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
TuiForModule,
|
||||
StoreIconComponentModule,
|
||||
FilterUpdatesPipe,
|
||||
UpdatesItemComponent,
|
||||
SkeletonListComponent,
|
||||
],
|
||||
})
|
||||
export class UpdatesComponent {
|
||||
private readonly marketplace = inject(
|
||||
AbstractMarketplaceService,
|
||||
) as MarketplaceService
|
||||
|
||||
readonly config = inject(ConfigService)
|
||||
|
||||
readonly data$ = combineLatest({
|
||||
hosts: this.marketplace.getKnownHosts$(true),
|
||||
mp: this.marketplace.getMarketplace$(),
|
||||
local: inject(PatchDB<DataModel>).watch$('package-data'),
|
||||
errors: this.marketplace.getRequestErrors$(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
combineLatest,
|
||||
EMPTY,
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
Observable,
|
||||
pairwise,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NotificationsService {
|
||||
private readonly emver = inject(Emver)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly marketplace = inject(
|
||||
AbstractMarketplaceService,
|
||||
) as MarketplaceService
|
||||
|
||||
private readonly local$ = inject(ConnectionService).connected$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.patch.watch$('package-data').pipe(first())),
|
||||
switchMap(outer =>
|
||||
this.patch.watch$('package-data').pipe(
|
||||
pairwise(),
|
||||
filter(([prev, curr]) =>
|
||||
Object.values(prev).some(
|
||||
p =>
|
||||
!curr[p.manifest.id] ||
|
||||
(p['install-progress'] &&
|
||||
!curr[p.manifest.id]['install-progress']),
|
||||
),
|
||||
),
|
||||
map(([_, curr]) => curr),
|
||||
startWith(outer),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private readonly updates$ = combineLatest([
|
||||
this.marketplace.getMarketplace$(true),
|
||||
this.local$,
|
||||
]).pipe(
|
||||
map(
|
||||
([marketplace, local]) =>
|
||||
Object.entries(marketplace).reduce(
|
||||
(list, [_, store]) =>
|
||||
store?.packages.reduce(
|
||||
(result, { manifest: { id, version } }) =>
|
||||
this.emver.compare(version, local[id]?.manifest.version) === 1
|
||||
? result.add(id)
|
||||
: result,
|
||||
list,
|
||||
) || list,
|
||||
new Set<string>(),
|
||||
).size,
|
||||
),
|
||||
)
|
||||
|
||||
getNotifications(id: string): Observable<number> {
|
||||
switch (id) {
|
||||
case '/portal/system/updates':
|
||||
return this.updates$
|
||||
default:
|
||||
return EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ServicesService extends Observable<readonly PackageDataEntry[]> {
|
||||
private readonly services$ = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly services$ = inject(PatchDB<DataModel>)
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
map(pkgs => Object.values(pkgs)),
|
||||
|
||||
@@ -17,7 +17,7 @@ export class FilterUpdatesPipe implements PipeTransform {
|
||||
({ manifest }) =>
|
||||
this.emver.compare(
|
||||
manifest.version,
|
||||
local[manifest.id]?.manifest.version || '', // @TODO this won't work, need old version
|
||||
local[manifest.id]?.manifest.version, // @TODO this won't work, need old version
|
||||
) === 1,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,5 +44,6 @@ const routes: Routes = [
|
||||
NgDompurifyModule,
|
||||
TuiProgressModule,
|
||||
],
|
||||
exports: [FilterUpdatesPipe, InstallProgressPipe],
|
||||
})
|
||||
export class UpdatesPageModule {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Widget } from 'src/app/services/patch-db/data-model'
|
||||
import { DataModel, Widget } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
@@ -17,7 +17,7 @@ import { BUILT_IN_WIDGETS } from '../widgets'
|
||||
export class AddWidgetComponent {
|
||||
readonly context = inject<TuiDialogContext<Widget>>(POLYMORPHEUS_CONTEXT)
|
||||
|
||||
readonly installed$ = inject(PatchDB).watch$('ui', 'widgets')
|
||||
readonly installed$ = inject(PatchDB<DataModel>).watch$('ui', 'widgets')
|
||||
|
||||
readonly widgets = BUILT_IN_WIDGETS
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getPackageInfo } from 'src/app/util/get-package-info'
|
||||
import { PkgInfo } from 'src/app/types/pkg-info'
|
||||
@@ -21,7 +24,7 @@ export class HealthComponent {
|
||||
'Transitioning',
|
||||
] as const
|
||||
|
||||
readonly data$ = inject(PatchDB)
|
||||
readonly data$ = inject(PatchDB<DataModel>)
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
map(data => {
|
||||
|
||||
@@ -111,7 +111,19 @@ export const mockPatchData: DataModel = {
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
},
|
||||
'package-data': {
|
||||
bitcoind: Mock.bitcoind,
|
||||
lnd: Mock.lnd,
|
||||
bitcoind: {
|
||||
...Mock.bitcoind,
|
||||
manifest: {
|
||||
...Mock.bitcoind.manifest,
|
||||
version: '0.19.0',
|
||||
},
|
||||
},
|
||||
lnd: {
|
||||
...Mock.lnd,
|
||||
manifest: {
|
||||
...Mock.lnd.manifest,
|
||||
version: '0.11.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user