fix: fix build after minor merged into major

This commit is contained in:
waterplea
2024-08-15 12:40:49 +04:00
parent b43ad93c54
commit a730543c76
189 changed files with 714 additions and 3652 deletions

View File

@@ -17,4 +17,4 @@ const ROUTES: Routes = [
@NgModule({
imports: [RouterModule.forChild(ROUTES)],
})
export class DiagnosticModule {}
export default class DiagnosticModule {}

View File

@@ -25,14 +25,6 @@
{{ error.code === 15 ? 'Setup Current Drive' : 'Enter Recovery Mode'}}
</button>
<button
tuiButton
appearance="secondary-warning"
(click)="presentAlertSystemRebuild()"
>
System Rebuild
</button>
<button
tuiButton
appearance="secondary-destructive"

View File

@@ -1,6 +1,6 @@
import { TUI_CONFIRM } from '@taiga-ui/kit'
import { Component, Inject } from '@angular/core'
import { WINDOW } from '@ng-web-apis/common'
import { WA_WINDOW } from '@ng-web-apis/common'
import { LoadingService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import { filter } from 'rxjs'
@@ -24,7 +24,7 @@ export class HomePage {
private readonly loader: LoadingService,
private readonly api: ApiService,
private readonly dialogs: TuiDialogService,
@Inject(WINDOW) private readonly window: Window,
@Inject(WA_WINDOW) private readonly window: Window,
) {}
async ngOnInit() {

View File

@@ -0,0 +1,55 @@
import { Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import {
formatProgress,
InitializingComponent,
provideSetupLogsService,
} from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import {
catchError,
defer,
EMPTY,
from,
map,
startWith,
switchMap,
tap,
} from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { StateService } from 'src/app/services/state.service'
@Component({
standalone: true,
template: `
<app-initializing [progress]="progress()" />
`,
providers: [provideSetupLogsService(ApiService)],
styles: ':host { padding: 1rem; }',
imports: [InitializingComponent],
})
export default class InitializingPage {
private readonly api = inject(ApiService)
private readonly state = inject(StateService)
readonly progress = toSignal(
defer(() => from(this.api.initGetProgress())).pipe(
switchMap(({ guid, progress }) =>
this.api
.openWebsocket$<T.FullProgress>(guid, {})
.pipe(startWith(progress)),
),
map(formatProgress),
tap<{ total: number; message: string }>(({ total }) => {
if (total === 1) {
this.state.syncState()
}
}),
catchError(e => {
console.error(e)
return EMPTY
}),
),
{ initialValue: { total: 0, message: '' } },
)
}

View File

@@ -1,24 +0,0 @@
import { Component, inject } from '@angular/core'
import { Router } from '@angular/router'
import {
InitializingComponent,
provideSetupLogsService,
provideSetupService,
} from '@start9labs/shared'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@Component({
standalone: true,
template: `
<app-initializing (finished)="router.navigate(['login'])" />
`,
providers: [
provideSetupService(ApiService),
provideSetupLogsService(ApiService),
],
styles: ':host { padding: 1rem; }',
imports: [InitializingComponent],
})
export default class LoadingPage {
readonly router = inject(Router)
}

View File

@@ -1,4 +1,3 @@
import { TuiConfirmService } from '@taiga-ui/kit'
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
@@ -15,7 +14,8 @@ import {
tuiMarkControlAsTouchedAndValidate,
TuiValueChanges,
} from '@taiga-ui/cdk'
import { TuiDialogContext, TuiButton } from '@taiga-ui/core'
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
import { TuiConfirmService } from '@taiga-ui/kit'
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
import { compare, Operation } from 'fast-json-patch'
import { FormModule } from 'src/app/routes/portal/components/form/form.module'
@@ -100,7 +100,7 @@ export interface FormContext<T> {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormComponent<T extends Record<string, any>> implements OnInit {
private readonly dialogFormService = inject(TuiConfirmService)
private readonly confirmService = inject(TuiConfirmService)
private readonly formService = inject(FormService)
private readonly invalidService = inject(InvalidService)
private readonly context = inject<TuiDialogContext<void, FormContext<T>>>(
@@ -116,7 +116,7 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
form = new FormGroup({})
ngOnInit() {
this.dialogFormService.markAsPristine()
this.confirmService.markAsPristine()
this.form = this.formService.createForm(this.spec, this.value)
this.process(this.patch)
}
@@ -140,7 +140,7 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
}
markAsDirty() {
this.dialogFormService.markAsDirty()
this.confirmService.markAsDirty()
}
close() {

View File

@@ -26,7 +26,6 @@ import { ERRORS } from '../form-group/form-group.component'
templateUrl: './form-array.component.html',
styleUrls: ['./form-array.component.scss'],
animations: [tuiFadeIn, tuiHeightCollapse, tuiParentStop],
providers: [],
})
export class FormArrayComponent {
@Input({ required: true })
@@ -41,6 +40,7 @@ export class FormArrayComponent {
private warned = false
private readonly formService = inject(FormService)
private readonly dialogs = inject(TuiDialogService)
private readonly destroyRef = inject(DestroyRef)
get canAdd(): boolean {
return (
@@ -95,6 +95,4 @@ export class FormArrayComponent {
this.array.control.insert(0, this.formService.getListItem(this.spec))
this.open.set(this.array.control.at(0), true)
}
readonly destroyRef = inject(DestroyRef)
}

View File

@@ -1,7 +1,6 @@
import { Component } from '@angular/core'
import { CT } from '@start9labs/start-sdk'
import { CT, utils } from '@start9labs/start-sdk'
import { Control } from '../control'
import { getDefaultString } from 'src/app/utils/config-utilities'
@Component({
selector: 'form-text',
@@ -12,6 +11,6 @@ export class FormTextComponent extends Control<CT.ValueSpecText, string> {
masked = true
generate() {
this.value = getDefaultString(this.spec.generate || '')
this.value = utils.getDefaultString(this.spec.generate || '')
}
}

View File

@@ -1,9 +1,9 @@
<form-control
[spec]="selectSpec"
[formControlName]="select"
formControlName="selection"
(tuiValueChanges)="onUnion($event)"
></form-control>
<tui-elastic-container class="g-form-group" [formGroupName]="value">
<tui-elastic-container class="g-form-group" formGroupName="value">
<form-group
class="group"
[spec]="(union && spec.variants[union].spec) || {}"

View File

@@ -23,25 +23,22 @@ import { tuiPure } from '@taiga-ui/cdk'
],
})
export class FormUnionComponent implements OnChanges {
@Input({ required: true })
@Input()
spec!: CT.ValueSpecUnion
selectSpec!: CT.ValueSpecSelect
readonly select = CT.unionSelectKey
readonly value = CT.unionValueKey
private readonly form = inject(FormGroupName)
private readonly formService = inject(FormService)
get union(): string {
return this.form.value[CT.unionSelectKey]
return this.form.value.selection
}
@tuiPure
onUnion(union: string) {
this.form.control.setControl(
CT.unionValueKey,
'value',
this.formService.getFormGroup(
union ? this.spec.variants[union].spec : {},
),

View File

@@ -38,7 +38,6 @@ import { FormColorComponent } from './form-color/form-color.component'
import { FormControlComponent } from './form-control/form-control.component'
import { FormDatetimeComponent } from './form-datetime/form-datetime.component'
import { FormFileComponent } from './form-file/form-file.component'
import { FormGroupComponent } from './form-group/form-group.component'
import { FormMultiselectComponent } from './form-multiselect/form-multiselect.component'
import { FormNumberComponent } from './form-number/form-number.component'

View File

@@ -2,7 +2,7 @@ import { TuiCell } from '@taiga-ui/layout'
import { TuiTitle, TuiButton } from '@taiga-ui/core'
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { CopyService, EmverPipesModule } from '@start9labs/shared'
import { CopyService, ExverPipesModule } from '@start9labs/shared'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -14,7 +14,7 @@ import { ConfigService } from 'src/app/services/config.service'
<div tuiCell>
<div tuiTitle>
<strong>Version</strong>
<div tuiSubtitle>{{ server.version | displayEmver }}</div>
<div tuiSubtitle>{{ server.version }}</div>
</div>
</div>
<div tuiCell>
@@ -50,10 +50,10 @@ import { ConfigService } from 'src/app/services/config.service'
styles: ['[tuiCell] { padding-inline: 0 }'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, EmverPipesModule, TuiTitle, TuiButton, TuiCell],
imports: [CommonModule, ExverPipesModule, TuiTitle, TuiButton, TuiCell],
})
export class AboutComponent {
readonly server$ = inject(PatchDB<DataModel>).watch$('serverInfo')
readonly server$ = inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo')
readonly copyService = inject(CopyService)
readonly gitHash = inject(ConfigService).gitHash
}

View File

@@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map, Observable, startWith } from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service'
import { NetworkService } from 'src/app/services/network.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
@@ -47,9 +48,9 @@ export class HeaderConnectionComponent {
icon: string
status: string
}> = combineLatest([
inject(ConnectionService).networkConnected$,
inject(ConnectionService).websocketConnected$.pipe(startWith(false)),
inject(PatchDB<DataModel>)
inject(NetworkService),
inject(ConnectionService),
inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'statusInfo')
.pipe(startWith({ restarting: false, shuttingDown: false })),
]).pipe(

View File

@@ -166,7 +166,7 @@ import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
export class HeaderComponent {
readonly options = OPTIONS
readonly breadcrumbs$ = inject(BreadcrumbsService)
readonly snekScore$ = inject(PatchDB<DataModel>).watch$(
readonly snekScore$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui',
'gaming',
'snake',

View File

@@ -6,7 +6,7 @@ import {
Input,
} from '@angular/core'
import { RouterLink } from '@angular/router'
import { WINDOW } from '@ng-web-apis/common'
import { WA_WINDOW } from '@ng-web-apis/common'
import { Breadcrumb } from 'src/app/services/breadcrumbs.service'
@Component({
@@ -68,7 +68,7 @@ import { Breadcrumb } from 'src/app/services/breadcrumbs.service'
imports: [TuiIcon, RouterLink],
})
export class HeaderMobileComponent {
private readonly win = inject(WINDOW)
private readonly win = inject(WA_WINDOW)
@Input() headerMobile: readonly Breadcrumb[] | null = []

View File

@@ -7,7 +7,7 @@ import {
inject,
Input,
} from '@angular/core'
import { WINDOW } from '@ng-web-apis/common'
import { WA_WINDOW } from '@ng-web-apis/common'
import { CopyService } from '@start9labs/shared'
import { TuiDialogService, TuiTitle, TuiButton } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@@ -68,7 +68,7 @@ import { AddressesService } from './interface.utils'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressItemComponent {
private readonly window = inject(WINDOW)
private readonly window = inject(WA_WINDOW)
private readonly dialogs = inject(TuiDialogService)
readonly service = inject(AddressesService)

View File

@@ -95,7 +95,10 @@ import { AddressDetails } from './interface.utils'
],
})
export class InterfaceComponent {
readonly network$ = inject(PatchDB<DataModel>).watch$('serverInfo', 'network')
readonly network$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'serverInfo',
'network',
)
@Input() packageContext?: {
packageId: string

View File

@@ -53,9 +53,9 @@ export type AddressDetails = {
url: string
}
export function getAddresses(
serviceInterface: T.ServiceInterfaceWithHostInfo,
): {
// @TODO Matt these types have change significantly
export function getAddresses(serviceInterface: any): {
// T.ServiceInterface): {
clearnet: AddressDetails[]
local: AddressDetails[]
tor: AddressDetails[]
@@ -76,7 +76,7 @@ export function getAddresses(
const local: AddressDetails[] = []
const tor: AddressDetails[] = []
hostnames.forEach(h => {
hostnames.forEach((h: any) => {
let scheme = ''
let port = ''

View File

@@ -51,7 +51,7 @@ export class LogsPipe implements PipeTransform {
),
).pipe(
catchError(() =>
this.connection.connected$.pipe(
this.connection.pipe(
tap(v => this.logs.status$.next(v ? 'reconnecting' : 'disconnected')),
filter(Boolean),
take(1),

View File

@@ -6,7 +6,7 @@ import {
isEmptyObject,
LoadingService,
} from '@start9labs/shared'
import { CT } from '@start9labs/start-sdk'
import { CT, T } from '@start9labs/start-sdk'
import {
TuiDialogContext,
TuiDialogService,
@@ -199,8 +199,6 @@ export class ConfigModal {
const loader = new Subscription()
try {
await this.uploadFiles(config, loader)
if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patchDb))) {
await this.configureDeps(config, loader)
} else {
@@ -213,24 +211,6 @@ export class ConfigModal {
}
}
private async uploadFiles(config: Record<string, any>, loader: Subscription) {
loader.unsubscribe()
loader.closed = false
// TODO: Could be nested files
const keys = Object.keys(config).filter(key => config[key] instanceof File)
const message = `Uploading File${keys.length > 1 ? 's' : ''}...`
if (!keys.length) return
loader.add(this.loader.open(message).subscribe())
const hashes = await Promise.all(
keys.map(key => this.embassyApi.uploadFile(config[key])),
)
keys.forEach((key, i) => (config[key] = hashes[i]))
}
private async configureDeps(
config: Record<string, any>,
loader: Subscription,
@@ -261,11 +241,11 @@ export class ConfigModal {
this.context.$implicit.complete()
}
private async approveBreakages(breakages: Breakages): Promise<boolean> {
private async approveBreakages(breakages: T.PackageId[]): Promise<boolean> {
const packages = await getAllPackages(this.patchDb)
const message =
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
const content = `${message}${Object.keys(breakages).map(
const content = `${message}${breakages.map(
id => `<li><b>${getManifest(packages[id]).title}</b></li>`,
)}</ul>`
const data: TuiConfirmData = { content, yes: 'Continue', no: 'Cancel' }

View File

@@ -58,5 +58,5 @@ export class PortalComponent {
this.breadcrumbs.update(e.url.replace('/portal/service/', ''))
})
readonly name$ = inject(PatchDB<DataModel>).watch$('ui', 'name')
readonly name$ = inject<PatchDB<DataModel>>(PatchDB).watch$('ui', 'name')
}

View File

@@ -108,7 +108,7 @@ export class ServiceComponent implements OnChanges {
@Input()
depErrors?: PkgDependencyErrors
readonly connected$ = inject(ConnectionService).connected$
readonly connected$ = inject(ConnectionService)
get installed(): boolean {
return this.pkg.stateInfo.state !== 'installed'

View File

@@ -11,7 +11,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
providedIn: 'root',
})
export class ServicesService extends Observable<readonly PackageDataEntry[]> {
private readonly services$ = inject(PatchDB<DataModel>)
private readonly services$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData')
.pipe(
map(pkgs =>

View File

@@ -64,7 +64,7 @@ export class UILaunchComponent {
@Input()
pkg!: PackageDataEntry
get interfaces(): readonly T.ServiceInterfaceWithHostInfo[] {
get interfaces(): readonly T.ServiceInterface[] {
return this.getInterfaces(this.pkg)
}
@@ -72,18 +72,20 @@ export class UILaunchComponent {
return this.pkg.status.main.status === 'running'
}
get first(): T.ServiceInterfaceWithHostInfo | undefined {
get first(): T.ServiceInterface | undefined {
return this.interfaces[0]
}
@tuiPure
getInterfaces(pkg?: PackageDataEntry): T.ServiceInterfaceWithHostInfo[] {
getInterfaces(pkg?: PackageDataEntry): T.ServiceInterface[] {
return pkg
? Object.values(pkg.serviceInterfaces).filter(({ type }) => type === 'ui')
: []
}
getHref(info?: T.ServiceInterfaceWithHostInfo): string | null {
return info && this.isRunning ? this.config.launchableAddress(info) : null
getHref(info?: T.ServiceInterface): string | null {
return info && this.isRunning
? this.config.launchableAddress(info, this.pkg.hosts)
: null
}
}

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { EmverPipesModule } from '@start9labs/shared'
import { ExverPipesModule } from '@start9labs/shared'
import { TuiIcon } from '@taiga-ui/core'
import { DependencyInfo } from '../types/dependency-info'
@@ -14,10 +14,8 @@ import { DependencyInfo } from '../types/dependency-info'
}
{{ dep.title }}
</strong>
<div>{{ dep.version | displayEmver }}</div>
<div [style.color]="color">
{{ dep.errorText || 'Satisfied' }}
</div>
<div>{{ dep.version }}</div>
<div [style.color]="color">{{ dep.errorText || 'Satisfied' }}</div>
</span>
@if (dep.actionText) {
<div>
@@ -41,7 +39,7 @@ import { DependencyInfo } from '../types/dependency-info'
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [EmverPipesModule, TuiIcon],
imports: [ExverPipesModule, TuiIcon],
})
export class ServiceDependencyComponent {
@Input({ required: true, alias: 'serviceDependency' })

View File

@@ -33,5 +33,5 @@ export class ServiceHealthChecksComponent {
@Input({ required: true })
checks: readonly T.HealthCheckResult[] = []
readonly connected$ = inject(ConnectionService).connected$
readonly connected$ = inject(ConnectionService)
}

View File

@@ -9,6 +9,7 @@ import {
} from '@angular/core'
import { map, timer } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
@Component({
@@ -56,9 +57,12 @@ import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
export class ServiceInterfaceListItemComponent {
private readonly config = inject(ConfigService)
@Input({ required: true, alias: 'serviceInterfaceListItem' })
@Input({ required: true })
info!: ExtendedInterfaceInfo
@Input({ required: true })
pkg!: PackageDataEntry
@Input()
disabled = false
@@ -68,6 +72,8 @@ export class ServiceInterfaceListItemComponent {
)
get href(): string | null {
return this.disabled ? null : this.config.launchableAddress(this.info)
return this.disabled
? null
: this.config.launchableAddress(this.info, this.pkg.hosts)
}
}

View File

@@ -11,7 +11,9 @@ import { ServiceInterfaceListItemComponent } from './interface-list-item.compone
@for (info of pkg | interfaceInfo; track $index) {
<a
class="g-action"
[serviceInterfaceListItem]="info"
serviceInterfaceListItem
[info]="info"
[pkg]="pkg"
[disabled]="status.primary !== 'running'"
[routerLink]="info.routerLink"
></a>

View File

@@ -1,4 +1,3 @@
import { TuiLoader, TuiIcon } from '@taiga-ui/core'
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
@@ -6,10 +5,10 @@ import {
HostBinding,
Input,
} from '@angular/core'
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
import { StatusRendering } from 'src/app/services/pkg-status-rendering.service'
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
import { UnitConversionPipesModule } from '@start9labs/shared'
@Component({
selector: 'service-status',
@@ -27,9 +26,6 @@ import { UnitConversionPipesModule } from '@start9labs/shared'
@if (rendering.showDots) {
<span class="loading-dots"></span>
}
@if (sigtermTimeout && (sigtermTimeout | durationToSeconds) > 30) {
<div>This may take a while</div>
}
}
`,
styles: [
@@ -58,13 +54,7 @@ import { UnitConversionPipesModule } from '@start9labs/shared'
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
InstallingProgressDisplayPipe,
UnitConversionPipesModule,
TuiIcon,
TuiLoader,
],
imports: [CommonModule, InstallingProgressDisplayPipe, TuiIcon, TuiLoader],
})
export class ServiceStatusComponent {
@Input({ required: true })
@@ -76,8 +66,6 @@ export class ServiceStatusComponent {
@Input()
connected = false
@Input() sigtermTimeout?: string | null = null
@HostBinding('class')
get class(): string | null {
if (!this.connected) return null

View File

@@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
export interface ExtendedInterfaceInfo extends T.ServiceInterfaceWithHostInfo {
export interface ExtendedInterfaceInfo extends T.ServiceInterface {
id: string
icon: string
color: string

View File

@@ -74,11 +74,7 @@ export class ToAdditionalPipe implements PipeTransform {
label: 'License',
size: 'l',
data: {
content: from(
this.api.getStatic(
`/public/package-data/${id}/${version}/LICENSE.md`,
),
),
content: from(this.api.getStaticInstalled(id, 'LICENSE.md')),
},
})
.subscribe()

View File

@@ -98,13 +98,13 @@ export class ToMenuPipe implements PipeTransform {
})
.subscribe(),
},
pkg.marketplaceUrl
pkg.registry
? {
icon: '@tui.shopping-bag',
name: 'Marketplace Listing',
description: `View ${manifest.title} on the Marketplace`,
routerLink: `/portal/system/marketplace`,
params: { url: pkg.marketplaceUrl, id: manifest.id },
params: { url: pkg.registry, id: manifest.id },
}
: {
icon: '@tui.shopping-bag',
@@ -114,7 +114,7 @@ export class ToMenuPipe implements PipeTransform {
]
}
private showInstructions({ title, id, version }: T.Manifest) {
private showInstructions({ title, id }: T.Manifest) {
this.api
.setDbValue<boolean>(['ack-instructions', id], true)
.catch(e => console.error('Failed to mark instructions as seen', e))
@@ -124,11 +124,7 @@ export class ToMenuPipe implements PipeTransform {
label: `${title} instructions`,
size: 'l',
data: {
content: from(
this.api.getStatic(
`/public/package-data/${id}/${version}/INSTRUCTIONS.md`,
),
),
content: from(this.api.getStaticInstalled(id, 'instructions.md')),
},
})
.subscribe()

View File

@@ -28,7 +28,7 @@ export class ServiceInterfaceRoute {
interfaceId: this.route.snapshot.paramMap.get('interfaceId') || '',
}
readonly interfaceInfo$ = inject(PatchDB<DataModel>)
readonly interfaceInfo$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$(
'packageData',
this.context.packageId,

View File

@@ -16,7 +16,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
imports: [CommonModule, RouterOutlet],
})
export class ServiceOutletComponent {
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly route = inject(ActivatedRoute)
private readonly router = inject(Router)

View File

@@ -47,11 +47,6 @@ import { DependencyInfo } from '../types/dependency-info'
[connected]="!!(connected$ | async)"
[installingInfo]="service.pkg.stateInfo.installingInfo"
[rendering]="getRendering(service.status)"
[sigtermTimeout]="
service.pkg.status.main.status === 'stopping'
? service.pkg.status.main.timeout
: null
"
/>
@if (isInstalled(service) && (connected$ | async)) {
@@ -164,14 +159,14 @@ import { DependencyInfo } from '../types/dependency-info'
],
})
export class ServiceRoute {
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly pkgId$ = inject(ActivatedRoute).paramMap.pipe(
map(params => params.get('pkgId')!),
)
private readonly depErrorService = inject(DepErrorService)
private readonly router = inject(Router)
private readonly formDialog = inject(FormDialogService)
readonly connected$ = inject(ConnectionService).connected$
readonly connected$ = inject(ConnectionService)
readonly service$ = this.pkgId$.pipe(
switchMap(pkgId =>
@@ -232,11 +227,11 @@ export class ServiceRoute {
depErrors,
)
const { title, icon, versionSpec } = pkg.currentDependencies[depId]
const { title, icon, versionRange } = pkg.currentDependencies[depId]
return {
id: depId,
version: versionSpec,
version: versionRange,
title,
icon,
errorText: errorText
@@ -322,7 +317,7 @@ export class ServiceRoute {
const dependentInfo: DependentInfo = {
id: manifest.id,
title: manifest.title,
version: pkg.currentDependencies[depId].versionSpec,
version: pkg.currentDependencies[depId].versionRange,
}
const navigationExtras: NavigationExtras = {
// @TODO state not being used by marketplace component. Maybe it is not important to use.

View File

@@ -1,7 +1,7 @@
export interface DependencyInfo {
id: string
title: string
icon: string
title: string | null
icon: string | null
version: string
errorText: string
actionText: string

View File

@@ -4,7 +4,7 @@ import {
inject,
Input,
} from '@angular/core'
import { Emver } from '@start9labs/shared'
import { Exver } from '@start9labs/shared'
import { TuiIcon } from '@taiga-ui/core'
import { BackupTarget } from 'src/app/services/api/api.types'
import { BackupType } from '../types/backup-type'
@@ -21,7 +21,7 @@ import { BackupType } from '../types/backup-type'
imports: [TuiIcon],
})
export class BackupsStatusComponent {
private readonly emver = inject(Emver)
private readonly exver = inject(Exver)
@Input({ required: true }) type!: BackupType
@Input({ required: true }) target!: BackupTarget
@@ -61,9 +61,8 @@ export class BackupsStatusComponent {
}
private get hasBackup(): boolean {
return (
!!this.target.startOs &&
this.emver.compare(this.target.startOs.version, '0.3.0') !== -1
)
return !!this.target.startOs
// @TODO Matt types changed
// && this.exver.compareExver(this.target.startOs.version, '0.3.0') !== -1
}
}

View File

@@ -95,7 +95,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
})
export class BackupsUpcomingComponent {
readonly current = toSignal(
inject(PatchDB<DataModel>)
inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'statusInfo', 'currentBackup', 'job')
.pipe(map(job => job || {})),
)

View File

@@ -77,7 +77,7 @@ interface Package {
imports: [FormsModule, TuiButton, TuiGroup, TuiLoader, TuiBlock, TuiCheckbox],
})
export class BackupsBackupModal {
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly context =
inject<TuiDialogContext<string[], { btnText: string } | undefined>>(
POLYMORPHEUS_CONTEXT,

View File

@@ -81,7 +81,7 @@ export class BackupsRecoverModal {
private readonly context =
inject<TuiDialogContext<void, RecoverData>>(POLYMORPHEUS_CONTEXT)
readonly packageData$ = inject(PatchDB<DataModel>)
readonly packageData$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData')
.pipe(take(1))
@@ -118,12 +118,13 @@ export class BackupsRecoverModal {
const ids = options.filter(({ checked }) => !!checked).map(({ id }) => id)
const loader = this.loader.open('Initializing...').subscribe()
const { targetId, password } = this.context.data
const { targetId, serverId, password } = this.context.data
try {
await this.api.restorePackages({
ids,
targetId,
serverId,
password,
})

View File

@@ -0,0 +1,30 @@
import { Component, inject } from '@angular/core'
import { ServerComponent, StartOSDiskInfo } from '@start9labs/shared'
import { TuiDialogContext } from '@taiga-ui/core'
import {
POLYMORPHEUS_CONTEXT,
PolymorpheusComponent,
} from '@taiga-ui/polymorpheus'
interface Data {
servers: StartOSDiskInfo[]
}
@Component({
standalone: true,
template: `
@for (server of context.data.servers; track $index) {
<button
[server]="server"
(click)="this.context.completeWith(server)"
></button>
}
`,
imports: [ServerComponent],
})
export class ServersComponent {
readonly context =
inject<TuiDialogContext<StartOSDiskInfo, Data>>(POLYMORPHEUS_CONTEXT)
}
export const SERVERS = new PolymorpheusComponent(ServersComponent)

View File

@@ -171,8 +171,8 @@ export class BackupsTargetsModal implements OnInit {
text: 'Save',
handler: ({ type }: BackupConfig) =>
this.add(
type[CT.unionSelectKey] === 'cifs' ? 'cifs' : 'cloud',
type[CT.unionValueKey],
type.selection === 'cifs' ? 'cifs' : 'cloud',
type.value,
),
},
],

View File

@@ -26,7 +26,7 @@ export class ToOptionsPipe implements PipeTransform {
id,
installed: !!packageData[id],
checked: false,
newerOS: this.compare(packageBackups[id].osVersion),
newerStartOs: this.compare(packageBackups[id].osVersion),
}))
.sort((a, b) =>
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,

View File

@@ -1,7 +1,11 @@
import { inject, Injectable } from '@angular/core'
import { Router } from '@angular/router'
import * as argon2 from '@start9labs/argon2'
import { ErrorService, LoadingService } from '@start9labs/shared'
import {
ErrorService,
LoadingService,
StartOSDiskInfo,
} from '@start9labs/shared'
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
import {
catchError,
@@ -18,10 +22,11 @@ import {
PROMPT,
PromptOptions,
} from 'src/app/routes/portal/modals/prompt.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { BackupTarget } from 'src/app/services/api/api.types'
import { TARGET, TARGET_RESTORE } from '../modals/target.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { RECOVER } from '../modals/recover.component'
import { SERVERS } from '../modals/servers.component'
import { TARGET, TARGET_RESTORE } from '../modals/target.component'
import { RecoverData } from '../types/recover-data'
@Injectable({
@@ -38,23 +43,33 @@ export class BackupsRestoreService {
this.dialogs
.open<BackupTarget>(TARGET, TARGET_RESTORE)
.pipe(
// @TODO Alex implement servers
switchMap(target =>
this.dialogs.open<string>(PROMPT, PROMPT_OPTIONS).pipe(
exhaustMap(password =>
this.getRecoverData(
target.id,
password,
target.startOs?.passwordHash || '',
this.dialogs
.open<StartOSDiskInfo & { id: string }>(SERVERS, {
data: { servers: [] },
})
.pipe(
switchMap(({ id, passwordHash }) =>
this.dialogs.open<string>(PROMPT, PROMPT_OPTIONS).pipe(
exhaustMap(password =>
this.getRecoverData(
target.id,
id,
password,
passwordHash || '',
),
),
take(1),
switchMap(data =>
this.dialogs.open(RECOVER, {
label: 'Select Services to Restore',
data,
}),
),
),
),
),
take(1),
switchMap(data =>
this.dialogs.open(RECOVER, {
label: 'Select Services to Restore',
data,
}),
),
),
),
)
.subscribe(() => {
@@ -64,6 +79,7 @@ export class BackupsRestoreService {
private getRecoverData(
targetId: string,
serverId: string,
password: string,
hash: string,
): Observable<RecoverData> {
@@ -81,7 +97,7 @@ export class BackupsRestoreService {
return EMPTY
}),
map(backupInfo => ({ targetId, password, backupInfo })),
map(backupInfo => ({ targetId, password, backupInfo, serverId })),
)
}
}

View File

@@ -1,16 +1,15 @@
import { CT } from '@start9labs/start-sdk'
import { RR } from 'src/app/services/api/api.types'
export type BackupConfig =
| {
type: {
[CT.unionSelectKey]: 'dropbox' | 'google-drive'
[CT.unionValueKey]: RR.AddCloudBackupTargetReq
selection: 'dropbox' | 'google-drive'
value: RR.AddCloudBackupTargetReq
}
}
| {
type: {
[CT.unionSelectKey]: 'cifs'
[CT.unionValueKey]: RR.AddCifsBackupTargetReq
selection: 'cifs'
value: RR.AddCifsBackupTargetReq
}
}

View File

@@ -2,6 +2,7 @@ import { BackupInfo } from 'src/app/services/api/api.types'
export interface RecoverData {
targetId: string
serverId: string
backupInfo: BackupInfo
password: string
}

View File

@@ -12,12 +12,12 @@ import {
MarketplacePkg,
} from '@start9labs/marketplace'
import {
Emver,
Exver,
ErrorService,
isEmptyObject,
LoadingService,
sameUrl,
EmverPipesModule,
ExverPipesModule,
} from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { firstValueFrom } from 'rxjs'
@@ -41,7 +41,7 @@ import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
localPkg.stateInfo.state === 'installed' && (localPkg | toManifest);
as localManifest
) {
@switch (localManifest.version | compareEmver: pkg.manifest.version) {
@switch (localManifest.version | compareExver: pkg.version) {
@case (1) {
<button
tuiButton
@@ -97,14 +97,14 @@ import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, EmverPipesModule, TuiButton, ToManifestPipe],
imports: [CommonModule, ExverPipesModule, TuiButton, ToManifestPipe],
})
export class MarketplaceControlsComponent {
private readonly alerts = inject(MarketplaceAlertsService)
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService)
private readonly emver = inject(Emver)
private readonly exver = inject(Exver)
private readonly router = inject(Router)
private readonly marketplace = inject(
AbstractMarketplaceService,
@@ -124,7 +124,7 @@ export class MarketplaceControlsComponent {
async tryInstall() {
const current = await firstValueFrom(this.marketplace.getSelectedHost$())
const url = this.url || current.url
const originalUrl = this.localPkg?.marketplaceUrl || ''
const originalUrl = this.localPkg?.registry || ''
if (!this.localPkg) {
if (await this.alerts.alertInstall(this.pkg)) this.install(url)
@@ -143,7 +143,7 @@ export class MarketplaceControlsComponent {
if (
hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) &&
this.emver.compare(localManifest.version, this.pkg.manifest.version) !== 0
this.exver.compareExver(localManifest.version, this.pkg.version) !== 0
) {
this.dryInstall(url)
} else {
@@ -152,14 +152,14 @@ export class MarketplaceControlsComponent {
}
async showService() {
this.router.navigate(['/portal/service', this.pkg.manifest.id])
this.router.navigate(['/portal/service', this.pkg.id])
}
private async dryInstall(url: string) {
const breakages = dryUpdate(
this.pkg.manifest,
this.pkg,
await getAllPackages(this.patch),
this.emver,
this.exver,
)
if (
@@ -172,7 +172,7 @@ export class MarketplaceControlsComponent {
private async install(url: string) {
const loader = this.loader.open('Beginning Install...').subscribe()
const { id, version } = this.pkg.manifest
const { id, version } = this.pkg
try {
await this.marketplace.installPackage(id, version, url)

View File

@@ -19,13 +19,13 @@ import { MARKETPLACE_REGISTRY } from '../modals/registry.component'
tuiIconButton
type="button"
appearance="icon"
icon="@tui.repeat"
iconStart="@tui.repeat"
(click)="changeRegistry()"
>
Change Registry
</button>
<button slot="mobile" class="mobile-button" (click)="changeRegistry()">
<tui-icon tuiAppearance="icon" icon="@tui.repeat"></tui-icon>
<tui-icon tuiAppearance="icon" icon="@tui.repeat" />
Change Registry
</button>
</menu>

View File

@@ -22,11 +22,11 @@ import { MarketplaceControlsComponent } from './controls.component'
<marketplace-item [pkg]="pkg" (click)="toggle(true)">
<marketplace-preview
*tuiSidebar="
(id$ | async) === pkg.manifest.id;
(id$ | async) === pkg.id;
direction: 'right';
autoWidth: true
"
[pkgId]="pkg.manifest.id"
[pkgId]="pkg.id"
class="preview-wrapper"
(tuiClickOutside)="toggle(false)"
>
@@ -45,7 +45,7 @@ import { MarketplaceControlsComponent } from './controls.component'
slot="controls"
class="controls-wrapper"
[pkg]="pkg"
[localPkg]="pkg.manifest.id | toLocal | async"
[localPkg]="pkg.id | toLocal | async"
/>
</marketplace-preview>
</marketplace-item>
@@ -125,7 +125,7 @@ export class MarketplaceTileComponent {
toggle(open: boolean) {
this.router.navigate([], {
queryParams: { id: open ? this.pkg.manifest.id : null },
queryParams: { id: open ? this.pkg.id : null },
})
}
}

View File

@@ -6,6 +6,8 @@ import {
Input,
TemplateRef,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { Router } from '@angular/router'
import {
AboutModule,
AbstractMarketplaceService,
@@ -14,21 +16,17 @@ import {
MarketplaceDependenciesComponent,
MarketplacePackageHeroComponent,
MarketplacePkg,
ReleaseNotesModule,
StoreIdentity,
} from '@start9labs/marketplace'
import { displayEmver, Emver, SharedPipesModule } from '@start9labs/shared'
import { BehaviorSubject, filter, switchMap, tap } from 'rxjs'
import { Exver, SharedPipesModule } from '@start9labs/shared'
import {
TuiButton,
TuiDialogContext,
TuiDialogService,
TuiLoader,
TuiIcon,
TuiButton,
TuiLoader,
} from '@taiga-ui/core'
import { TuiRadioList, TuiStringifyContentPipe } from '@taiga-ui/kit'
import { FormsModule } from '@angular/forms'
import { Router } from '@angular/router'
import { BehaviorSubject, filter, switchMap, tap } from 'rxjs'
@Component({
selector: 'marketplace-preview',
@@ -44,13 +42,12 @@ import { Router } from '@angular/router'
</marketplace-package-hero>
<div class="inner-container">
<marketplace-about [pkg]="pkg" />
@if (!(pkg.manifest.dependencies | empty)) {
@if (!(pkg.dependencyMetadata | empty)) {
<marketplace-dependencies
[pkg]="pkg"
(open)="open($event)"
></marketplace-dependencies>
}
<release-notes [pkg]="pkg" />
<marketplace-additional [pkg]="pkg">
<marketplace-additional-item
(click)="presentAlertVersions(pkg, version)"
@@ -64,11 +61,7 @@ import { Router } from '@angular/router'
let-data="data"
let-completeWith="completeWith"
>
<tui-radio-list
[items]="data.items"
[itemContent]="displayEmver | tuiStringifyContent"
[(ngModel)]="data.value"
/>
<tui-radio-list [items]="data.items" [(ngModel)]="data.value" />
<footer class="buttons">
<button
tuiButton
@@ -160,15 +153,14 @@ import { Router } from '@angular/router'
imports: [
CommonModule,
MarketplacePackageHeroComponent,
TuiButton,
MarketplaceDependenciesComponent,
ReleaseNotesModule,
MarketplaceAdditionalItemComponent,
TuiButton,
AdditionalModule,
AboutModule,
SharedPipesModule,
FormsModule,
TuiStringifyContentPipe,
MarketplaceAdditionalItemComponent,
TuiRadioList,
TuiLoader,
TuiIcon,
@@ -180,11 +172,9 @@ export class MarketplacePreviewComponent {
readonly loading$ = new BehaviorSubject(true)
readonly displayEmver = displayEmver
private readonly router = inject(Router)
private readonly marketplaceService = inject(AbstractMarketplaceService)
readonly url =
this.router.routerState.snapshot.root.queryParamMap.get('url') || undefined
readonly url = this.router.routerState.snapshot.root.queryParamMap.get('url')
readonly loadVersion$ = new BehaviorSubject<string>('*')
readonly pkg$ = this.loadVersion$.pipe(
@@ -199,7 +189,7 @@ export class MarketplacePreviewComponent {
constructor(
private readonly dialogs: TuiDialogService,
private readonly emver: Emver,
private readonly exver: Exver,
) {}
open(id: string) {
@@ -215,9 +205,9 @@ export class MarketplacePreviewComponent {
label: 'Versions',
size: 's',
data: {
value: pkg.manifest.version,
items: [...new Set(pkg.versions)].sort(
(a, b) => -1 * (this.emver.compare(a, b) || 0),
value: pkg.version,
items: [...new Set(Object.keys(pkg.otherVersions))].sort(
(a, b) => -1 * (this.exver.compareExver(a, b) || 0),
),
},
})

View File

@@ -90,7 +90,7 @@ export class MarketplaceRegistryModal {
private readonly marketplace = inject(
AbstractMarketplaceService,
) as MarketplaceService
private readonly hosts$ = inject(PatchDB<DataModel>).watch$(
private readonly hosts$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui',
'marketplace',
'knownHosts',

View File

@@ -9,7 +9,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
standalone: true,
})
export class ToLocalPipe implements PipeTransform {
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
transform(id: string): Observable<PackageDataEntry> {
return this.patch.watch$('packageData', id).pipe(filter(Boolean))

View File

@@ -11,7 +11,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
})
export class MarketplaceAlertsService {
private readonly dialogs = inject(TuiDialogService)
private readonly marketplace$ = inject(PatchDB<DataModel>).watch$(
private readonly marketplace$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui',
'marketplace',
)
@@ -60,8 +60,8 @@ export class MarketplaceAlertsService {
})
}
async alertInstall({ manifest }: MarketplacePkg): Promise<boolean> {
const content = manifest.alerts.install
async alertInstall({ alerts }: MarketplacePkg): Promise<boolean> {
const content = alerts.install
return (
!!content &&

View File

@@ -112,7 +112,7 @@ import { toRouterLink } from 'src/app/utils/to-router-link'
imports: [CommonModule, RouterLink, TuiLineClamp, TuiLink, TuiIcon],
})
export class NotificationItemComponent {
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly service = inject(NotificationService)
@Input({ required: true }) notificationItem!: ServerNotification<number>

View File

@@ -68,7 +68,7 @@ export class SettingsMenuComponent {
private readonly clientStorageService = inject(ClientStorageService)
private readonly alerts = inject(TuiAlertService)
readonly server$ = inject(PatchDB<DataModel>).watch$('serverInfo')
readonly server$ = inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo')
readonly service = inject(SettingsService)
manageClicks = 0

View File

@@ -47,7 +47,7 @@ import { EOSService } from 'src/app/services/eos.service'
],
})
export class SettingsUpdateModal {
readonly versions = Object.entries(this.eosService.eos?.releaseNotes!)
readonly versions = Object.entries(this.eosService.osUpdate?.releaseNotes!)
.sort(([a], [b]) => a.localeCompare(b))
.reverse()
.map(([version, notes]) => ({

View File

@@ -64,7 +64,7 @@ export class SettingsDomainsComponent {
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly formDialog = inject(FormDialogService)
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly api = inject(ApiService)
private readonly dialogs = inject(TuiDialogService)
@@ -151,9 +151,9 @@ export class SettingsDomainsComponent {
}
// @TODO figure out how to get types here
private getNetworkStrategy(strategy: any) {
return strategy.unionSelectKey === 'local'
? { ipStrategy: strategy.unionValueKey.ipStrategy }
: { proxy: strategy.unionValueKey.proxyId }
return strategy.selection === 'local'
? { ipStrategy: strategy.value.ipStrategy }
: { proxy: strategy.value.proxyId }
}
private async deleteDomain(hostname?: string) {
@@ -189,7 +189,7 @@ export class SettingsDomainsComponent {
// @TODO figure out how to get types here
private async save({ provider, strategy, hostname }: any): Promise<boolean> {
const loader = this.loader.open('Saving...').subscribe()
const name = provider.unionSelectKey
const name = provider.selection
try {
await this.api.addDomain({
@@ -197,8 +197,8 @@ export class SettingsDomainsComponent {
networkStrategy: this.getNetworkStrategy(strategy),
provider: {
name,
username: name === 'start9' ? null : provider.unionValueKey.username,
password: name === 'start9' ? null : provider.unionValueKey.password,
username: name === 'start9' ? null : provider.value.username,
password: name === 'start9' ? null : provider.value.password,
},
})
return true

View File

@@ -76,7 +76,7 @@ export class SettingsEmailComponent {
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly formService = inject(FormService)
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly api = inject(ApiService)
testAddress = ''

View File

@@ -80,7 +80,7 @@ export class SettingsExperimentalComponent {
private readonly dialogs = inject(TuiDialogService)
private readonly alerts = inject(TuiAlertService)
readonly server$ = inject(PatchDB<DataModel>).watch$('server-info')
readonly server$ = inject<PatchDB<DataModel>>(PatchDB).watch$('server-info')
readonly isTor = inject(ConfigService).isTor()
wipe = false

View File

@@ -29,7 +29,8 @@ export class StartOsUiComponent {
.watch$('serverInfo', 'ui')
.pipe(
map(hosts => {
const serviceInterface: T.ServiceInterfaceWithHostInfo = {
// @TODO Matt fix types
const serviceInterface: T.ServiceInterface = {
id: 'startos-ui',
name: 'StartOS UI',
description: 'The primary web user interface for StartOS',
@@ -60,7 +61,7 @@ export class StartOsUiComponent {
kind: 'multi',
hostnames: hosts,
},
}
} as any
return {
...serviceInterface,

View File

@@ -40,7 +40,7 @@ export class SettingsProxiesComponent {
private readonly api = inject(ApiService)
private readonly formDialog = inject(FormDialogService)
readonly proxies$ = inject(PatchDB<DataModel>).watch$(
readonly proxies$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'serverInfo',
'network',
'proxies',

View File

@@ -65,5 +65,5 @@ import { RouterPortComponent } from './table.component'
],
})
export class SettingsRouterComponent {
readonly server$ = inject(PatchDB<DataModel>).watch$('serverInfo')
readonly server$ = inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo')
}

View File

@@ -99,7 +99,7 @@ export class SettingsWifiComponent {
private readonly cdr = inject(ChangeDetectorRef)
readonly wifi$ = merge(this.getWifi$(), this.update$)
readonly enabled$ = inject(PatchDB<DataModel>).watch$(
readonly enabled$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'serverInfo',
'network',
'wifi',

View File

@@ -39,7 +39,7 @@ export class SettingsService {
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly formDialog = inject(FormDialogService)
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly api = inject(ApiService)
private readonly auth = inject(AuthService)
private readonly isTor = inject(ConfigService).isTor()

View File

@@ -10,7 +10,7 @@ import {
MarketplacePkg,
} from '@start9labs/marketplace'
import {
Emver,
Exver,
ErrorService,
LoadingService,
SharedPipesModule,
@@ -37,7 +37,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
*ngIf="button !== null && button !== 'Install'"
tuiButton
appearance="tertiary-solid"
[routerLink]="'/portal/service/' + package.manifest.id"
[routerLink]="'/portal/service/' + package.id"
>
View installed
</a>
@@ -47,7 +47,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
</div>
</marketplace-package-hero>
<marketplace-about [pkg]="package" />
@if (!(package.manifest.dependencies | empty)) {
@if (!(package.dependencyMetadata | empty)) {
<marketplace-dependencies [pkg]="package" (open)="open($event)" />
}
<marketplace-additional [pkg]="package" />
@@ -93,18 +93,18 @@ export class SideloadPackageComponent {
private readonly errorService = inject(ErrorService)
private readonly router = inject(Router)
private readonly alerts = inject(TuiAlertService)
private readonly emver = inject(Emver)
private readonly exver = inject(Exver)
readonly button$ = combineLatest([
inject(ClientStorageService).showDevTools$,
inject(PatchDB<DataModel>)
inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData')
.pipe(
map(local =>
local[this.package.manifest.id]
? this.emver.compare(
getManifest(local[this.package.manifest.id]).version,
this.package.manifest.version,
local[this.package.id]
? this.exver.compareExver(
getManifest(local[this.package.id]).version,
this.package.version,
)
: null,
),
@@ -132,14 +132,12 @@ export class SideloadPackageComponent {
async upload() {
const loader = this.loader.open('Uploading package').subscribe()
const { manifest, icon } = this.package
const { size } = this.file
try {
const pkg = await this.api.sideloadPackage({ manifest, icon, size })
const { upload } = await this.api.sideloadPackage()
await this.api.uploadPackage(pkg, this.file)
await this.router.navigate(['/portal/service', manifest.id])
await this.api.uploadPackage(upload, this.file).catch(console.error)
await this.router.navigate(['/portal/service', this.package.id])
this.alerts
.open('Package uploaded successfully', { status: 'success' })

View File

@@ -12,8 +12,6 @@ import { Subject } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service'
import { SideloadPackageComponent } from './package.component'
import { parseS9pk, validateS9pk } from './sideload.utils'
@Component({
template: `
<ng-container *ngIf="refresh$ | async"></ng-container>
@@ -105,13 +103,14 @@ export default class SideloadComponent {
this.package = null
}
// @TODO Alex refactor sideload
async onFile(file: File | null) {
if (!file || !(await validateS9pk(file))) {
this.invalid = true
} else {
this.package = await parseS9pk(file)
this.file = file
}
// if (!file || !(await validateS9pk(file))) {
// this.invalid = true
// } else {
// this.package = await parseS9pk(file)
// this.file = file
// }
this.refresh$.next()
}

View File

@@ -1,158 +0,0 @@
import { MarketplacePkg } from '@start9labs/marketplace'
import cbor from 'cbor'
interface Positions {
[key: string]: [bigint, bigint] // [position, length]
}
const MAGIC = new Uint8Array([59, 59])
const VERSION = new Uint8Array([1])
export async function validateS9pk(file: File): Promise<boolean> {
const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2)))
const version = new Uint8Array(await blobToBuffer(file.slice(2, 3)))
return compare(magic, MAGIC) && compare(version, VERSION)
}
export async function parseS9pk(file: File): Promise<MarketplacePkg> {
const positions: Positions = {}
// magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point
let start = 103
let end = start + 1 // 104
const tocLength = new DataView(
await blobToBuffer(file.slice(99, 103) ?? new Blob()),
).getUint32(0, false)
await getPositions(start, end, file, positions, tocLength as any)
const manifest = await getAsset(positions, file, 'manifest')
const [icon] = await Promise.all([
await getIcon(positions, file),
// getAsset(positions, file, 'license'),
// getAsset(positions, file, 'instructions'),
])
return {
manifest,
icon,
license: '',
instructions: '',
categories: [],
versions: [],
dependencyMetadata: {},
publishedAt: '',
}
}
async function getPositions(
initialStart: number,
initialEnd: number,
file: Blob,
positions: Positions,
tocLength: number,
) {
let start = initialStart
let end = initialEnd
const titleLength = new Uint8Array(
await blobToBuffer(file.slice(start, end)),
)[0]
const tocTitle = await file.slice(end, end + titleLength).text()
start = end + titleLength
end = start + 8
const chapterPosition = new DataView(
await blobToBuffer(file.slice(start, end)),
).getBigUint64(0, false)
start = end
end = start + 8
const chapterLength = new DataView(
await blobToBuffer(file.slice(start, end)),
).getBigUint64(0, false)
positions[tocTitle] = [chapterPosition, chapterLength]
start = end
end = start + 1
if (end <= tocLength + (initialStart - 1)) {
await getPositions(start, end, file, positions, tocLength)
}
}
async function readBlobAsDataURL(
f: Blob | File,
): Promise<string | ArrayBuffer | null> {
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.onloadend = () => {
resolve(reader.result)
}
reader.readAsDataURL(f)
reader.onerror = _ => reject(new Error('error reading blob'))
})
}
async function blobToDataURL(data: Blob | File): Promise<string> {
const res = await readBlobAsDataURL(data)
if (res instanceof ArrayBuffer) {
throw new Error('readBlobAsDataURL response should not be an array buffer')
}
if (res == null) {
throw new Error('readBlobAsDataURL response should not be null')
}
if (typeof res === 'string') return res
throw new Error('no possible blob to data url resolution found')
}
async function blobToBuffer(data: Blob | File): Promise<ArrayBuffer> {
const res = await readBlobToArrayBuffer(data)
if (res instanceof String) {
throw new Error('readBlobToArrayBuffer response should not be a string')
}
if (res == null) {
throw new Error('readBlobToArrayBuffer response should not be null')
}
if (res instanceof ArrayBuffer) return res
throw new Error('no possible blob to array buffer resolution found')
}
async function readBlobToArrayBuffer(
f: Blob | File,
): Promise<string | ArrayBuffer | null> {
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.onloadend = () => {
resolve(reader.result)
}
reader.readAsArrayBuffer(f)
reader.onerror = _ => reject(new Error('error reading blob'))
})
}
function compare(a: Uint8Array, b: Uint8Array) {
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
async function getAsset(
positions: Positions,
file: Blob,
asset: 'manifest' | 'license' | 'instructions',
): Promise<any> {
const data = await blobToBuffer(
file.slice(
Number(positions[asset][0]),
Number(positions[asset][0]) + Number(positions[asset][1]),
),
)
return cbor.decode(data, true)
}
async function getIcon(positions: Positions, file: Blob): Promise<string> {
const contentType = '' // @TODO
const data = file.slice(
Number(positions['icon'][0]),
Number(positions['icon'][0]) + Number(positions['icon'][1]),
contentType,
)
return blobToDataURL(data)
}

View File

@@ -1,5 +1,5 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { Emver } from '@start9labs/shared'
import { Exver } from '@start9labs/shared'
import { MarketplacePkg } from '@start9labs/marketplace'
import {
InstalledState,
@@ -12,7 +12,7 @@ import {
standalone: true,
})
export class FilterUpdatesPipe implements PipeTransform {
private readonly emver = inject(Emver)
private readonly exver = inject(Exver)
transform(
pkgs?: MarketplacePkg[],
@@ -20,10 +20,10 @@ export class FilterUpdatesPipe implements PipeTransform {
): MarketplacePkg[] | null {
return (
pkgs?.filter(
({ manifest }) =>
this.emver.compare(
manifest.version,
local?.[manifest.id]?.stateInfo.manifest.version,
({ version, id }) =>
this.exver.compareExver(
version,
local?.[id]?.stateInfo.manifest.version || '',
) === 1,
) || null
)

View File

@@ -5,7 +5,6 @@ import {
MarketplacePkg,
} from '@start9labs/marketplace'
import {
EmverPipesModule,
MarkdownPipeModule,
SafeLinksDirective,
SharedPipesModule,
@@ -45,12 +44,12 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps'
<img alt="" [src]="marketplacePkg.icon" />
</tui-avatar>
<div [style.flex]="1" [style.overflow]="'hidden'">
<strong>{{ marketplacePkg.manifest.title }}</strong>
<strong>{{ marketplacePkg.title }}</strong>
<div>
{{ localPkg.stateInfo.manifest.version | displayEmver }}
{{ localPkg.stateInfo.manifest.version }}
<tui-icon icon="@tui.arrow-right" [style.font-size.rem]="1" />
<span [style.color]="'var(--tui-text-positive)'">
{{ marketplacePkg.manifest.version | displayEmver }}
{{ marketplacePkg.version }}
</span>
</div>
<div [style.color]="'var(--tui-text-negative)'">{{ errors }}</div>
@@ -84,16 +83,13 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps'
<strong>What's new</strong>
<p
safeLinks
[innerHTML]="
marketplacePkg.manifest.releaseNotes | markdown | dompurify
"
[innerHTML]="marketplacePkg.releaseNotes | markdown | dompurify"
></p>
<a
tuiLink
iconAlign="right"
icon="@tui.external-link"
iconEnd="@tui.external-link"
routerLink="/marketplace"
[queryParams]="{ url: url, id: marketplacePkg.manifest.id }"
[queryParams]="{ url: url, id: marketplacePkg.id }"
>
View listing
</a>
@@ -115,7 +111,6 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps'
standalone: true,
imports: [
RouterLink,
EmverPipesModule,
MarkdownPipeModule,
NgDompurifyModule,
SafeLinksDirective,
@@ -132,7 +127,7 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps'
})
export class UpdatesItemComponent {
private readonly dialogs = inject(TuiDialogService)
private readonly patch = inject(PatchDB<DataModel>)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly marketplace = inject(
AbstractMarketplaceService,
) as MarketplaceService
@@ -147,7 +142,7 @@ export class UpdatesItemComponent {
url!: string
get pkgId(): string {
return this.marketplacePkg.manifest.id
return this.marketplacePkg.id
}
get errors(): string {
@@ -159,7 +154,7 @@ export class UpdatesItemComponent {
}
async onClick() {
const { id } = this.marketplacePkg.manifest
const { id } = this.marketplacePkg
delete this.marketplace.updateErrors[id]
this.marketplace.updateQueue[id] = true
@@ -178,7 +173,7 @@ export class UpdatesItemComponent {
}
private async update() {
const { id, version } = this.marketplacePkg.manifest
const { id, version } = this.marketplacePkg
try {
await this.marketplace.installPackage(id, version, this.url)

View File

@@ -35,7 +35,7 @@ import { isInstalled, isUpdating } from 'src/app/utils/get-package-data'
@for (pkg of pkgs; track pkg) {
<updates-item
[marketplacePkg]="pkg"
[localPkg]="data.local[pkg.manifest.id]"
[localPkg]="data.local[pkg.id]"
[url]="host.url"
/>
} @empty {
@@ -76,7 +76,7 @@ export default class UpdatesComponent {
readonly data$ = combineLatest({
hosts: this.service.getKnownHosts$(true),
mp: this.service.getMarketplace$(),
local: inject(PatchDB<DataModel>)
local: inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData')
.pipe(
map(pkgs =>