fix: fix building UI project (#2794)

* fix: fix building UI project

* fix makefile

* inputspec instead of config

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Alex Inkin
2024-12-03 03:44:27 +04:00
committed by GitHub
parent 75e7556bfa
commit 9f640b24b3
54 changed files with 9188 additions and 6748 deletions

View File

@@ -295,7 +295,7 @@ web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC)
touch web/dist/raw/setup-wizard/index.html touch web/dist/raw/setup-wizard/index.html
web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
npm --prefix web run build:install-wiz npm --prefix web run build:install
touch web/dist/raw/install-wizard/index.html touch web/dist/raw/install-wizard/index.html
$(COMPRESSED_WEB_UIS): $(WEB_UIS) $(ENVIRONMENT_FILE) $(COMPRESSED_WEB_UIS): $(WEB_UIS) $(ENVIRONMENT_FILE)

View File

@@ -47,7 +47,7 @@ export class S9pk {
), ),
) )
return new S9pk(manifest, archive, source.length) return new S9pk(manifest, archive, source.size)
} }
async icon(): Promise<DataUrl> { async icon(): Promise<DataUrl> {
const iconName = Object.keys(this.archive.contents.contents).find( const iconName = Object.keys(this.archive.contents.contents).find(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

7864
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,7 @@
"check:install": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck", "check:install": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck",
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", "check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck", "check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
"build:deps": "rm -rf .angular/cache && (cd ../patch-db/client && npm ci && npm run build) && (cd ../sdk && make bundle)", "build:deps": "rimraf .angular/cache && (cd ../sdk && make bundle) && (cd ../patch-db/client && npm ci && npm run build)",
"build:deps:win": "rimraf .angular/cache && (cd ../sdk && npm ci && npm run build) && (cd ../patch-db/client && npm ci && npm run build)",
"build:install": "ng run install-wizard:build", "build:install": "ng run install-wizard:build",
"build:setup": "ng run setup-wizard:build", "build:setup": "ng run setup-wizard:build",
"build:ui": "ng run ui:build", "build:ui": "ng run ui:build",
@@ -44,24 +43,22 @@
"@angular/router": "^17.3.1", "@angular/router": "^17.3.1",
"@angular/service-worker": "^17.3.1", "@angular/service-worker": "^17.3.1",
"@materia-ui/ngx-monaco-editor": "^6.0.0", "@materia-ui/ngx-monaco-editor": "^6.0.0",
"@ng-web-apis/common": "^3.2.3",
"@ng-web-apis/mutation-observer": "^3.2.3",
"@ng-web-apis/resize-observer": "^3.2.3",
"@noble/curves": "^1.4.0", "@noble/curves": "^1.4.0",
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.2.2", "@start9labs/argon2": "^0.2.2",
"@start9labs/start-sdk": "file:../sdk/baseDist", "@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.0.0-rc.7", "@taiga-ui/addon-charts": "4.16.0",
"@taiga-ui/addon-commerce": "4.0.0-rc.7", "@taiga-ui/addon-commerce": "4.16.0",
"@taiga-ui/addon-mobile": "4.0.0-rc.7", "@taiga-ui/addon-mobile": "4.16.0",
"@taiga-ui/cdk": "4.0.0-rc.7", "@taiga-ui/cdk": "4.16.0",
"@taiga-ui/core": "4.0.0-rc.7", "@taiga-ui/core": "4.16.0",
"@taiga-ui/event-plugins": "^4.0.1", "@taiga-ui/event-plugins": "4.3.1",
"@taiga-ui/icons": "4.0.0-rc.7", "@taiga-ui/icons": "4.16.0",
"@taiga-ui/kit": "4.0.0-rc.7", "@taiga-ui/kit": "4.16.0",
"@taiga-ui/layout": "4.0.0-rc.7", "@taiga-ui/layout": "4.16.0",
"@taiga-ui/legacy": "4.0.0-rc.7", "@taiga-ui/legacy": "4.16.0",
"@taiga-ui/styles": "4.0.0-rc.7", "@taiga-ui/polymorpheus": "4.7.4",
"@taiga-ui/styles": "4.16.0",
"@tinkoff/ng-dompurify": "4.0.0", "@tinkoff/ng-dompurify": "4.0.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
@@ -88,7 +85,7 @@
"rxjs": "^7.5.6", "rxjs": "^7.5.6",
"swiper": "^8.2.4", "swiper": "^8.2.4",
"ts-matches": "^5.5.1", "ts-matches": "^5.5.1",
"tslib": "^2.6.3", "tslib": "^2.8.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"zone.js": "^0.14.2" "zone.js": "^0.14.2"
}, },

View File

@@ -1,4 +1,8 @@
<tui-notification *ngIf="error$ | async as error" status="error" safeLinks> <tui-notification
*ngIf="error$ | async as error"
appearance="negative"
safeLinks
>
{{ error }} {{ error }}
</tui-notification> </tui-notification>

View File

@@ -31,15 +31,6 @@ export class DurationToSecondsPipe implements PipeTransform {
} }
} }
export function convertBytes(bytes: number) {
if (bytes === 0) return '0 Bytes'
const k = 1000
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const unitsToSeconds: Record<string, number> = { const unitsToSeconds: Record<string, number> = {

View File

@@ -15,7 +15,7 @@ export class ErrorService extends ErrorHandler {
this.alerts this.alerts
.open(getErrorMessage(error, link), { .open(getErrorMessage(error, link), {
label: 'Error', label: 'Error',
status: 'error', appearance: 'negative',
}) })
.subscribe() .subscribe()
} }

View File

@@ -22,8 +22,8 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
[tuiAlert]="!!(visible$ | async)" [tuiAlert]="!!(visible$ | async)"
[tuiAlertOptions]="{ [tuiAlertOptions]="{
label: 'StartOS download complete!', label: 'StartOS download complete!',
status: 'success', appearance: 'positive',
autoClose: 0 autoClose: 0,
}" }"
(tuiAlertChange)="onDismiss()" (tuiAlertChange)="onDismiss()"
> >

View File

@@ -7,7 +7,6 @@ import {
} from '@angular/core' } from '@angular/core'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { AbstractControl, FormArrayName } from '@angular/forms' import { AbstractControl, FormArrayName } from '@angular/forms'
import { CT } from '@start9labs/start-sdk'
import { import {
TUI_ANIMATIONS_SPEED, TUI_ANIMATIONS_SPEED,
TuiDialogService, TuiDialogService,

View File

@@ -52,7 +52,7 @@ export class FormControlComponent<
this.alerts this.alerts
.open<boolean>(this.warning, { .open<boolean>(this.warning, {
label: 'Warning', label: 'Warning',
status: 'warning', appearance: 'warning',
closeable: false, closeable: false,
autoClose: 0, autoClose: 0,
}) })

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { TuiFileLike } from '@taiga-ui/kit' import { TuiFileLike } from '@taiga-ui/kit'
import { CT } from '@start9labs/start-sdk' import { IST } from '@start9labs/start-sdk'
import { Control } from '../control' import { Control } from '../control'
@Component({ @Component({
@@ -8,4 +8,7 @@ import { Control } from '../control'
templateUrl: './form-file.component.html', templateUrl: './form-file.component.html',
styleUrls: ['./form-file.component.scss'], styleUrls: ['./form-file.component.scss'],
}) })
export class FormFileComponent extends Control<CT.ValueSpecFile, TuiFileLike> {} export class FormFileComponent extends Control<
IST.ValueSpecFile,
TuiFileLike
> {}

View File

@@ -1,4 +1,4 @@
import { CB, CT, T, utils } from '@start9labs/start-sdk' import { ISB, IST, T, utils } from '@start9labs/start-sdk'
import { TuiDialogOptions } from '@taiga-ui/core' import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiConfirmData } from '@taiga-ui/kit' import { TuiConfirmData } from '@taiga-ui/kit'
import { NetworkInfo } from 'src/app/services/patch-db/data-model' import { NetworkInfo } from 'src/app/services/patch-db/data-model'
@@ -22,7 +22,7 @@ export const REMOVE: Partial<TuiDialogOptions<TuiConfirmData>> = {
export function getClearnetSpec({ export function getClearnetSpec({
domains, domains,
start9ToSubdomain, start9ToSubdomain,
}: NetworkInfo): Promise<CT.InputSpec> { }: NetworkInfo): Promise<IST.InputSpec> {
const start9ToDomain = `${start9ToSubdomain?.value}.start9.to` const start9ToDomain = `${start9ToSubdomain?.value}.start9.to`
const base = start9ToSubdomain ? { [start9ToDomain]: start9ToDomain } : {} const base = start9ToSubdomain ? { [start9ToDomain]: start9ToDomain } : {}
@@ -34,15 +34,16 @@ export function getClearnetSpec({
}, base) }, base)
return configBuilderToSpec( return configBuilderToSpec(
CB.Config.of({ ISB.InputSpec.of({
domain: CB.Value.select({ domain: ISB.Value.select({
name: 'Domain', name: 'Domain',
required: { default: null }, default: domains[0].value,
values, values,
}), }),
subdomain: CB.Value.text({ subdomain: ISB.Value.text({
name: 'Subdomain', name: 'Subdomain',
required: false, required: false,
default: '',
}), }),
}), }),
) )

View File

@@ -44,7 +44,7 @@ const RUNNING = ['running', 'starting', 'restarting']
*tuiLet="hasUnmet() | async as hasUnmet" *tuiLet="hasUnmet() | async as hasUnmet"
tuiIconButton tuiIconButton
iconStart="@tui.play" iconStart="@tui.play"
[disabled]="status().primary !== 'stopped' || !pkg().status.configured" [disabled]="status().primary !== 'stopped'"
(click)="actions.start(manifest(), !!hasUnmet)" (click)="actions.start(manifest(), !!hasUnmet)"
> >
Start Start

View File

@@ -62,7 +62,8 @@ export class StatusComponent {
return ( return (
!this.hasDepErrors && // no deps error !this.hasDepErrors && // no deps error
!!this.pkg.status.configured && // no config needed // @TODO Matt how do we handle this now?
// !!this.pkg.status.configured && // no config needed
status.health !== 'failure' // no health issues status.health !== 'failure' // no health issues
) )
} }
@@ -86,8 +87,9 @@ export class StatusComponent {
return 'Running' return 'Running'
case 'stopped': case 'stopped':
return 'Stopped' return 'Stopped'
case 'needsConfig': // @TODO Matt just dropping this?
return 'Needs Config' // case 'needsConfig':
// return 'Needs Config'
case 'updating': case 'updating':
return 'Updating...' return 'Updating...'
case 'stopping': case 'stopping':
@@ -111,8 +113,9 @@ export class StatusComponent {
switch (this.getStatus(this.pkg).primary) { switch (this.getStatus(this.pkg).primary) {
case 'running': case 'running':
return 'var(--tui-status-positive)' return 'var(--tui-status-positive)'
case 'needsConfig': // @TODO Matt just dropping this?
return 'var(--tui-status-warning)' // case 'needsConfig':
// return 'var(--tui-status-warning)'
case 'installing': case 'installing':
case 'updating': case 'updating':
case 'stopping': case 'stopping':

View File

@@ -69,7 +69,7 @@ export class UILaunchComponent {
} }
get isRunning(): boolean { get isRunning(): boolean {
return this.pkg.status.main.status === 'running' return this.pkg.status.main === 'running'
} }
get first(): T.ServiceInterface | undefined { get first(): T.ServiceInterface | undefined {

View File

@@ -1,56 +0,0 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { CopyService } from '@start9labs/shared'
import { TuiDialogContext, TuiButton } from '@taiga-ui/core'
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
import { QrCodeModule } from 'ng-qrcode'
import { ActionResponse } from 'src/app/services/api/api.types'
@Component({
template: `
{{ context.data.message }}
<ng-container *ngIf="context.data.value">
<qr-code
*ngIf="context.data.qr"
size="240"
[value]="context.data.value"
></qr-code>
<p>
{{ context.data.value }}
<button
*ngIf="context.data.copyable"
tuiIconButton
appearance="flat"
iconStart="@tui.copy"
(click)="copyService.copy(context.data.value)"
>
Copy
</button>
</p>
</ng-container>
`,
styles: [
`
qr-code {
margin: 1rem auto;
display: flex;
justify-content: center;
}
p {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, QrCodeModule, TuiButton],
})
export class ServiceActionSuccessComponent {
readonly copyService = inject(CopyService)
readonly context =
inject<TuiDialogContext<void, ActionResponse>>(POLYMORPHEUS_CONTEXT)
}

View File

@@ -103,7 +103,9 @@ export class ServiceActionsComponent {
} }
get canConfigure(): boolean { get canConfigure(): boolean {
return !this.service.pkg.status.configured // @TODO Matt should we just drop this?
// return !this.service.pkg.status.configured
return false
} }
@tuiPure @tuiPure

View File

@@ -0,0 +1,44 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TuiTitle } from '@taiga-ui/core'
import { TuiAccordion, TuiFade } from '@taiga-ui/kit'
import { ActionSuccessMemberComponent } from './action-success-member.component'
import { GroupResult } from './types'
@Component({
standalone: true,
selector: 'app-action-success-group',
template: `
@for (member of group.value; track $index) {
<p>
@if (member.type === 'single') {
<app-action-success-member [member]="member" />
}
@if (member.type === 'group') {
<tui-accordion-item>
<div tuiFade>{{ member.name }}</div>
<ng-template tuiAccordionItemContent>
<app-action-success-group [group]="member" />
</ng-template>
</tui-accordion-item>
}
</p>
}
`,
styles: [
`
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiTitle, ActionSuccessMemberComponent, TuiAccordion, TuiFade],
})
export class ActionSuccessGroupComponent {
@Input()
group!: GroupResult
}

View File

@@ -0,0 +1,172 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
inject,
Input,
TemplateRef,
ViewChild,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { T } from '@start9labs/start-sdk'
import { TuiButton, TuiDialogService, TuiTitle } from '@taiga-ui/core'
import {
TuiInputModule,
TuiTextfieldComponent,
TuiTextfieldControllerModule,
} from '@taiga-ui/legacy'
import { QrCodeModule } from 'ng-qrcode'
@Component({
standalone: true,
selector: 'app-action-success-member',
template: `
<tui-input
[readOnly]="true"
[ngModel]="member.value"
[tuiTextfieldCustomContent]="actions"
>
{{ member.name }}
<input
tuiTextfieldLegacy
[style.border-inline-end-width.rem]="border"
[type]="member.masked && masked ? 'password' : 'text'"
/>
</tui-input>
@if (member.description) {
<label [style.padding-top.rem]="0.25" tuiTitle>
<span tuiSubtitle [style.opacity]="0.8">{{ member.description }}</span>
</label>
}
<ng-template #actions>
@if (member.masked) {
<button
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
[style.pointer-events]="'auto'"
(click)="masked = !masked"
>
Reveal/Hide
</button>
}
@if (member.copyable) {
<button
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
iconStart="@tui.copy"
[style.pointer-events]="'auto'"
(click)="copy()"
>
Copy
</button>
}
@if (member.qr) {
<button
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
iconStart="@tui.qr-code"
[style.pointer-events]="'auto'"
(click)="show(qr)"
>
Show QR
</button>
}
</ng-template>
<ng-template #qr>
<qr-code
[value]="member.value"
[style.filter]="member.masked && masked ? 'blur(0.5rem)' : null"
size="350"
/>
@if (member.masked && masked) {
<button
tuiIconButton
class="reveal"
iconStart="@tui.eye"
[style.border-radius.%]="100"
(click)="masked = false"
>
Reveal
</button>
}
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
@import '@taiga-ui/core/styles/taiga-ui-local';
.reveal {
@include center-all();
}
.qr {
position: relative;
text-align: center;
}
`,
],
imports: [
FormsModule,
TuiInputModule,
TuiTextfieldControllerModule,
TuiButton,
QrCodeModule,
TuiTitle,
],
})
export class ActionSuccessMemberComponent {
@ViewChild(TuiTextfieldComponent, { read: ElementRef })
private readonly input!: ElementRef<HTMLInputElement>
private readonly dialogs = inject(TuiDialogService)
@Input()
member!: T.ActionResultMember & { type: 'single' }
masked = true
get border(): number {
let border = 0
if (this.member.masked) border += 2
if (this.member.copyable) border += 2
if (this.member.qr) border += 2
return border
}
show(template: TemplateRef<any>) {
const masked = this.masked
this.masked = this.member.masked
this.dialogs
.open(template, { label: 'Scan this QR', size: 's' })
.subscribe({
complete: () => (this.masked = masked),
})
}
copy() {
const el = this.input.nativeElement
if (!el) {
return
}
el.type = 'text'
el.focus()
el.select()
el.ownerDocument.execCommand('copy')
el.type = this.masked && this.member.masked ? 'password' : 'text'
}
}

View File

@@ -0,0 +1,140 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
ViewChild,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiButton } from '@taiga-ui/core'
import {
TuiInputModule,
TuiTextfieldComponent,
TuiTextfieldControllerModule,
} from '@taiga-ui/legacy'
import { QrCodeModule } from 'ng-qrcode'
import { SingleResult } from './types'
@Component({
standalone: true,
selector: 'app-action-success-single',
template: `
<p class="qr"><ng-container *ngTemplateOutlet="qr" /></p>
<tui-input
[readOnly]="true"
[ngModel]="single.value"
[tuiTextfieldLabelOutside]="true"
[tuiTextfieldCustomContent]="actions"
>
<input
tuiTextfieldLegacy
[style.border-inline-end-width.rem]="border"
[type]="single.masked && masked ? 'password' : 'text'"
/>
</tui-input>
<ng-template #actions>
@if (single.masked) {
<button
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
[style.pointer-events]="'auto'"
(click)="masked = !masked"
>
Reveal/Hide
</button>
}
@if (single.copyable) {
<button
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
iconStart="@tui.copy"
[style.pointer-events]="'auto'"
(click)="copy()"
>
Copy
</button>
}
</ng-template>
<ng-template #qr>
<qr-code
[value]="single.value"
[style.filter]="single.masked && masked ? 'blur(0.5rem)' : null"
size="350"
/>
@if (single.masked && masked) {
<button
tuiIconButton
class="reveal"
iconStart="@tui.eye"
[style.border-radius.%]="100"
(click)="masked = false"
>
Reveal
</button>
}
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
@import '@taiga-ui/core/styles/taiga-ui-local';
.reveal {
@include center-all();
}
.qr {
position: relative;
text-align: center;
}
`,
],
imports: [
CommonModule,
FormsModule,
TuiInputModule,
TuiTextfieldControllerModule,
TuiButton,
QrCodeModule,
],
})
export class ActionSuccessSingleComponent {
@ViewChild(TuiTextfieldComponent, { read: ElementRef })
private readonly input!: ElementRef<HTMLInputElement>
@Input()
single!: SingleResult
masked = true
get border(): number {
let border = 0
if (this.single.masked) border += 2
if (this.single.copyable) border += 2
return border
}
copy() {
const el = this.input.nativeElement
if (!el) {
return
}
el.type = 'text'
el.focus()
el.select()
el.ownerDocument.execCommand('copy')
el.type = this.masked && this.single.masked ? 'password' : 'text'
}
}

View File

@@ -0,0 +1,31 @@
import { Component, inject } from '@angular/core'
import { TuiDialogContext } from '@taiga-ui/core'
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
import { ActionSuccessGroupComponent } from './action-success-group.component'
import { ActionSuccessSingleComponent } from './action-success-single.component'
import { ActionResponseWithResult } from './types'
@Component({
standalone: true,
template: `
@if (data.message) {
<p>{{ data.message }}</p>
}
@if (single) {
<app-action-success-single [single]="single" />
}
@if (group) {
<app-action-success-group [group]="group" />
}
`,
imports: [ActionSuccessGroupComponent, ActionSuccessSingleComponent],
})
export class ActionSuccessPage {
readonly data =
inject<TuiDialogContext<void, ActionResponseWithResult>>(
POLYMORPHEUS_CONTEXT,
).data
readonly single = this.data.result.type === 'single' ? this.data.result : null
readonly group = this.data.result.type === 'group' ? this.data.result : null
}

View File

@@ -0,0 +1,7 @@
import { RR } from 'src/app/services/api/api.types'
type ActionResponse = NonNullable<RR.ActionRes>
type ActionResult = NonNullable<ActionResponse['result']>
export type ActionResponseWithResult = ActionResponse & { result: ActionResult }
export type SingleResult = ActionResult & { type: 'single' }
export type GroupResult = ActionResult & { type: 'group' }

View File

@@ -55,7 +55,8 @@ export class ServicePropertiesModal {
this.loading$.next(true) this.loading$.next(true)
try { try {
this.properties = await this.api.getPackageProperties({ id: this.id }) // @TODO Matt this needs complete rework, right?
// this.properties = await this.api.getPackageProperties({ id: this.id })
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {

View File

@@ -5,10 +5,11 @@ import { T } from '@start9labs/start-sdk'
import { TuiDialogService } from '@taiga-ui/core' import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { from } from 'rxjs' import { from } from 'rxjs'
import { // @TODO Alex implement config
ConfigModal, // import {
PackageConfigData, // ConfigModal,
} from 'src/app/routes/portal/modals/config.component' // PackageConfigData,
// } from 'src/app/routes/portal/modals/config.component'
import { ServiceAdditionalModal } from 'src/app/routes/portal/routes/service/modals/additional.component' import { ServiceAdditionalModal } from 'src/app/routes/portal/routes/service/modals/additional.component'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
@@ -131,9 +132,9 @@ export class ToMenuPipe implements PipeTransform {
} }
private openConfig({ title, id }: T.Manifest) { private openConfig({ title, id }: T.Manifest) {
this.formDialog.open<PackageConfigData>(ConfigModal, { // this.formDialog.open<PackageConfigData>(ConfigModal, {
label: `${title} configuration`, // label: `${title} configuration`,
data: { pkgId: id }, // data: { pkgId: id },
}) // })
} }
} }

View File

@@ -22,7 +22,7 @@ import {
import { hasCurrentDeps } from 'src/app/utils/has-deps' import { hasCurrentDeps } from 'src/app/utils/has-deps'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
import { ServiceActionComponent } from '../components/action.component' import { ServiceActionComponent } from '../components/action.component'
import { ServiceActionSuccessComponent } from '../components/action-success.component' import { ActionSuccessPage } from '../modals/action-success/action-success.page'
import { GroupActionsPipe } from '../pipes/group-actions.pipe' import { GroupActionsPipe } from '../pipes/group-actions.pipe'
import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest' import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
@@ -50,7 +50,7 @@ import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
[action]="{ [action]="{
name: action.name, name: action.name,
description: action.description, description: action.description,
icon: '@tui.circle-play' icon: '@tui.circle-play',
}" }"
(click)="handleAction(action)" (click)="handleAction(action)"
></button> ></button>
@@ -92,45 +92,46 @@ export class ServiceActionsRoute {
) {} ) {}
async handleAction(action: WithId<T.ActionMetadata>) { async handleAction(action: WithId<T.ActionMetadata>) {
if (action.disabled) { // @TODO Matt this needs complete rework, right?
this.dialogs // if (action.disabled) {
.open(action.disabled, { // this.dialogs
label: 'Forbidden', // .open(action.disabled, {
size: 's', // label: 'Forbidden',
}) // size: 's',
.subscribe() // })
} else { // .subscribe()
if (action.input && !isEmptyObject(action.input)) { // } else {
this.formDialog.open(FormComponent, { // if (action.input && !isEmptyObject(action.input)) {
label: action.name, // this.formDialog.open(FormComponent, {
data: { // label: action.name,
spec: action.input, // data: {
buttons: [ // spec: action.input,
{ // buttons: [
text: 'Execute', // {
handler: async (value: any) => // text: 'Execute',
this.executeAction(action.id, value), // handler: async (value: any) =>
}, // this.executeAction(action.id, value),
], // },
}, // ],
}) // },
} else { // })
this.dialogs // } else {
.open(TUI_CONFIRM, { // this.dialogs
label: 'Confirm', // .open(TUI_CONFIRM, {
size: 's', // label: 'Confirm',
data: { // size: 's',
content: `Are you sure you want to execute action "${ // data: {
action.name // content: `Are you sure you want to execute action "${
}"? ${action.warning || ''}`, // action.name
yes: 'Execute', // }"? ${action.warning || ''}`,
no: 'Cancel', // yes: 'Execute',
}, // no: 'Cancel',
}) // },
.pipe(filter(Boolean)) // })
.subscribe(() => this.executeAction(action.id)) // .pipe(filter(Boolean))
} // .subscribe(() => this.executeAction(action.id))
} // }
// }
} }
async tryUninstall(pkg: PackageDataEntry): Promise<void> { async tryUninstall(pkg: PackageDataEntry): Promise<void> {
@@ -181,22 +182,20 @@ export class ServiceActionsRoute {
const loader = this.loader.open('Executing action...').subscribe() const loader = this.loader.open('Executing action...').subscribe()
try { try {
const data = await this.embassyApi.executePackageAction({ // @TODO Matt this needs complete rework, right?
id: this.id, // const data = await this.embassyApi.executePackageAction({
actionId, // id: this.id,
input, // actionId,
}) // input,
// })
timer(500) timer(500)
.pipe( .pipe(
switchMap(() => switchMap(() =>
this.dialogs.open( this.dialogs.open(new PolymorpheusComponent(ActionSuccessPage), {
new PolymorpheusComponent(ServiceActionSuccessComponent), label: 'Execution Complete',
{ // data,
label: 'Execution Complete', }),
data,
},
),
), ),
) )
.subscribe() .subscribe()

View File

@@ -5,10 +5,11 @@ import { isEmptyObject } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { combineLatest, map, switchMap } from 'rxjs' import { combineLatest, map, switchMap } from 'rxjs'
import { // @TODO Alex implement config
ConfigModal, // import {
PackageConfigData, // ConfigModal,
} from 'src/app/routes/portal/modals/config.component' // PackageConfigData,
// } from 'src/app/routes/portal/modals/config.component'
import { ServiceBackupsComponent } from 'src/app/routes/portal/routes/service/components/backups.component' import { ServiceBackupsComponent } from 'src/app/routes/portal/routes/service/components/backups.component'
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pipes/install-progress.pipe' import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pipes/install-progress.pipe'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
@@ -180,9 +181,7 @@ export class ServiceRoute {
) )
readonly health$ = this.pkgId$.pipe( readonly health$ = this.pkgId$.pipe(
switchMap(pkgId => switchMap(pkgId => this.patch.watch$('packageData', pkgId, 'status')),
this.patch.watch$('packageData', pkgId, 'status', 'main'),
),
map(toHealthCheck), map(toHealthCheck),
) )
@@ -264,10 +263,11 @@ export class ServiceRoute {
errorText = 'Incorrect version' errorText = 'Incorrect version'
fixText = 'Update' fixText = 'Update'
fixAction = () => this.fixDep(pkg, pkgManifest, 'update', depId) fixAction = () => this.fixDep(pkg, pkgManifest, 'update', depId)
} else if (depError.type === 'configUnsatisfied') { // @TODO Matt do we just remove this case?
errorText = 'Config not satisfied' // } else if (depError.type === 'configUnsatisfied') {
fixText = 'Auto config' // errorText = 'Config not satisfied'
fixAction = () => this.fixDep(pkg, pkgManifest, 'configure', depId) // fixText = 'Auto config'
// fixAction = () => this.fixDep(pkg, pkgManifest, 'configure', depId)
} else if (depError.type === 'notRunning') { } else if (depError.type === 'notRunning') {
errorText = 'Not running' errorText = 'Not running'
fixText = 'Start' fixText = 'Start'
@@ -296,13 +296,13 @@ export class ServiceRoute {
case 'update': case 'update':
return this.installDep(pkg, pkgManifest, depId) return this.installDep(pkg, pkgManifest, depId)
case 'configure': case 'configure':
return this.formDialog.open<PackageConfigData>(ConfigModal, { // return this.formDialog.open<PackageConfigData>(ConfigModal, {
label: `${pkg.currentDependencies[depId].title} config`, // label: `${pkg.currentDependencies[depId].title} config`,
data: { // data: {
pkgId: depId, // pkgId: depId,
dependentInfo: pkgManifest, // dependentInfo: pkgManifest,
}, // },
}) // })
} }
} }
@@ -329,8 +329,10 @@ export class ServiceRoute {
} }
} }
function toHealthCheck(main: T.MainStatus): T.NamedHealthCheckResult[] | null { function toHealthCheck(
return main.status !== 'running' || isEmptyObject(main.health) status: T.MainStatus,
): T.NamedHealthCheckResult[] | null {
return status.main !== 'running' || isEmptyObject(status.health)
? null ? null
: Object.values(main.health) : Object.values(status.health)
} }

View File

@@ -43,7 +43,7 @@ import { RecoverOption } from '../types/recover-option'
<input <input
type="checkbox" type="checkbox"
tuiCheckbox tuiCheckbox
[disabled]="option.installed || option.newerStartOs" [disabled]="option.installed || option.newerOs"
[(ngModel)]="option.checked" [(ngModel)]="option.checked"
/> />
</label> </label>
@@ -85,8 +85,8 @@ export class BackupsRecoverModal {
.watch$('packageData') .watch$('packageData')
.pipe(take(1)) .pipe(take(1))
readonly toMessage = ({ newerStartOs, installed, title }: RecoverOption) => { readonly toMessage = ({ newerOs, installed, title }: RecoverOption) => {
if (newerStartOs) { if (newerOs) {
return { return {
text: `Unavailable. Backup was made on a newer version of StartOS.`, text: `Unavailable. Backup was made on a newer version of StartOS.`,
color: 'var(--tui-status-negative)', color: 'var(--tui-status-negative)',

View File

@@ -6,6 +6,7 @@ import {
signal, signal,
} from '@angular/core' } from '@angular/core'
import { ErrorService, Exver } from '@start9labs/shared' import { ErrorService, Exver } from '@start9labs/shared'
import { Version } from '@start9labs/start-sdk'
import { import {
TuiButton, TuiButton,
TuiDialogContext, TuiDialogContext,
@@ -116,9 +117,8 @@ export class BackupsTargetModal {
hasBackup(target: BackupTarget): boolean { hasBackup(target: BackupTarget): boolean {
return ( return (
target.startOs?.[this.serverId] && target.startOs?.[this.serverId] &&
this.exver.compareOsVersion( Version.parse(target.startOs[this.serverId].version).compare(
target.startOs[this.serverId].version, Version.parse('0.3.6'),
'0.3.6',
) !== 'less' ) !== 'less'
) )
} }

View File

@@ -1,5 +1,4 @@
import { inject, Pipe, PipeTransform } from '@angular/core' import { inject, Pipe, PipeTransform } from '@angular/core'
import { Exver } from '@start9labs/shared'
import { map, Observable } from 'rxjs' import { map, Observable } from 'rxjs'
import { PackageBackupInfo } from 'src/app/services/api/api.types' import { PackageBackupInfo } from 'src/app/services/api/api.types'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
@@ -7,20 +6,12 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { RecoverOption } from '../types/recover-option' import { RecoverOption } from '../types/recover-option'
import { Version } from '@start9labs/start-sdk' import { Version } from '@start9labs/start-sdk'
export interface AppRecoverOption extends PackageBackupInfo {
id: string
checked: boolean
installed: boolean
newerOS: boolean
}
@Pipe({ @Pipe({
name: 'toOptions', name: 'toOptions',
standalone: true, standalone: true,
}) })
export class ToOptionsPipe implements PipeTransform { export class ToOptionsPipe implements PipeTransform {
private readonly config = inject(ConfigService) private readonly config = inject(ConfigService)
private readonly exver = inject(Exver)
transform( transform(
packageData$: Observable<Record<string, PackageDataEntry>>, packageData$: Observable<Record<string, PackageDataEntry>>,
@@ -34,7 +25,7 @@ export class ToOptionsPipe implements PipeTransform {
id, id,
installed: !!packageData[id], installed: !!packageData[id],
checked: false, checked: false,
newerOS: newerOs:
Version.parse(packageBackups[id].osVersion).compare( Version.parse(packageBackups[id].osVersion).compare(
Version.parse(this.config.version), Version.parse(this.config.version),
) === 'greater', ) === 'greater',

View File

@@ -1,92 +1,103 @@
import { CB } from '@start9labs/start-sdk' import { ISB } from '@start9labs/start-sdk'
export const dropboxSpec = CB.Config.of({ export const dropboxSpec = ISB.InputSpec.of({
name: CB.Value.text({ name: ISB.Value.text({
name: 'Name', name: 'Name',
description: 'A friendly name for this Dropbox target', description: 'A friendly name for this Dropbox target',
placeholder: 'My Dropbox', placeholder: 'My Dropbox',
required: { default: null }, required: true,
default: null,
}), }),
token: CB.Value.text({ token: ISB.Value.text({
name: 'Access Token', name: 'Access Token',
description: 'The secret access token for your custom Dropbox app', description: 'The secret access token for your custom Dropbox app',
required: { default: null }, required: true,
default: null,
masked: true, masked: true,
}), }),
path: CB.Value.text({ path: ISB.Value.text({
name: 'Path', name: 'Path',
description: 'The fully qualified path to the backup directory', description: 'The fully qualified path to the backup directory',
placeholder: 'e.g. /Desktop/my-folder', placeholder: 'e.g. /Desktop/my-folder',
required: { default: null }, required: true,
default: null,
}), }),
}) })
export const googleDriveSpec = CB.Config.of({ export const googleDriveSpec = ISB.InputSpec.of({
name: CB.Value.text({ name: ISB.Value.text({
name: 'Name', name: 'Name',
description: 'A friendly name for this Google Drive target', description: 'A friendly name for this Google Drive target',
placeholder: 'My Google Drive', placeholder: 'My Google Drive',
required: { default: null }, required: true,
default: null,
}), }),
path: CB.Value.text({ path: ISB.Value.text({
name: 'Path', name: 'Path',
description: 'The fully qualified path to the backup directory', description: 'The fully qualified path to the backup directory',
placeholder: 'e.g. /Desktop/my-folder', placeholder: 'e.g. /Desktop/my-folder',
required: { default: null }, required: true,
}), default: null,
key: CB.Value.file({
name: 'Private Key File',
description:
'Your Google Drive service account private key file (.json file)',
required: { default: null },
extensions: ['json'],
}), }),
// @TODO Matt do we just drop file specs?
// key: ISB.Value.file({
// name: 'Private Key File',
// description:
// 'Your Google Drive service account private key file (.json file)',
// required: { default: null },
// extensions: ['json'],
// }),
}) })
export const cifsSpec = CB.Config.of({ export const cifsSpec = ISB.InputSpec.of({
name: CB.Value.text({ name: ISB.Value.text({
name: 'Name', name: 'Name',
description: 'A friendly name for this Network Folder', description: 'A friendly name for this Network Folder',
placeholder: 'My Network Folder', placeholder: 'My Network Folder',
required: { default: null }, required: true,
default: null,
}), }),
hostname: CB.Value.text({ hostname: ISB.Value.text({
name: 'Hostname', name: 'Hostname',
description: description:
'The hostname of your target device on the Local Area Network.', 'The hostname of your target device on the Local Area Network.',
warning: null, warning: null,
placeholder: `e.g. 'My Computer' OR 'my-computer.local'`, placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
required: { default: null }, required: true,
default: null,
patterns: [], patterns: [],
}), }),
path: CB.Value.text({ path: ISB.Value.text({
name: 'Path', name: 'Path',
description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`, description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`,
placeholder: 'e.g. my-shared-folder or /Desktop/my-folder', placeholder: 'e.g. my-shared-folder or /Desktop/my-folder',
required: { default: null }, required: true,
default: null,
}), }),
username: CB.Value.text({ username: ISB.Value.text({
name: 'Username', name: 'Username',
description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`, description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`,
required: { default: null }, required: true,
default: null,
placeholder: 'My Network Folder', placeholder: 'My Network Folder',
}), }),
password: CB.Value.text({ password: ISB.Value.text({
name: 'Password', name: 'Password',
description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`, description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`,
required: false, required: false,
masked: true, masked: true,
default: null,
placeholder: 'My Network Folder', placeholder: 'My Network Folder',
}), }),
}) })
export const remoteBackupTargetSpec = CB.Config.of({ export const remoteBackupTargetSpec = ISB.InputSpec.of({
type: CB.Value.union( type: ISB.Value.union(
{ {
name: 'Target Type', name: 'Target Type',
required: { default: 'dropbox' }, default: 'dropbox',
}, },
CB.Variants.of({ ISB.Variants.of({
dropbox: { dropbox: {
name: 'Dropbox', name: 'Dropbox',
spec: dropboxSpec, spec: dropboxSpec,
@@ -103,17 +114,19 @@ export const remoteBackupTargetSpec = CB.Config.of({
), ),
}) })
export const diskBackupTargetSpec = CB.Config.of({ export const diskBackupTargetSpec = ISB.InputSpec.of({
name: CB.Value.text({ name: ISB.Value.text({
name: 'Name', name: 'Name',
description: 'A friendly name for this physical target', description: 'A friendly name for this physical target',
placeholder: 'My Physical Target', placeholder: 'My Physical Target',
required: { default: null }, required: true,
default: null,
}), }),
path: CB.Value.text({ path: ISB.Value.text({
name: 'Path', name: 'Path',
description: 'The fully qualified path to the backup directory', description: 'The fully qualified path to the backup directory',
placeholder: 'e.g. /Backups/my-folder', placeholder: 'e.g. /Backups/my-folder',
required: { default: null }, required: true,
default: null,
}), }),
}) })

View File

@@ -7,7 +7,7 @@ import { ConfigService } from 'src/app/services/config.service'
selector: 'marketplace-notification', selector: 'marketplace-notification',
template: ` template: `
<tui-notification <tui-notification
[status]="status || 'warning'" [appearance]="status || 'warning'"
icon="" icon=""
class="notification-wrapper" class="notification-wrapper"
> >

View File

@@ -1,8 +1,8 @@
import { CT } from '@start9labs/start-sdk' import { IST } from '@start9labs/start-sdk'
import { TuiDialogOptions } from '@taiga-ui/core' import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiConfirmData } from '@taiga-ui/kit' import { TuiConfirmData } from '@taiga-ui/kit'
export function getMarketplaceValueSpec(): CT.ValueSpecObject { export function getMarketplaceValueSpec(): IST.ValueSpecObject {
return { return {
type: 'object', type: 'object',
name: 'Add Custom Registry', name: 'Add Custom Registry',

View File

@@ -5,7 +5,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({ @Component({
selector: 'settings-sync', selector: 'settings-sync',
template: ` template: `
<tui-notification status="warning"> <tui-notification appearance="warning">
<div tuiCell [style.padding]="0"> <div tuiCell [style.padding]="0">
<div tuiTitle> <div tuiTitle>
Clock sync failure Clock sync failure

View File

@@ -1,42 +1,45 @@
import { CB } from '@start9labs/start-sdk' import { ISB } from '@start9labs/start-sdk'
import { Proxy } from 'src/app/services/patch-db/data-model' import { Proxy } from 'src/app/services/patch-db/data-model'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
const auth = CB.Config.of({ const auth = ISB.InputSpec.of({
username: CB.Value.text({ username: ISB.Value.text({
name: 'Username', name: 'Username',
required: { default: null }, required: true,
default: null,
}), }),
password: CB.Value.text({ password: ISB.Value.text({
name: 'Password', name: 'Password',
required: { default: null }, required: true,
default: null,
masked: true, masked: true,
}), }),
}) })
function getStrategyUnion(proxies: Proxy[]) { function getStrategyUnion(proxies: Proxy[]) {
const inboundProxies = proxies const inboundProxies: Record<string, string> = proxies
.filter(p => p.type === 'inbound-outbound') .filter(p => p.type === 'inbound-outbound')
.reduce((prev, curr) => { .reduce(
return { (prev, curr) => ({
[curr.id]: curr.name, [curr.id]: curr.name,
...prev, ...prev,
} }),
}, {}) {},
)
return CB.Value.union( return ISB.Value.union(
{ {
name: 'Networking Strategy', name: 'Networking Strategy',
required: { default: null }, default: 'local',
description: `<h5>Local</h5>Select this option if you do not mind exposing your home/business IP address to the Internet. This option requires configuring router settings, which StartOS can do automatically if you have an OpenWRT router description: `<h5>Local</h5>Select this option if you do not mind exposing your home/business IP address to the Internet. This option requires configuring router settings, which StartOS can do automatically if you have an OpenWRT router
<h5>Proxy</h5>Select this option is you prefer to hide your home/business IP address from the Internet. This option requires running your own Virtual Private Server (VPS) <i>or</i> paying service provider such as Static Wire <h5>Proxy</h5>Select this option is you prefer to hide your home/business IP address from the Internet. This option requires running your own Virtual Private Server (VPS) <i>or</i> paying service provider such as Static Wire
`, `,
}, },
CB.Variants.of({ ISB.Variants.of({
local: { local: {
name: 'Local', name: 'Local',
spec: CB.Config.of({ spec: ISB.InputSpec.of({
ipStrategy: CB.Value.select({ ipStrategy: ISB.Value.select({
name: 'IP Strategy', name: 'IP Strategy',
description: `<h5>IPv6 Only (recommended)</h5><b>Requirements</b>:<ol><li>ISP IPv6 support</li><li>OpenWRT (recommended) or Linksys router</li></ol><b>Pros</b>: Ready for IPv6 Internet. Enhanced privacy. Run multiple clearnet servers from the same network description: `<h5>IPv6 Only (recommended)</h5><b>Requirements</b>:<ol><li>ISP IPv6 support</li><li>OpenWRT (recommended) or Linksys router</li></ol><b>Pros</b>: Ready for IPv6 Internet. Enhanced privacy. Run multiple clearnet servers from the same network
<b>Cons</b>: Interfaces using this domain will only be accessible to people whose ISP supports IPv6 <b>Cons</b>: Interfaces using this domain will only be accessible to people whose ISP supports IPv6
@@ -45,7 +48,7 @@ function getStrategyUnion(proxies: Proxy[]) {
<h5>IPv4 Only</h5><b>Pros</b>: Accessible by anyone <h5>IPv4 Only</h5><b>Pros</b>: Accessible by anyone
<b>Cons</b>: Less private, as IPv4 addresses are closely correlated with geographic areas. Cannot run multiple clearnet servers from the same network <b>Cons</b>: Less private, as IPv4 addresses are closely correlated with geographic areas. Cannot run multiple clearnet servers from the same network
`, `,
required: { default: 'ipv6' }, default: 'ipv6',
values: { values: {
ipv6: 'IPv6 Only', ipv6: 'IPv6 Only',
ipv4: 'IPv4 Only', ipv4: 'IPv4 Only',
@@ -56,10 +59,10 @@ function getStrategyUnion(proxies: Proxy[]) {
}, },
proxy: { proxy: {
name: 'Proxy', name: 'Proxy',
spec: CB.Config.of({ spec: ISB.InputSpec.of({
proxyId: CB.Value.select({ proxyId: ISB.Value.select({
name: 'Select Proxy', name: 'Select Proxy',
required: { default: null }, default: proxies.filter(p => p.type === 'inbound-outbound')[0].id,
values: inboundProxies, values: inboundProxies,
}), }),
}), }),
@@ -70,7 +73,7 @@ function getStrategyUnion(proxies: Proxy[]) {
export function getStart9ToSpec(proxies: Proxy[]) { export function getStart9ToSpec(proxies: Proxy[]) {
return configBuilderToSpec( return configBuilderToSpec(
CB.Config.of({ ISB.InputSpec.of({
strategy: getStrategyUnion(proxies), strategy: getStrategyUnion(proxies),
}), }),
) )
@@ -78,21 +81,22 @@ export function getStart9ToSpec(proxies: Proxy[]) {
export function getCustomSpec(proxies: Proxy[]) { export function getCustomSpec(proxies: Proxy[]) {
return configBuilderToSpec( return configBuilderToSpec(
CB.Config.of({ ISB.InputSpec.of({
hostname: CB.Value.text({ hostname: ISB.Value.text({
name: 'Hostname', name: 'Hostname',
required: { default: null }, required: true,
default: null,
placeholder: 'yourdomain.com', placeholder: 'yourdomain.com',
}), }),
provider: CB.Value.union( provider: ISB.Value.union(
{ {
name: 'Dynamic DNS Provider', name: 'Dynamic DNS Provider',
required: { default: 'start9' }, default: 'start9',
}, },
CB.Variants.of({ ISB.Variants.of({
start9: { start9: {
name: 'Start9', name: 'Start9',
spec: CB.Config.of({}), spec: ISB.InputSpec.of({}),
}, },
njalla: { njalla: {
name: 'Njalla', name: 'Njalla',

View File

@@ -6,7 +6,7 @@ import {
UntypedFormGroup, UntypedFormGroup,
} from '@angular/forms' } from '@angular/forms'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { config, CT } from '@start9labs/start-sdk' import { IST, inputSpec } from '@start9labs/start-sdk'
import { TuiButton, TuiDialogService } from '@taiga-ui/core' import { TuiButton, TuiDialogService } from '@taiga-ui/core'
import { TuiInputModule } from '@taiga-ui/legacy' import { TuiInputModule } from '@taiga-ui/legacy'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
@@ -80,8 +80,8 @@ export class SettingsEmailComponent {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
testAddress = '' testAddress = ''
readonly spec: Promise<CT.InputSpec> = configBuilderToSpec( readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
config.constants.customSmtp, inputSpec.constants.customSmtp,
) )
readonly form$ = this.patch readonly form$ = this.patch
.watch$('serverInfo', 'smtp') .watch$('serverInfo', 'smtp')
@@ -96,7 +96,7 @@ export class SettingsEmailComponent {
try { try {
await this.api.configureEmail( await this.api.configureEmail(
config.constants.customSmtp.validator.unsafeCast(value), inputSpec.constants.customSmtp.validator.unsafeCast(value),
) )
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)

View File

@@ -1,4 +1,4 @@
import { CB } from '@start9labs/start-sdk' import { ISB } from '@start9labs/start-sdk'
import { TuiDialogOptions } from '@taiga-ui/core' import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiConfirmData } from '@taiga-ui/kit' import { TuiConfirmData } from '@taiga-ui/kit'
@@ -12,17 +12,19 @@ export const DELETE_OPTIONS: Partial<TuiDialogOptions<TuiConfirmData>> = {
}, },
} }
export const wireguardSpec = CB.Config.of({ export const wireguardSpec = ISB.InputSpec.of({
name: CB.Value.text({ name: ISB.Value.text({
name: 'Name', name: 'Name',
description: 'A friendly name to help you remember and identify this proxy', description: 'A friendly name to help you remember and identify this proxy',
required: { default: null }, required: true,
}), default: null,
config: CB.Value.file({
name: 'Wiregaurd Config',
required: { default: null },
extensions: ['.conf'],
}), }),
// @TODO Matt same here
// config: ISB.Value.file({
// name: 'Wiregaurd Config',
// required: { default: null },
// extensions: ['.conf'],
// }),
}) })
export type WireguardSpec = typeof wireguardSpec.validator._TYPE export type WireguardSpec = typeof wireguardSpec.validator._TYPE

View File

@@ -3,11 +3,12 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiDialogOptions, TuiButton } from '@taiga-ui/core' import { TuiDialogOptions, TuiButton } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { Observable } from 'rxjs'
import { import {
FormComponent, FormComponent,
FormContext, FormContext,
} from 'src/app/routes/portal/components/form.component' } from 'src/app/routes/portal/components/form.component'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel, Proxy } from 'src/app/services/patch-db/data-model'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ProxiesTableComponent } from './table.component' import { ProxiesTableComponent } from './table.component'
@@ -40,11 +41,9 @@ export class SettingsProxiesComponent {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly formDialog = inject(FormDialogService) private readonly formDialog = inject(FormDialogService)
readonly proxies$ = inject<PatchDB<DataModel>>(PatchDB).watch$( readonly proxies$: Observable<Proxy[]> = inject<PatchDB<DataModel>>(
'serverInfo', PatchDB,
'network', ).watch$('serverInfo', 'network', 'proxies')
'proxies',
)
async add() { async add() {
const options: Partial<TuiDialogOptions<FormContext<WireguardSpec>>> = { const options: Partial<TuiDialogOptions<FormContext<WireguardSpec>>> = {
@@ -63,7 +62,8 @@ export class SettingsProxiesComponent {
this.formDialog.open(FormComponent, options) this.formDialog.open(FormComponent, options)
} }
private async save({ name, config }: WireguardSpec): Promise<boolean> { // @TODO fix type to be WireguardSpec
private async save({ name, config }: any): Promise<boolean> {
const loader = this.loader.open('Saving...').subscribe() const loader = this.loader.open('Saving...').subscribe()
try { try {

View File

@@ -6,7 +6,7 @@ import {
Input, Input,
} from '@angular/core' } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { CB } from '@start9labs/start-sdk' import { ISB } from '@start9labs/start-sdk'
import { import {
TuiButton, TuiButton,
TuiDialogOptions, TuiDialogOptions,
@@ -172,8 +172,8 @@ export class ProxiesTableComponent {
} }
async rename(proxy: Proxy) { async rename(proxy: Proxy) {
const spec = { name: 'Name', required: { default: proxy.name } } const spec = { name: 'Name', required: true, default: proxy.name }
const name = await CB.Value.text(spec).build({} as any) const name = await ISB.Value.text(spec).build({} as any)
const options: Partial<TuiDialogOptions<FormContext<{ name: string }>>> = { const options: Partial<TuiDialogOptions<FormContext<{ name: string }>>> = {
label: `Rename ${proxy.name}`, label: `Rename ${proxy.name}`,
data: { data: {

View File

@@ -5,7 +5,7 @@ import { TuiNotification } from '@taiga-ui/core'
@Component({ @Component({
selector: 'router-info', selector: 'router-info',
template: ` template: `
<tui-notification [status]="enabled ? 'success' : 'warning'"> <tui-notification [appearance]="enabled ? 'positive' : 'warning'">
<ng-container *ngIf="enabled; else disabled"> <ng-container *ngIf="enabled; else disabled">
<strong>UPnP Enabled!</strong> <strong>UPnP Enabled!</strong>
<p> <p>

View File

@@ -1,4 +1,4 @@
import { CT } from '@start9labs/start-sdk' import { IST } from '@start9labs/start-sdk'
import { AvailableWifi } from 'src/app/services/api/api.types' import { AvailableWifi } from 'src/app/services/api/api.types'
import { RR } from 'src/app/services/api/api.types' import { RR } from 'src/app/services/api/api.types'
@@ -28,7 +28,7 @@ export function parseWifi(res: RR.GetWifiRes): WifiData {
} }
} }
export const wifiSpec: CT.ValueSpecObject = { export const wifiSpec: IST.ValueSpecObject = {
type: 'object', type: 'object',
name: 'WiFi Credentials', name: 'WiFi Credentials',
description: description:

View File

@@ -175,7 +175,7 @@ export class SettingsWifiComponent {
this.alerts this.alerts
.open('Check credentials and try again', { .open('Check credentials and try again', {
label: 'Failed to connect', label: 'Failed to connect',
status: 'warning', appearance: 'warning',
}) })
.subscribe() .subscribe()
break break
@@ -188,7 +188,7 @@ export class SettingsWifiComponent {
if (newWifi.connected === ssid) { if (newWifi.connected === ssid) {
this.update$.next(parseWifi(newWifi)) this.update$.next(parseWifi(newWifi))
this.alerts this.alerts
.open('Connection successful!', { status: 'success' }) .open('Connection successful!', { appearance: 'positive' })
.subscribe() .subscribe()
break break
} else { } else {

View File

@@ -1,6 +1,6 @@
import { CT } from '@start9labs/start-sdk' import { IST } from '@start9labs/start-sdk'
export const wifiSpec: CT.ValueSpecObject = { export const wifiSpec: IST.ValueSpecObject = {
type: 'object', type: 'object',
name: 'WiFi Credentials', name: 'WiFi Credentials',
description: description:

View File

@@ -1,4 +1,4 @@
import { CB } from '@start9labs/start-sdk' import { ISB } from '@start9labs/start-sdk'
export interface SettingBtn { export interface SettingBtn {
title: string title: string
@@ -8,26 +8,23 @@ export interface SettingBtn {
routerLink?: string routerLink?: string
} }
export const passwordSpec = CB.Config.of({ export const passwordSpec = ISB.InputSpec.of({
currentPassword: CB.Value.text({ currentPassword: ISB.Value.text({
name: 'Current Password', name: 'Current Password',
required: { required: true,
default: null, default: null,
},
masked: true, masked: true,
}), }),
newPassword1: CB.Value.text({ newPassword1: ISB.Value.text({
name: 'New Password', name: 'New Password',
required: { required: true,
default: null, default: null,
},
masked: true, masked: true,
}), }),
newPassword2: CB.Value.text({ newPassword2: ISB.Value.text({
name: 'Retype New Password', name: 'Retype New Password',
required: { required: true,
default: null, default: null,
},
masked: true, masked: true,
}), }),
}) })

View File

@@ -142,7 +142,7 @@ export class SideloadPackageComponent {
await this.router.navigate(['/portal/service', this.package.id]) await this.router.navigate(['/portal/service', this.package.id])
this.alerts this.alerts
.open('Package uploaded successfully', { status: 'success' }) .open('Package uploaded successfully', { appearance: 'positive' })
.subscribe() .subscribe()
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)

View File

@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core' import { inject, Injectable } from '@angular/core'
import { AlertController } from '@ionic/angular'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core' import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { TUI_CONFIRM } from '@taiga-ui/kit'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { filter } from 'rxjs'
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
@@ -29,14 +30,11 @@ const allowedStatuses = {
providedIn: 'root', providedIn: 'root',
}) })
export class ActionService { export class ActionService {
constructor( private readonly api = inject(ApiService)
private readonly api: ApiService, private readonly dialogs = inject(TuiDialogService)
private readonly dialogs: TuiDialogService, private readonly errorService = inject(ErrorService)
private readonly alertCtrl: AlertController, private readonly loader = inject(LoadingService)
private readonly errorService: ErrorService, private readonly formDialog = inject(FormDialogService)
private readonly loader: LoadingService,
private readonly formDialog: FormDialogService,
) {}
async present(data: PackageActionData) { async present(data: PackageActionData) {
const { pkgInfo, actionInfo } = data const { pkgInfo, actionInfo } = data
@@ -53,25 +51,18 @@ export class ActionService {
}) })
} else { } else {
if (actionInfo.metadata.warning) { if (actionInfo.metadata.warning) {
const alert = await this.alertCtrl.create({ this.dialogs
header: 'Warning', .open(TUI_CONFIRM, {
message: actionInfo.metadata.warning, label: 'Warning',
buttons: [ size: 's',
{ data: {
text: 'Cancel', no: 'Cancel',
role: 'cancel', yes: 'Run',
content: actionInfo.metadata.warning,
}, },
{ })
text: 'Run', .pipe(filter(Boolean))
handler: () => { .subscribe(() => this.execute(pkgInfo.id, actionInfo.id))
this.execute(pkgInfo.id, actionInfo.id)
},
cssClass: 'enter-click',
},
],
cssClass: 'alert-warning-message',
})
await alert.present()
} else { } else {
this.execute(pkgInfo.id, actionInfo.id) this.execute(pkgInfo.id, actionInfo.id)
} }
@@ -92,15 +83,18 @@ export class ActionService {
} else { } else {
error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.` error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
} }
const alert = await this.alertCtrl.create({
header: 'Forbidden', this.dialogs
message: .open(
error || error ||
`Action "${actionInfo.metadata.name}" can only be executed when service is ${statusesStr}`, `Action "${actionInfo.metadata.name}" can only be executed when service is ${statusesStr}`,
buttons: ['OK'], {
cssClass: 'alert-error-message enter-click', label: 'Forbidden',
}) size: 's',
await alert.present() },
)
.pipe(filter(Boolean))
.subscribe()
} }
} }

View File

@@ -5,10 +5,11 @@ import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit' import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { defaultIfEmpty, filter, firstValueFrom } from 'rxjs' import { defaultIfEmpty, filter, firstValueFrom } from 'rxjs'
import { // @TODO Alex implement config
ConfigModal, // import {
PackageConfigData, // ConfigModal,
} from 'src/app/routes/portal/modals/config.component' // PackageConfigData,
// } from 'src/app/routes/portal/modals/config.component'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -27,10 +28,10 @@ export class ActionsService {
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
configure(manifest: T.Manifest): void { configure(manifest: T.Manifest): void {
this.formDialog.open<PackageConfigData>(ConfigModal, { // this.formDialog.open<PackageConfigData>(ConfigModal, {
label: `${manifest.title} configuration`, // label: `${manifest.title} configuration`,
data: { pkgId: manifest.id }, // data: { pkgId: manifest.id },
}) // })
} }
async start(manifest: T.Manifest, unmet: boolean): Promise<void> { async start(manifest: T.Manifest, unmet: boolean): Promise<void> {

View File

@@ -3,7 +3,7 @@ import {
PackageDataEntry, PackageDataEntry,
ServerStatusInfo, ServerStatusInfo,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { RR, ServerNotifications } from './api.types' import { RR, ServerMetrics, ServerNotifications } from './api.types'
import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons' import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons'
import { Log } from '@start9labs/shared' import { Log } from '@start9labs/shared'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'

View File

@@ -2,11 +2,10 @@ import {
DomainInfo, DomainInfo,
NetworkStrategy, NetworkStrategy,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared' import { FetchLogsReq, FetchLogsRes, Log } from '@start9labs/shared'
import { config } from '@start9labs/start-sdk'
import { Dump } from 'patch-db-client' import { Dump } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' import { StartOSDiskInfo } from '@start9labs/shared'
import { IST, T } from '@start9labs/start-sdk' import { IST, T } from '@start9labs/start-sdk'
import { WebSocketSubjectConfig } from 'rxjs/webSocket' import { WebSocketSubjectConfig } from 'rxjs/webSocket'
@@ -232,8 +231,7 @@ export module RR {
// email // email
export type ConfigureEmailReq = export type ConfigureEmailReq = T.SmtpValue // email.configure
typeof config.constants.customSmtp.validator._TYPE // email.configure
export type ConfigureEmailRes = null export type ConfigureEmailRes = null
export type TestEmailReq = ConfigureEmailReq & { to: string } // email.test export type TestEmailReq = ConfigureEmailReq & { to: string } // email.test
@@ -328,9 +326,18 @@ export module RR {
export type CreateBackupRes = null export type CreateBackupRes = null
// package // package
// @TODO Matt I just copy-pasted those types from minor
export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs export type GetPackageLogsReq = {
export type GetPackageLogsRes = LogsRes id: string
before: boolean
cursor?: string
limit?: number
} // package.logs
export type GetPackageLogsRes = {
entries: Log[]
startCursor?: string
endCursor?: string
}
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
export type FollowPackageLogsRes = FollowServerLogsRes export type FollowPackageLogsRes = FollowServerLogsRes

View File

@@ -249,7 +249,7 @@ function listValidators(spec: IST.ValueSpecList): ValidatorFn[] {
return validators return validators
} }
function fileValidators(spec: CT.ValueSpecFile): ValidatorFn[] { function fileValidators(spec: IST.ValueSpecFile): ValidatorFn[] {
const validators: ValidatorFn[] = [] const validators: ValidatorFn[] = []
if (spec.required) { if (spec.required) {

View File

@@ -1,6 +1,5 @@
import { BackupJob, ServerNotifications } from '../api/api.types' import { BackupJob, ServerNotifications } from '../api/api.types'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { config } from '@start9labs/start-sdk'
export type DataModel = { export type DataModel = {
ui: UIData ui: UIData
@@ -51,7 +50,7 @@ export type ServerInfo = {
pubkey: string pubkey: string
caFingerprint: string caFingerprint: string
ntpSynced: boolean ntpSynced: boolean
smtp: typeof config.constants.customSmtp.validator._TYPE smtp: T.SmtpValue | null
passwordHash: string passwordHash: string
platform: string platform: string
arch: string arch: string

View File

@@ -11,7 +11,7 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { ApiService } from './api/embassy-api.service' import { ApiService } from './api/embassy-api.service'
import { DataModel } from './patch-db/data-model' import { DataModel } from './patch-db/data-model'
import { CB } from '@start9labs/start-sdk' import { ISB } from '@start9labs/start-sdk'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -29,18 +29,19 @@ export class ProxyService {
const network = await firstValueFrom( const network = await firstValueFrom(
this.patch.watch$('serverInfo', 'network'), this.patch.watch$('serverInfo', 'network'),
) )
const config = CB.Config.of({ const config = ISB.InputSpec.of({
proxyId: CB.Value.select({ proxyId: ISB.Value.select({
name: 'Select Proxy', name: 'Select Proxy',
required: { default: current }, default: current || '',
values: network.proxies values: network.proxies
.filter(p => p.type === 'outbound' || p.type === 'inbound-outbound') .filter(p => p.type === 'outbound' || p.type === 'inbound-outbound')
.reduce((prev, curr) => { .reduce<Record<string, string>>(
return { (prev, curr) => ({
[curr.id]: curr.name, [curr.id]: curr.name,
...prev, ...prev,
} }),
}, {}), {},
),
}), }),
}) })

View File

@@ -95,7 +95,7 @@ export class StateService extends Observable<RR.ServerState | null> {
.open('Trying to reach server', { .open('Trying to reach server', {
label: 'State unknown', label: 'State unknown',
autoClose: 0, autoClose: 0,
status: 'error', appearance: 'negative',
}) })
.pipe( .pipe(
takeUntil( takeUntil(
@@ -106,7 +106,7 @@ export class StateService extends Observable<RR.ServerState | null> {
), ),
this.alerts.open('Connection restored', { this.alerts.open('Connection restored', {
label: 'Server reached', label: 'Server reached',
status: 'success', appearance: 'positive',
}), }),
), ),
), ),