mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
2
Makefile
2
Makefile
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
3337
sdk/base/package-lock.json
generated
3337
sdk/base/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
3609
sdk/package/package-lock.json
generated
3609
sdk/package/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
7864
web/package-lock.json
generated
7864
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ErrorService extends ErrorHandler {
|
||||
this.alerts
|
||||
.open(getErrorMessage(error, link), {
|
||||
label: 'Error',
|
||||
status: 'error',
|
||||
appearance: 'negative',
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
@@ -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()"
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -52,7 +52,7 @@ export class FormControlComponent<
|
||||
this.alerts
|
||||
.open<boolean>(this.warning, {
|
||||
label: 'Warning',
|
||||
status: 'warning',
|
||||
appearance: 'warning',
|
||||
closeable: false,
|
||||
autoClose: 0,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
> {}
|
||||
|
||||
@@ -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: '',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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' }
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}, {}),
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user