mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 06:19:44 +00:00
fix: fix build after minor merged into major
This commit is contained in:
@@ -17,4 +17,4 @@ const ROUTES: Routes = [
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(ROUTES)],
|
||||
})
|
||||
export class DiagnosticModule {}
|
||||
export default class DiagnosticModule {}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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: '' } },
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 || '')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) || {}"
|
||||
|
||||
@@ -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 : {},
|
||||
),
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -33,5 +33,5 @@ export class ServiceHealthChecksComponent {
|
||||
@Input({ required: true })
|
||||
checks: readonly T.HealthCheckResult[] = []
|
||||
|
||||
readonly connected$ = inject(ConnectionService).connected$
|
||||
readonly connected$ = inject(ConnectionService)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || {})),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BackupInfo } from 'src/app/services/api/api.types'
|
||||
|
||||
export interface RecoverData {
|
||||
targetId: string
|
||||
serverId: string
|
||||
backupInfo: BackupInfo
|
||||
password: string
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]) => ({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
Reference in New Issue
Block a user