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
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
$(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> {
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:setup": "tsc --project projects/setup-wizard/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:win": "rimraf .angular/cache && (cd ../sdk && npm ci && npm run build) && (cd ../patch-db/client && npm ci && npm run build)",
"build:deps": "rimraf .angular/cache && (cd ../sdk && make bundle) && (cd ../patch-db/client && npm ci && npm run build)",
"build:install": "ng run install-wizard:build",
"build:setup": "ng run setup-wizard:build",
"build:ui": "ng run ui:build",
@@ -44,24 +43,22 @@
"@angular/router": "^17.3.1",
"@angular/service-worker": "^17.3.1",
"@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/hashes": "^1.4.0",
"@start9labs/argon2": "^0.2.2",
"@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.0.0-rc.7",
"@taiga-ui/addon-commerce": "4.0.0-rc.7",
"@taiga-ui/addon-mobile": "4.0.0-rc.7",
"@taiga-ui/cdk": "4.0.0-rc.7",
"@taiga-ui/core": "4.0.0-rc.7",
"@taiga-ui/event-plugins": "^4.0.1",
"@taiga-ui/icons": "4.0.0-rc.7",
"@taiga-ui/kit": "4.0.0-rc.7",
"@taiga-ui/layout": "4.0.0-rc.7",
"@taiga-ui/legacy": "4.0.0-rc.7",
"@taiga-ui/styles": "4.0.0-rc.7",
"@taiga-ui/addon-charts": "4.16.0",
"@taiga-ui/addon-commerce": "4.16.0",
"@taiga-ui/addon-mobile": "4.16.0",
"@taiga-ui/cdk": "4.16.0",
"@taiga-ui/core": "4.16.0",
"@taiga-ui/event-plugins": "4.3.1",
"@taiga-ui/icons": "4.16.0",
"@taiga-ui/kit": "4.16.0",
"@taiga-ui/layout": "4.16.0",
"@taiga-ui/legacy": "4.16.0",
"@taiga-ui/polymorpheus": "4.7.4",
"@taiga-ui/styles": "4.16.0",
"@tinkoff/ng-dompurify": "4.0.0",
"ansi-to-html": "^0.7.2",
"base64-js": "^1.5.1",
@@ -88,7 +85,7 @@
"rxjs": "^7.5.6",
"swiper": "^8.2.4",
"ts-matches": "^5.5.1",
"tslib": "^2.6.3",
"tslib": "^2.8.1",
"uuid": "^8.3.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 }}
</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 unitsToSeconds: Record<string, number> = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,7 +62,8 @@ export class StatusComponent {
return (
!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
)
}
@@ -86,8 +87,9 @@ export class StatusComponent {
return 'Running'
case 'stopped':
return 'Stopped'
case 'needsConfig':
return 'Needs Config'
// @TODO Matt just dropping this?
// case 'needsConfig':
// return 'Needs Config'
case 'updating':
return 'Updating...'
case 'stopping':
@@ -111,8 +113,9 @@ export class StatusComponent {
switch (this.getStatus(this.pkg).primary) {
case 'running':
return 'var(--tui-status-positive)'
case 'needsConfig':
return 'var(--tui-status-warning)'
// @TODO Matt just dropping this?
// case 'needsConfig':
// return 'var(--tui-status-warning)'
case 'installing':
case 'updating':
case 'stopping':

View File

@@ -69,7 +69,7 @@ export class UILaunchComponent {
}
get isRunning(): boolean {
return this.pkg.status.main.status === 'running'
return this.pkg.status.main === 'running'
}
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 {
return !this.service.pkg.status.configured
// @TODO Matt should we just drop this?
// return !this.service.pkg.status.configured
return false
}
@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)
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) {
this.errorService.handleError(e)
} finally {

View File

@@ -5,10 +5,11 @@ import { T } from '@start9labs/start-sdk'
import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { from } from 'rxjs'
import {
ConfigModal,
PackageConfigData,
} from 'src/app/routes/portal/modals/config.component'
// @TODO Alex implement config
// import {
// ConfigModal,
// PackageConfigData,
// } from 'src/app/routes/portal/modals/config.component'
import { ServiceAdditionalModal } from 'src/app/routes/portal/routes/service/modals/additional.component'
import { ApiService } from 'src/app/services/api/embassy-api.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) {
this.formDialog.open<PackageConfigData>(ConfigModal, {
label: `${title} configuration`,
data: { pkgId: id },
})
// this.formDialog.open<PackageConfigData>(ConfigModal, {
// label: `${title} configuration`,
// data: { pkgId: id },
// })
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { Exver } from '@start9labs/shared'
import { map, Observable } from 'rxjs'
import { PackageBackupInfo } from 'src/app/services/api/api.types'
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 { Version } from '@start9labs/start-sdk'
export interface AppRecoverOption extends PackageBackupInfo {
id: string
checked: boolean
installed: boolean
newerOS: boolean
}
@Pipe({
name: 'toOptions',
standalone: true,
})
export class ToOptionsPipe implements PipeTransform {
private readonly config = inject(ConfigService)
private readonly exver = inject(Exver)
transform(
packageData$: Observable<Record<string, PackageDataEntry>>,
@@ -34,7 +25,7 @@ export class ToOptionsPipe implements PipeTransform {
id,
installed: !!packageData[id],
checked: false,
newerOS:
newerOs:
Version.parse(packageBackups[id].osVersion).compare(
Version.parse(this.config.version),
) === '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({
name: CB.Value.text({
export const dropboxSpec = ISB.InputSpec.of({
name: ISB.Value.text({
name: 'Name',
description: 'A friendly name for this Dropbox target',
placeholder: 'My Dropbox',
required: { default: null },
required: true,
default: null,
}),
token: CB.Value.text({
token: ISB.Value.text({
name: 'Access Token',
description: 'The secret access token for your custom Dropbox app',
required: { default: null },
required: true,
default: null,
masked: true,
}),
path: CB.Value.text({
path: ISB.Value.text({
name: 'Path',
description: 'The fully qualified path to the backup directory',
placeholder: 'e.g. /Desktop/my-folder',
required: { default: null },
required: true,
default: null,
}),
})
export const googleDriveSpec = CB.Config.of({
name: CB.Value.text({
export const googleDriveSpec = ISB.InputSpec.of({
name: ISB.Value.text({
name: 'Name',
description: 'A friendly name for this Google Drive target',
placeholder: 'My Google Drive',
required: { default: null },
required: true,
default: null,
}),
path: CB.Value.text({
path: ISB.Value.text({
name: 'Path',
description: 'The fully qualified path to the backup directory',
placeholder: 'e.g. /Desktop/my-folder',
required: { 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'],
required: true,
default: null,
}),
// @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({
name: CB.Value.text({
export const cifsSpec = ISB.InputSpec.of({
name: ISB.Value.text({
name: 'Name',
description: 'A friendly name for this Network Folder',
placeholder: 'My Network Folder',
required: { default: null },
required: true,
default: null,
}),
hostname: CB.Value.text({
hostname: ISB.Value.text({
name: 'Hostname',
description:
'The hostname of your target device on the Local Area Network.',
warning: null,
placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
required: { default: null },
required: true,
default: null,
patterns: [],
}),
path: CB.Value.text({
path: ISB.Value.text({
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).`,
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',
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',
}),
password: CB.Value.text({
password: ISB.Value.text({
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.`,
required: false,
masked: true,
default: null,
placeholder: 'My Network Folder',
}),
})
export const remoteBackupTargetSpec = CB.Config.of({
type: CB.Value.union(
export const remoteBackupTargetSpec = ISB.InputSpec.of({
type: ISB.Value.union(
{
name: 'Target Type',
required: { default: 'dropbox' },
default: 'dropbox',
},
CB.Variants.of({
ISB.Variants.of({
dropbox: {
name: 'Dropbox',
spec: dropboxSpec,
@@ -103,17 +114,19 @@ export const remoteBackupTargetSpec = CB.Config.of({
),
})
export const diskBackupTargetSpec = CB.Config.of({
name: CB.Value.text({
export const diskBackupTargetSpec = ISB.InputSpec.of({
name: ISB.Value.text({
name: 'Name',
description: 'A friendly name for this physical target',
placeholder: 'My Physical Target',
required: { default: null },
required: true,
default: null,
}),
path: CB.Value.text({
path: ISB.Value.text({
name: 'Path',
description: 'The fully qualified path to the backup directory',
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',
template: `
<tui-notification
[status]="status || 'warning'"
[appearance]="status || 'warning'"
icon=""
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 { TuiConfirmData } from '@taiga-ui/kit'
export function getMarketplaceValueSpec(): CT.ValueSpecObject {
export function getMarketplaceValueSpec(): IST.ValueSpecObject {
return {
type: 'object',
name: 'Add Custom Registry',

View File

@@ -5,7 +5,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'settings-sync',
template: `
<tui-notification status="warning">
<tui-notification appearance="warning">
<div tuiCell [style.padding]="0">
<div tuiTitle>
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 { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
const auth = CB.Config.of({
username: CB.Value.text({
const auth = ISB.InputSpec.of({
username: ISB.Value.text({
name: 'Username',
required: { default: null },
required: true,
default: null,
}),
password: CB.Value.text({
password: ISB.Value.text({
name: 'Password',
required: { default: null },
required: true,
default: null,
masked: true,
}),
})
function getStrategyUnion(proxies: Proxy[]) {
const inboundProxies = proxies
const inboundProxies: Record<string, string> = proxies
.filter(p => p.type === 'inbound-outbound')
.reduce((prev, curr) => {
return {
.reduce(
(prev, curr) => ({
[curr.id]: curr.name,
...prev,
}
}, {})
}),
{},
)
return CB.Value.union(
return ISB.Value.union(
{
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
<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: {
name: 'Local',
spec: CB.Config.of({
ipStrategy: CB.Value.select({
spec: ISB.InputSpec.of({
ipStrategy: ISB.Value.select({
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
<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
<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: {
ipv6: 'IPv6 Only',
ipv4: 'IPv4 Only',
@@ -56,10 +59,10 @@ function getStrategyUnion(proxies: Proxy[]) {
},
proxy: {
name: 'Proxy',
spec: CB.Config.of({
proxyId: CB.Value.select({
spec: ISB.InputSpec.of({
proxyId: ISB.Value.select({
name: 'Select Proxy',
required: { default: null },
default: proxies.filter(p => p.type === 'inbound-outbound')[0].id,
values: inboundProxies,
}),
}),
@@ -70,7 +73,7 @@ function getStrategyUnion(proxies: Proxy[]) {
export function getStart9ToSpec(proxies: Proxy[]) {
return configBuilderToSpec(
CB.Config.of({
ISB.InputSpec.of({
strategy: getStrategyUnion(proxies),
}),
)
@@ -78,21 +81,22 @@ export function getStart9ToSpec(proxies: Proxy[]) {
export function getCustomSpec(proxies: Proxy[]) {
return configBuilderToSpec(
CB.Config.of({
hostname: CB.Value.text({
ISB.InputSpec.of({
hostname: ISB.Value.text({
name: 'Hostname',
required: { default: null },
required: true,
default: null,
placeholder: 'yourdomain.com',
}),
provider: CB.Value.union(
provider: ISB.Value.union(
{
name: 'Dynamic DNS Provider',
required: { default: 'start9' },
default: 'start9',
},
CB.Variants.of({
ISB.Variants.of({
start9: {
name: 'Start9',
spec: CB.Config.of({}),
spec: ISB.InputSpec.of({}),
},
njalla: {
name: 'Njalla',

View File

@@ -6,7 +6,7 @@ import {
UntypedFormGroup,
} from '@angular/forms'
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 { TuiInputModule } from '@taiga-ui/legacy'
import { PatchDB } from 'patch-db-client'
@@ -80,8 +80,8 @@ export class SettingsEmailComponent {
private readonly api = inject(ApiService)
testAddress = ''
readonly spec: Promise<CT.InputSpec> = configBuilderToSpec(
config.constants.customSmtp,
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
inputSpec.constants.customSmtp,
)
readonly form$ = this.patch
.watch$('serverInfo', 'smtp')
@@ -96,7 +96,7 @@ export class SettingsEmailComponent {
try {
await this.api.configureEmail(
config.constants.customSmtp.validator.unsafeCast(value),
inputSpec.constants.customSmtp.validator.unsafeCast(value),
)
} catch (e: any) {
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 { TuiConfirmData } from '@taiga-ui/kit'
@@ -12,17 +12,19 @@ export const DELETE_OPTIONS: Partial<TuiDialogOptions<TuiConfirmData>> = {
},
}
export const wireguardSpec = CB.Config.of({
name: CB.Value.text({
export const wireguardSpec = ISB.InputSpec.of({
name: ISB.Value.text({
name: 'Name',
description: 'A friendly name to help you remember and identify this proxy',
required: { default: null },
}),
config: CB.Value.file({
name: 'Wiregaurd Config',
required: { default: null },
extensions: ['.conf'],
required: true,
default: null,
}),
// @TODO Matt same here
// config: ISB.Value.file({
// name: 'Wiregaurd Config',
// required: { default: null },
// extensions: ['.conf'],
// }),
})
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 { TuiDialogOptions, TuiButton } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client'
import { Observable } from 'rxjs'
import {
FormComponent,
FormContext,
} 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 { ApiService } from 'src/app/services/api/embassy-api.service'
import { ProxiesTableComponent } from './table.component'
@@ -40,11 +41,9 @@ export class SettingsProxiesComponent {
private readonly api = inject(ApiService)
private readonly formDialog = inject(FormDialogService)
readonly proxies$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'serverInfo',
'network',
'proxies',
)
readonly proxies$: Observable<Proxy[]> = inject<PatchDB<DataModel>>(
PatchDB,
).watch$('serverInfo', 'network', 'proxies')
async add() {
const options: Partial<TuiDialogOptions<FormContext<WireguardSpec>>> = {
@@ -63,7 +62,8 @@ export class SettingsProxiesComponent {
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()
try {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core'
import { AlertController } from '@ionic/angular'
import { inject, Injectable } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
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 { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
@@ -29,14 +30,11 @@ const allowedStatuses = {
providedIn: 'root',
})
export class ActionService {
constructor(
private readonly api: ApiService,
private readonly dialogs: TuiDialogService,
private readonly alertCtrl: AlertController,
private readonly errorService: ErrorService,
private readonly loader: LoadingService,
private readonly formDialog: FormDialogService,
) {}
private readonly api = inject(ApiService)
private readonly dialogs = inject(TuiDialogService)
private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService)
private readonly formDialog = inject(FormDialogService)
async present(data: PackageActionData) {
const { pkgInfo, actionInfo } = data
@@ -53,25 +51,18 @@ export class ActionService {
})
} else {
if (actionInfo.metadata.warning) {
const alert = await this.alertCtrl.create({
header: 'Warning',
message: actionInfo.metadata.warning,
buttons: [
{
text: 'Cancel',
role: 'cancel',
this.dialogs
.open(TUI_CONFIRM, {
label: 'Warning',
size: 's',
data: {
no: 'Cancel',
yes: 'Run',
content: actionInfo.metadata.warning,
},
{
text: 'Run',
handler: () => {
this.execute(pkgInfo.id, actionInfo.id)
},
cssClass: 'enter-click',
},
],
cssClass: 'alert-warning-message',
})
await alert.present()
})
.pipe(filter(Boolean))
.subscribe(() => this.execute(pkgInfo.id, actionInfo.id))
} else {
this.execute(pkgInfo.id, actionInfo.id)
}
@@ -92,15 +83,18 @@ export class ActionService {
} 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.`
}
const alert = await this.alertCtrl.create({
header: 'Forbidden',
message:
this.dialogs
.open(
error ||
`Action "${actionInfo.metadata.name}" can only be executed when service is ${statusesStr}`,
buttons: ['OK'],
cssClass: 'alert-error-message enter-click',
})
await alert.present()
`Action "${actionInfo.metadata.name}" can only be executed when service is ${statusesStr}`,
{
label: 'Forbidden',
size: 's',
},
)
.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 { PatchDB } from 'patch-db-client'
import { defaultIfEmpty, filter, firstValueFrom } from 'rxjs'
import {
ConfigModal,
PackageConfigData,
} from 'src/app/routes/portal/modals/config.component'
// @TODO Alex implement config
// import {
// ConfigModal,
// PackageConfigData,
// } from 'src/app/routes/portal/modals/config.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -27,10 +28,10 @@ export class ActionsService {
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
configure(manifest: T.Manifest): void {
this.formDialog.open<PackageConfigData>(ConfigModal, {
label: `${manifest.title} configuration`,
data: { pkgId: manifest.id },
})
// this.formDialog.open<PackageConfigData>(ConfigModal, {
// label: `${manifest.title} configuration`,
// data: { pkgId: manifest.id },
// })
}
async start(manifest: T.Manifest, unmet: boolean): Promise<void> {

View File

@@ -3,7 +3,7 @@ import {
PackageDataEntry,
ServerStatusInfo,
} 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 { Log } from '@start9labs/shared'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'

View File

@@ -2,11 +2,10 @@ import {
DomainInfo,
NetworkStrategy,
} from 'src/app/services/patch-db/data-model'
import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
import { config } from '@start9labs/start-sdk'
import { FetchLogsReq, FetchLogsRes, Log } from '@start9labs/shared'
import { Dump } from 'patch-db-client'
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 { WebSocketSubjectConfig } from 'rxjs/webSocket'
@@ -232,8 +231,7 @@ export module RR {
// email
export type ConfigureEmailReq =
typeof config.constants.customSmtp.validator._TYPE // email.configure
export type ConfigureEmailReq = T.SmtpValue // email.configure
export type ConfigureEmailRes = null
export type TestEmailReq = ConfigureEmailReq & { to: string } // email.test
@@ -328,9 +326,18 @@ export module RR {
export type CreateBackupRes = null
// package
export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs
export type GetPackageLogsRes = LogsRes
// @TODO Matt I just copy-pasted those types from minor
export type GetPackageLogsReq = {
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 FollowPackageLogsRes = FollowServerLogsRes

View File

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

View File

@@ -1,6 +1,5 @@
import { BackupJob, ServerNotifications } from '../api/api.types'
import { T } from '@start9labs/start-sdk'
import { config } from '@start9labs/start-sdk'
export type DataModel = {
ui: UIData
@@ -51,7 +50,7 @@ export type ServerInfo = {
pubkey: string
caFingerprint: string
ntpSynced: boolean
smtp: typeof config.constants.customSmtp.validator._TYPE
smtp: T.SmtpValue | null
passwordHash: string
platform: 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 { ApiService } from './api/embassy-api.service'
import { DataModel } from './patch-db/data-model'
import { CB } from '@start9labs/start-sdk'
import { ISB } from '@start9labs/start-sdk'
@Injectable({
providedIn: 'root',
@@ -29,18 +29,19 @@ export class ProxyService {
const network = await firstValueFrom(
this.patch.watch$('serverInfo', 'network'),
)
const config = CB.Config.of({
proxyId: CB.Value.select({
const config = ISB.InputSpec.of({
proxyId: ISB.Value.select({
name: 'Select Proxy',
required: { default: current },
default: current || '',
values: network.proxies
.filter(p => p.type === 'outbound' || p.type === 'inbound-outbound')
.reduce((prev, curr) => {
return {
.reduce<Record<string, string>>(
(prev, curr) => ({
[curr.id]: curr.name,
...prev,
}
}, {}),
}),
{},
),
}),
})

View File

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