mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Fix/fe bugs 3 (#2943)
* fix typeo in patch db seed * show all registries in updates tab, fix required dependnecy display in marketplace, update browser tab title desc * always show pointer for version select * chore: fix comments * support html in action desc and marketplace long desc, only show qr in action res if qr is true * disable save if smtp creds not edited, show better smtp success message * dont dismiss login spinner until patchDB returns * feat: redesign of service dashboard and interface (#2946) * feat: redesign of service dashboard and interface * chore: comments * re-add setup complete * dibale launch UI when not running, re-style things, rename things * back to 1000 * fix clearnet docs link and require password retype in setup wiz * faster hint display * display dependency ID if title not available * fix migration * better init progress view * fix setup success page by providing VERSION and notifications page fixes * force uninstall from service error page, soft or hard * handle error state better * chore: fixed for install and setup wizards * chore: fix issues (#2949) * enable and disable kiosk mode * minor fixes * fix dependency mounts * dismissable tasks * provide replayId * default if health check success message is null * look for wifi interface too * dash for null user agent in sessions * add disk repair to diagnostic api --------- Co-authored-by: waterplea <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"https://registry.start9.com/": "Start9 Registry",
|
||||
"https://community-registry.start9.com/": "Community Registry"
|
||||
},
|
||||
"startosRegisrty": "https://registry.start9.com/",
|
||||
"startosRegistry": "https://registry.start9.com/",
|
||||
"snakeHighScore": 0,
|
||||
"ackInstructions": {}
|
||||
}
|
||||
|
||||
@@ -61,3 +61,8 @@ main {
|
||||
margin-left: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
[tuiCell]:not(:last-of-type) {
|
||||
box-shadow: 0 calc(0.5rem + 1px) 0 -0.5rem var(--tui-border-normal);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,12 @@ export class AppComponent {
|
||||
this.dialogs
|
||||
.open(
|
||||
'Please wait for StartOS to restart, then refresh this page',
|
||||
{ label: 'Rebooting', size: 's' },
|
||||
{
|
||||
label: 'Rebooting',
|
||||
size: 's',
|
||||
closeable: false,
|
||||
dismissible: false,
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
Past Release Notes
|
||||
</button>
|
||||
<h2 class="additional-detail-title" [style.margin-top.rem]="2">About</h2>
|
||||
<p>{{ pkg.description.long }}</p>
|
||||
<p [innerHTML]="pkg.description.long"></p>
|
||||
<a
|
||||
*ngIf="pkg.marketingSite as url"
|
||||
tuiButton
|
||||
|
||||
@@ -20,8 +20,8 @@ import { MarketplacePkgBase } from '../../../types'
|
||||
</span>
|
||||
<p>
|
||||
<ng-container [ngSwitch]="dep.value.optional">
|
||||
<span *ngSwitchCase="true">(required)</span>
|
||||
<span *ngSwitchCase="false">(optional)</span>
|
||||
<span *ngSwitchCase="true">(optional)</span>
|
||||
<span *ngSwitchCase="false">(required)</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Component, inject } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api.service'
|
||||
import { StateService } from './services/state.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -11,9 +13,15 @@ export class AppComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly stateService = inject(StateService)
|
||||
private readonly document = inject(DOCUMENT)
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
this.stateService.kiosk = ['localhost', '127.0.0.1'].includes(
|
||||
this.document.location.hostname,
|
||||
)
|
||||
|
||||
const inProgress = await this.api.getStatus()
|
||||
|
||||
let route = 'home'
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PreloadAllModules, RouterModule } from '@angular/router'
|
||||
import {
|
||||
provideSetupLogsService,
|
||||
RELATIVE_URL,
|
||||
VERSION,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
import { tuiButtonOptionsProvider, TuiRoot } from '@taiga-ui/core'
|
||||
@@ -20,6 +21,8 @@ const {
|
||||
ui: { api },
|
||||
} = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
const version = require('../../../../package.json').version
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
@@ -43,6 +46,10 @@ const {
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
{
|
||||
provide: VERSION,
|
||||
useValue: version,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ const FADE_FACTOR = 0.07
|
||||
standalone: true,
|
||||
selector: 'canvas[matrix]',
|
||||
template: 'Your browser does not support the canvas element.',
|
||||
styles: ':host { position: fixed; }',
|
||||
styles: ':host { position: fixed; top: 0 }',
|
||||
})
|
||||
export class MatrixComponent implements OnInit {
|
||||
private readonly ngZone = inject(NgZone)
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
AbstractControl,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext, TuiError } from '@taiga-ui/core'
|
||||
import { TuiInputPasswordModule } from '@taiga-ui/legacy'
|
||||
import { TuiAutoFocus, TuiMapperPipe, TuiValidator } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDialogContext,
|
||||
TuiError,
|
||||
TuiIcon,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import {
|
||||
TuiFieldErrorPipe,
|
||||
TuiPassword,
|
||||
tuiValidationErrorsProvider,
|
||||
} from '@taiga-ui/kit'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
|
||||
interface DialogData {
|
||||
@@ -21,18 +39,38 @@ interface DialogData {
|
||||
Enter the password that was used to encrypt this drive.
|
||||
}
|
||||
|
||||
<form [style.margin-top.rem]="1" (ngSubmit)="submit()">
|
||||
<tui-input-password [formControl]="password">
|
||||
Enter Password
|
||||
<input tuiTextfieldLegacy maxlength="64" />
|
||||
</tui-input-password>
|
||||
<tui-error [error]="passwordError"></tui-error>
|
||||
<form [formGroup]="form" [style.margin-top.rem]="1" (ngSubmit)="submit()">
|
||||
<tui-textfield>
|
||||
<label tuiLabel>Enter Password</label>
|
||||
<input
|
||||
tuiTextfield
|
||||
type="password"
|
||||
tuiAutoFocus
|
||||
maxlength="64"
|
||||
formControlName="password"
|
||||
/>
|
||||
<tui-icon tuiPassword />
|
||||
</tui-textfield>
|
||||
<tui-error
|
||||
formControlName="password"
|
||||
[error]="[] | tuiFieldError | async"
|
||||
/>
|
||||
@if (storageDrive) {
|
||||
<tui-input-password [style.margin-top.rem]="1" [formControl]="confirm">
|
||||
Retype Password
|
||||
<input tuiTextfieldLegacy maxlength="64" />
|
||||
</tui-input-password>
|
||||
<tui-error [error]="confirmError"></tui-error>
|
||||
<tui-textfield [style.margin-top.rem]="1">
|
||||
<label tuiLabel>Retype Password</label>
|
||||
<input
|
||||
tuiTextfield
|
||||
type="password"
|
||||
maxlength="64"
|
||||
formControlName="confirm"
|
||||
[tuiValidator]="form.controls.password.value | tuiMapper: validator"
|
||||
/>
|
||||
<tui-icon tuiPassword />
|
||||
</tui-textfield>
|
||||
<tui-error
|
||||
formControlName="confirm"
|
||||
[error]="[] | tuiFieldError | async"
|
||||
/>
|
||||
}
|
||||
<footer>
|
||||
<button
|
||||
@@ -43,22 +81,38 @@ interface DialogData {
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!password.value || !!confirmError || !!passwordError"
|
||||
>
|
||||
<button tuiButton [disabled]="form.invalid">
|
||||
{{ storageDrive ? 'Finish' : 'Unlock' }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
`,
|
||||
styles: ['footer { display: flex; gap: 1rem; margin-top: 1rem }'],
|
||||
styles: `
|
||||
footer {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
AsyncPipe,
|
||||
ReactiveFormsModule,
|
||||
TuiButton,
|
||||
TuiInputPasswordModule,
|
||||
TuiError,
|
||||
TuiAutoFocus,
|
||||
TuiFieldErrorPipe,
|
||||
TuiTextfield,
|
||||
TuiPassword,
|
||||
TuiValidator,
|
||||
TuiIcon,
|
||||
TuiMapperPipe,
|
||||
],
|
||||
providers: [
|
||||
tuiValidationErrorsProvider({
|
||||
required: 'Required',
|
||||
minlength: 'Must be 12 characters or greater',
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class PasswordComponent {
|
||||
@@ -67,31 +121,29 @@ export class PasswordComponent {
|
||||
injectContext<TuiDialogContext<string, DialogData>>()
|
||||
|
||||
readonly storageDrive = this.context.data.storageDrive
|
||||
readonly password = new FormControl('', { nonNullable: true })
|
||||
readonly confirm = new FormControl('', { nonNullable: true })
|
||||
readonly form = new FormGroup({
|
||||
password: new FormControl('', [
|
||||
Validators.required,
|
||||
Validators.minLength(12),
|
||||
]),
|
||||
confirm: new FormControl('', this.storageDrive ? Validators.required : []),
|
||||
})
|
||||
|
||||
get passwordError(): string | null {
|
||||
return this.password.touched && this.password.value.length < 12
|
||||
? 'Must be 12 characters or greater'
|
||||
: null
|
||||
}
|
||||
|
||||
get confirmError(): string | null {
|
||||
return this.confirm.touched && this.password.value !== this.confirm.value
|
||||
? 'Passwords do not match'
|
||||
: null
|
||||
}
|
||||
readonly validator = (value: any) => (control: AbstractControl) =>
|
||||
value === control.value ? null : { match: 'Passwords do not match' }
|
||||
|
||||
submit() {
|
||||
const password = this.form.controls.password.value || ''
|
||||
|
||||
if (this.storageDrive) {
|
||||
this.context.completeWith(this.password.value)
|
||||
this.context.completeWith(password)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
argon2.verify(this.context.data.passwordHash || '', this.password.value)
|
||||
this.context.completeWith(this.password.value)
|
||||
argon2.verify(this.context.data.passwordHash || '', password)
|
||||
this.context.completeWith(password)
|
||||
} catch (e) {
|
||||
this.errorService.handleError('Incorrect password provided')
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<section tuiCardLarge>
|
||||
<section tuiCardLarge="compact">
|
||||
<header>Use existing drive</header>
|
||||
<div>Select the physical drive containing your StartOS data</div>
|
||||
|
||||
@@ -31,9 +31,11 @@ import { StateService } from 'src/app/services/state.service'
|
||||
valid StartOS data drive (not a backup) and is firmly connected, then
|
||||
refresh the page.
|
||||
}
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
<footer>
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
</footer>
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
template: `
|
||||
<img class="logo" src="assets/img/icon.png" alt="Start9" />
|
||||
@if (!loading) {
|
||||
<section tuiCardLarge>
|
||||
<section tuiCardLarge="compact">
|
||||
<header [style.padding-top.rem]="1.25">
|
||||
@if (recover) {
|
||||
<button
|
||||
@@ -30,7 +30,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
</header>
|
||||
<div class="pages">
|
||||
<div class="options" [class.options_recover]="recover">
|
||||
<a tuiCell [routerLink]="error || recover ? null : '/storage'">
|
||||
<button tuiCell [routerLink]="error || recover ? null : '/storage'">
|
||||
<tui-icon icon="@tui.plus" />
|
||||
<span tuiTitle>
|
||||
<span class="g-positive">Start Fresh</span>
|
||||
@@ -38,7 +38,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
Get started with a brand new Start9 server
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
<button
|
||||
tuiCell
|
||||
[disabled]="error || recover"
|
||||
|
||||
@@ -17,7 +17,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<section tuiCardLarge>
|
||||
<section tuiCardLarge="compact">
|
||||
<header>Restore from Backup</header>
|
||||
@if (loading) {
|
||||
<tui-loader />
|
||||
@@ -26,7 +26,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
Restore StartOS data from a folder on another computer that is connected
|
||||
to the same network as your server.
|
||||
|
||||
<button tuiCell (click)="onCifs()">
|
||||
<button tuiCell [style.box-shadow]="'none'" (click)="onCifs()">
|
||||
<tui-icon icon="@tui.folder" />
|
||||
<span tuiTitle>Open</span>
|
||||
</button>
|
||||
@@ -49,10 +49,11 @@ import { StateService } from 'src/app/services/state.service'
|
||||
(password)="select($event, server)"
|
||||
></button>
|
||||
}
|
||||
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
<footer>
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
</footer>
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
|
||||
@@ -19,7 +19,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<section tuiCardLarge>
|
||||
<section tuiCardLarge="compact">
|
||||
@if (loading || drives.length) {
|
||||
<header>Select storage drive</header>
|
||||
This is the drive where your StartOS data will be stored.
|
||||
@@ -39,10 +39,11 @@ import { StateService } from 'src/app/services/state.service'
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
<footer>
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
`,
|
||||
imports: [TuiCardLarge, TuiLoader, TuiCell, TuiButton, DriveComponent],
|
||||
|
||||
@@ -18,15 +18,13 @@ import { StateService } from 'src/app/services/state.service'
|
||||
standalone: true,
|
||||
template: `
|
||||
<canvas matrix></canvas>
|
||||
@if (isKiosk) {
|
||||
@if (stateService.kiosk) {
|
||||
<section tuiCardLarge>
|
||||
<h1 class="heading">
|
||||
<tui-icon icon="@tui.check-square" class="g-positive" />
|
||||
Setup Complete!
|
||||
</h1>
|
||||
<button tuiButton (click)="exitKiosk()" iconEnd="@tui.log-in">
|
||||
Continue to Login
|
||||
</button>
|
||||
<button tuiButton (click)="exitKiosk()">Continue to Login</button>
|
||||
</section>
|
||||
} @else if (lanAddress) {
|
||||
<section tuiCardLarge>
|
||||
@@ -111,16 +109,12 @@ import { StateService } from 'src/app/services/state.service'
|
||||
export default class SuccessPage implements AfterViewInit {
|
||||
@ViewChild(DocumentationComponent, { read: ElementRef })
|
||||
private readonly documentation?: ElementRef<HTMLElement>
|
||||
|
||||
private readonly document = inject(DOCUMENT)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly downloadHtml = inject(DownloadHTMLService)
|
||||
|
||||
readonly stateService = inject(StateService)
|
||||
readonly isKiosk = ['localhost', '127.0.0.1'].includes(
|
||||
this.document.location.hostname,
|
||||
)
|
||||
|
||||
torAddresses?: string[]
|
||||
lanAddress?: string
|
||||
@@ -157,7 +151,7 @@ export default class SuccessPage implements AfterViewInit {
|
||||
private async complete() {
|
||||
try {
|
||||
const ret = await this.api.complete()
|
||||
if (!this.isKiosk) {
|
||||
if (!this.stateService.kiosk) {
|
||||
this.torAddresses = ret.torAddresses.map(a =>
|
||||
a.replace(/^https:/, 'http:'),
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<section tuiCardLarge>
|
||||
<section tuiCardLarge="compact">
|
||||
<header>Transfer</header>
|
||||
Select the physical drive containing your StartOS data
|
||||
@if (loading) {
|
||||
@@ -30,9 +30,11 @@ import { StateService } from 'src/app/services/state.service'
|
||||
@for (drive of drives; track drive) {
|
||||
<button tuiCell [drive]="drive" (click)="select(drive)"></button>
|
||||
}
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
<footer>
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
`,
|
||||
imports: [TuiCardLarge, TuiCell, TuiButton, TuiLoader, DriveComponent],
|
||||
|
||||
@@ -8,6 +8,7 @@ import { T } from '@start9labs/start-sdk'
|
||||
export class StateService {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
kiosk?: boolean
|
||||
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
|
||||
recoverySource?: T.RecoverySource<string>
|
||||
|
||||
@@ -15,6 +16,7 @@ export class StateService {
|
||||
await this.api.attach({
|
||||
guid,
|
||||
startOsPassword: await this.api.encrypt(password),
|
||||
kiosk: this.kiosk,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +35,7 @@ export class StateService {
|
||||
password: await this.api.encrypt(this.recoverySource.password),
|
||||
}
|
||||
: null,
|
||||
kiosk: this.kiosk,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ router-outlet + * {
|
||||
|
||||
[tuiCardLarge] {
|
||||
width: 100%;
|
||||
background: var(--tui-background-base-alt);
|
||||
background: var(--tui-background-elevation-2);
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
@@ -67,3 +67,11 @@ h2 {
|
||||
.g-info {
|
||||
color: var(--tui-status-info);
|
||||
}
|
||||
|
||||
[tuiCardLarge] footer button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[tuiCell]:not(:last-of-type) {
|
||||
box-shadow: 0 calc(0.5rem + 1px) 0 -0.5rem var(--tui-border-normal);
|
||||
}
|
||||
|
||||
BIN
web/projects/shared/assets/img/background_marketplace.jpg
Normal file
BIN
web/projects/shared/assets/img/background_marketplace.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 694 KiB |
@@ -12,7 +12,7 @@ import { i18nPipe } from '../i18n/i18n.pipe'
|
||||
<h1 [style.font-size.rem]="2" [style.margin-bottom.rem]="2">
|
||||
{{ 'Setting up your server' | i18n }}
|
||||
</h1>
|
||||
<div *ngIf="progress.total">
|
||||
<div>
|
||||
{{ 'Progress' | i18n }}: {{ (progress.total * 100).toFixed(0) }}%
|
||||
</div>
|
||||
<progress
|
||||
@@ -21,7 +21,7 @@ import { i18nPipe } from '../i18n/i18n.pipe'
|
||||
[style.margin]="'1rem auto'"
|
||||
[attr.value]="progress.total"
|
||||
></progress>
|
||||
<p [innerHTML]="progress.message"></p>
|
||||
<p [innerHTML]="progress.message || 'Finished'"></p>
|
||||
</section>
|
||||
<logs-window />
|
||||
`,
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
&:hover {
|
||||
text-indent: var(--indent, 0);
|
||||
text-overflow: clip;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
input,
|
||||
} from '@angular/core'
|
||||
|
||||
const HOST = 'https://staging.docs.start9.com'
|
||||
export const VERSION = new InjectionToken<string>('VERSION')
|
||||
|
||||
@Directive({
|
||||
@@ -26,6 +25,6 @@ export class DocsLinkDirective {
|
||||
protected readonly url = computed(() => {
|
||||
const path = this.href()
|
||||
const relative = path.startsWith('/') ? path : `/${path}`
|
||||
return `${HOST}${relative}?os=${this.version}`
|
||||
return `https://docs.start9.com${relative}?os=${this.version}`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
57: 'Herunterfahren wird eingeleitet',
|
||||
58: 'Hinzufügen',
|
||||
59: 'Ok',
|
||||
60: 'Möchten Sie diesen Eintrag wirklich löschen?',
|
||||
60: 'französisch',
|
||||
61: 'Dieser Wert kann nach dem Festlegen nicht geändert werden',
|
||||
62: 'Fortfahren',
|
||||
63: 'Klicken oder Datei hierher ziehen',
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
82: 'Metriken',
|
||||
83: 'Protokolle',
|
||||
84: 'Benachrichtigungen',
|
||||
85: 'UI starten',
|
||||
85: 'Hartes Deinstallieren',
|
||||
86: 'QR-Code anzeigen',
|
||||
87: 'URL kopieren',
|
||||
88: 'Aktionen',
|
||||
@@ -230,9 +230,9 @@ export default {
|
||||
227: 'Unbekannter Fehler',
|
||||
228: 'Fehler',
|
||||
229: '"Container neu bauen" ist eine harmlose Aktion, die nur wenige Sekunden dauert. Sie wird dieses Problem wahrscheinlich beheben.',
|
||||
230: '"Dienst deinstallieren" ist eine gefährliche Aktion, die den Dienst aus StartOS entfernt und alle zugehörigen Daten dauerhaft löscht.',
|
||||
230: '"Hartes Deinstallieren" ist eine gefährliche Aktion, die den Dienst aus StartOS entfernt und alle zugehörigen Daten dauerhaft löscht.',
|
||||
231: 'Container neu bauen',
|
||||
232: 'Dienst deinstallieren',
|
||||
232: 'Weiches Deinstallieren',
|
||||
233: 'Vollständige Nachricht anzeigen',
|
||||
234: 'Dienstfehler',
|
||||
235: 'Warte auf Ergebnis',
|
||||
@@ -247,7 +247,6 @@ export default {
|
||||
244: 'Hosting',
|
||||
245: 'Installation läuft',
|
||||
246: 'Siehe unten',
|
||||
247: 'Steuerelemente',
|
||||
248: 'Keine Dienste installiert',
|
||||
249: 'Läuft',
|
||||
250: 'Gestoppt',
|
||||
@@ -414,12 +413,12 @@ export default {
|
||||
411: 'Weitere Netzwerke',
|
||||
412: 'WiFi ist deaktiviert',
|
||||
413: 'Keine drahtlose Schnittstelle erkannt',
|
||||
414: 'WiFi wird aktiviert',
|
||||
415: 'WiFi wird deaktiviert',
|
||||
414: 'wird aktiviert',
|
||||
415: 'wird deaktiviert',
|
||||
416: 'Verbindung wird hergestellt. Dies kann einen Moment dauern',
|
||||
417: 'Erneut versuchen',
|
||||
418: 'Mehr anzeigen',
|
||||
419: 'Versionshinweise',
|
||||
419: 'Details anzeigen',
|
||||
420: 'Eintrag anzeigen',
|
||||
421: 'Dienste, die von folgendem abhängen:',
|
||||
422: 'werden nicht mehr ordnungsgemäß funktionieren und könnten abstürzen.',
|
||||
@@ -503,5 +502,20 @@ export default {
|
||||
500: 'Marktplatz anzeigen',
|
||||
501: 'Willkommen bei',
|
||||
502: 'souveränes computing',
|
||||
503: 'französisch',
|
||||
503: 'Passen Sie den Namen an, der in Ihrem Browser-Tab erscheint',
|
||||
504: 'Verwalten',
|
||||
505: 'Möchten Sie diese Adresse wirklich löschen?',
|
||||
506: '"Weiches Deinstallieren" entfernt den Dienst aus StartOS, behält jedoch die Daten bei.',
|
||||
507: 'Keine gespeicherten Anbieter',
|
||||
508: 'Kiosk-Modus',
|
||||
509: 'Aktiviert',
|
||||
510: 'Deaktiviere den Kiosk-Modus, es sei denn, du musst einen Monitor anschließen',
|
||||
511: 'Aktiviere den Kiosk-Modus, wenn du einen Monitor anschließen musst',
|
||||
512: 'Der Kiosk-Modus ist auf diesem Gerät nicht verfügbar',
|
||||
513: 'Aktivieren',
|
||||
514: 'Deaktivieren',
|
||||
515: 'Du verwendest derzeit einen Kiosk. Wenn du den Kiosk-Modus deaktivierst, wird die Verbindung zum Kiosk getrennt.',
|
||||
516: 'Empfohlen',
|
||||
517: 'Möchten Sie diese Aufgabe wirklich verwerfen?',
|
||||
518: 'Verwerfen',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ENGLISH = {
|
||||
'Beginning restart': 42,
|
||||
'You are on the latest version of StartOS.': 43,
|
||||
'Up to date!': 44,
|
||||
'Release Notes': 45,
|
||||
'Release notes': 45,
|
||||
'Begin Update': 46,
|
||||
'Beginning update': 47,
|
||||
'You are currently connected over Tor. If you reset the Tor daemon, you will lose connectivity until it comes back online.': 48,
|
||||
@@ -59,7 +59,7 @@ export const ENGLISH = {
|
||||
'Beginning shutdown': 57,
|
||||
'Add': 58,
|
||||
'Ok': 59,
|
||||
'Are you sure you want to delete this entry?': 60,
|
||||
'french': 60,
|
||||
'This value cannot be changed once set': 61,
|
||||
'Continue': 62,
|
||||
'Click or drop file here': 63,
|
||||
@@ -84,7 +84,7 @@ export const ENGLISH = {
|
||||
'Metrics': 82, // system info such as CPU, RAM, and storage usage
|
||||
'Logs': 83, // as in, application logs
|
||||
'Notifications': 84,
|
||||
'Launch UI': 85,
|
||||
'Hard uninstall': 85, // as in, hard reset or hard reboot, except for uninstalling
|
||||
'Show QR': 86,
|
||||
'Copy URL': 87,
|
||||
'Actions': 88, // as in, actions available to the user
|
||||
@@ -229,9 +229,9 @@ export const ENGLISH = {
|
||||
'Unknown error': 227,
|
||||
'Error': 228,
|
||||
'"Rebuild container" is a harmless action that and only takes a few seconds to complete. It will likely resolve this issue.': 229,
|
||||
'"Uninstall service" is a dangerous action that will remove the service from StartOS and wipe all its data.': 230,
|
||||
'"Hard uninstall" is a dangerous action that will remove the service from StartOS and wipe all its data.': 230,
|
||||
'Rebuild container': 231,
|
||||
'Uninstall service': 232,
|
||||
'Soft uninstall': 232, // as in, uninstall the service but preserve its data
|
||||
'View full message': 233,
|
||||
'Service error': 234,
|
||||
'Awaiting result': 235,
|
||||
@@ -246,7 +246,6 @@ export const ENGLISH = {
|
||||
'Hosting': 244,
|
||||
'Installing': 245,
|
||||
'See below': 246,
|
||||
'Controls': 247,
|
||||
'No services installed': 248,
|
||||
'Running': 249,
|
||||
'Stopped': 250,
|
||||
@@ -413,12 +412,12 @@ export const ENGLISH = {
|
||||
'Other Networks': 411,
|
||||
'WiFi is disabled': 412,
|
||||
'No wireless interface detected': 413,
|
||||
'Enabling WiFi': 414,
|
||||
'Disabling WiFi': 415,
|
||||
'Enabling': 414,
|
||||
'Disabling': 415,
|
||||
'Connecting. This could take a while': 416,
|
||||
'Retry': 417,
|
||||
'Show more': 418,
|
||||
'Release notes': 419,
|
||||
'View details': 419,
|
||||
'View listing': 420,
|
||||
'Services that depend on': 421,
|
||||
'will no longer work properly and may crash.': 422,
|
||||
@@ -502,5 +501,20 @@ export const ENGLISH = {
|
||||
'View Marketplace': 500,
|
||||
'Welcome to': 501,
|
||||
'sovereign computing': 502,
|
||||
'french': 503,
|
||||
'Customize the name appearing in your browser tab': 503,
|
||||
'Manage': 504, // as in, administer
|
||||
'Are you sure you want to delete this address?': 505, // this address referes to a domain or URL
|
||||
'"Soft uninstall" will remove the service from StartOS but preserve its data.': 506,
|
||||
'No saved providers': 507,
|
||||
'Kiosk Mode': 508, // an OS mode that permits attaching a monitor to the computer
|
||||
'Enabled': 509,
|
||||
'Disable Kiosk Mode unless you need to attach a monitor': 510,
|
||||
'Enable Kiosk Mode if you need to attach a monitor': 511,
|
||||
'Kiosk Mode is unavailable on this device': 512,
|
||||
'Enable': 513,
|
||||
'Disable': 514,
|
||||
'You are currently using a kiosk. Disabling Kiosk Mode will result in the kiosk disconnecting.': 515,
|
||||
'Recommended': 516, // as in, we recommend this
|
||||
'Are you sure you want to dismiss this task?': 517,
|
||||
'Dismiss': 518, // as in, dismiss or delete a task
|
||||
} as const
|
||||
|
||||
@@ -45,7 +45,7 @@ export default {
|
||||
42: 'Iniciando reinicio',
|
||||
43: 'Estás usando la última versión de StartOS.',
|
||||
44: '¡Actualizado!',
|
||||
45: 'Notas de la versión',
|
||||
45: 'notas de la versión',
|
||||
46: 'Iniciar actualización',
|
||||
47: 'Iniciando actualización',
|
||||
48: 'Actualmente estás conectado a través de Tor. Si restableces el servicio Tor, perderás la conexión hasta que vuelva a estar en línea.',
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
57: 'Iniciando apagado',
|
||||
58: 'Agregar',
|
||||
59: 'Ok',
|
||||
60: '¿Estás seguro de que deseas eliminar esta entrada?',
|
||||
60: 'francés',
|
||||
61: 'Este valor no se puede cambiar una vez establecido',
|
||||
62: 'Continuar',
|
||||
63: 'Haz clic o suelta el archivo aquí',
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
82: 'Métricas',
|
||||
83: 'Registros',
|
||||
84: 'Notificaciones',
|
||||
85: 'Abrir interfaz',
|
||||
85: 'Desinstalación forzada',
|
||||
86: 'Mostrar QR',
|
||||
87: 'Copiar URL',
|
||||
88: 'Acciones',
|
||||
@@ -230,9 +230,9 @@ export default {
|
||||
227: 'Error desconocido',
|
||||
228: 'Error',
|
||||
229: '"Reconstruir contenedor" es una acción inofensiva que solo toma unos segundos. Probablemente resolverá este problema.',
|
||||
230: '"Desinstalar servicio" es una acción peligrosa que eliminará el servicio de StartOS y borrará todos sus datos.',
|
||||
230: '"Desinstalación forzada" es una acción peligrosa que eliminará el servicio de StartOS y borrará todos sus datos.',
|
||||
231: 'Reconstruir contenedor',
|
||||
232: 'Desinstalar servicio',
|
||||
232: 'Desinstalación suave',
|
||||
233: 'Ver mensaje completo',
|
||||
234: 'Error del servicio',
|
||||
235: 'Esperando resultado',
|
||||
@@ -247,7 +247,6 @@ export default {
|
||||
244: 'Alojamiento',
|
||||
245: 'Instalando',
|
||||
246: 'Ver abajo',
|
||||
247: 'Controles',
|
||||
248: 'No hay servicios instalados',
|
||||
249: 'En ejecución',
|
||||
250: 'Detenido',
|
||||
@@ -414,12 +413,12 @@ export default {
|
||||
411: 'Otras redes',
|
||||
412: 'WiFi está deshabilitado',
|
||||
413: 'No se detectó interfaz inalámbrica',
|
||||
414: 'Habilitando WiFi',
|
||||
415: 'Deshabilitando WiFi',
|
||||
414: 'Habilitando',
|
||||
415: 'Deshabilitando',
|
||||
416: 'Conectando. Esto podría tardar un poco',
|
||||
417: 'Reintentar',
|
||||
418: 'Mostrar más',
|
||||
419: 'Notas de la versión',
|
||||
419: 'Ver detalles',
|
||||
420: 'Ver listado',
|
||||
421: 'Servicios que dependen de',
|
||||
422: 'ya no funcionarán correctamente y podrían fallar.',
|
||||
@@ -503,5 +502,20 @@ export default {
|
||||
500: 'Ver Marketplace',
|
||||
501: 'Bienvenido a',
|
||||
502: 'computación soberana',
|
||||
503: 'francés',
|
||||
503: 'Personaliza el nombre que aparece en la pestaña de tu navegador',
|
||||
504: 'Administrar',
|
||||
505: '¿Estás seguro de que deseas eliminar esta dirección?',
|
||||
506: '"Desinstalación suave" eliminará el servicio de StartOS pero conservará sus datos.',
|
||||
507: 'No hay proveedores guardados',
|
||||
508: 'Modo quiosco',
|
||||
509: 'Activado',
|
||||
510: 'Desactiva el modo quiosco a menos que necesites conectar un monitor',
|
||||
511: 'Activa el modo quiosco si necesitas conectar un monitor',
|
||||
512: 'El modo quiosco no está disponible en este dispositivo',
|
||||
513: 'Activar',
|
||||
514: 'Desactivar',
|
||||
515: 'Actualmente estás utilizando un quiosco. Desactivar el modo quiosco provocará su desconexión.',
|
||||
516: 'Recomendado',
|
||||
517: '¿Estás seguro de que deseas descartar esta tarea?',
|
||||
518: 'Descartar',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
57: 'Arrêt initié',
|
||||
58: 'Ajouter',
|
||||
59: 'OK',
|
||||
60: 'Voulez-vous vraiment supprimer cette entrée ?',
|
||||
60: 'français',
|
||||
61: 'Cette valeur ne peut plus être modifiée une fois définie',
|
||||
62: 'Continuer',
|
||||
63: 'Cliquez ou déposez le fichier ici',
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
82: 'Métriques',
|
||||
83: 'Journaux',
|
||||
84: 'Notifications',
|
||||
85: 'Lancer l’interface utilisateur',
|
||||
85: 'Désinstallation forcée',
|
||||
86: 'Afficher le QR',
|
||||
87: 'Copier l’URL',
|
||||
88: 'Actions',
|
||||
@@ -230,9 +230,9 @@ export default {
|
||||
227: 'Erreur inconnue',
|
||||
228: 'Erreur',
|
||||
229: '« Reconstruire le conteneur » est une action sans risque qui ne prend que quelques secondes. Cela résoudra probablement ce problème.',
|
||||
230: '« Désinstaller le service » est une action risquée qui supprimera le service de StartOS et effacera toutes ses données.',
|
||||
230: '« Désinstallation forcée » est une action risquée qui supprimera le service de StartOS et effacera toutes ses données.',
|
||||
231: 'Reconstruire le conteneur',
|
||||
232: 'Désinstaller le service',
|
||||
232: 'Désinstallation douce',
|
||||
233: 'Voir le message complet',
|
||||
234: 'Erreur du service',
|
||||
235: 'En attente du résultat',
|
||||
@@ -247,7 +247,6 @@ export default {
|
||||
244: 'Hébergement',
|
||||
245: 'Installation',
|
||||
246: 'Voir ci-dessous',
|
||||
247: 'Contrôles',
|
||||
248: 'Aucun service installé',
|
||||
249: 'En fonctionnement',
|
||||
250: 'Arrêté',
|
||||
@@ -414,12 +413,12 @@ export default {
|
||||
411: 'Autres réseaux',
|
||||
412: 'Le WiFi est désactivé',
|
||||
413: 'Aucune interface sans fil détectée',
|
||||
414: 'Activation du WiFi',
|
||||
415: 'Désactivation du WiFi',
|
||||
414: 'Activation',
|
||||
415: 'Désactivation',
|
||||
416: 'Connexion en cours. Cela peut prendre un certain temps',
|
||||
417: 'Réessayer',
|
||||
418: 'Afficher plus',
|
||||
419: 'Notes de version',
|
||||
419: 'Voir les détails',
|
||||
420: 'Voir la fiche',
|
||||
421: 'Services dépendants de',
|
||||
422: 'ne fonctionneront plus correctement et pourraient planter.',
|
||||
@@ -503,5 +502,20 @@ export default {
|
||||
500: 'Voir la bibliothèque de services',
|
||||
501: 'Bienvenue sur',
|
||||
502: 'informatique souveraine',
|
||||
503: 'français',
|
||||
503: 'Personnalisez le nom qui apparaît dans l’onglet de votre navigateur',
|
||||
504: 'Gérer',
|
||||
505: 'Êtes-vous sûr de vouloir supprimer cette adresse ?',
|
||||
506: '« Désinstallation douce » supprimera le service de StartOS tout en conservant ses données.',
|
||||
507: 'Aucun fournisseur enregistré',
|
||||
508: 'Mode kiosque',
|
||||
509: 'Activé',
|
||||
510: 'Désactivez le mode kiosque sauf si vous devez connecter un moniteur',
|
||||
511: 'Activez le mode kiosque si vous devez connecter un moniteur',
|
||||
512: 'Le mode kiosque n’est pas disponible sur cet appareil',
|
||||
513: 'Activer',
|
||||
514: 'Désactiver',
|
||||
515: 'Vous utilisez actuellement un kiosque. Désactiver le mode kiosque entraînera sa déconnexion.',
|
||||
516: 'Recommandé',
|
||||
517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?',
|
||||
518: 'Ignorer',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
57: 'Rozpoczynanie wyłączania',
|
||||
58: 'Dodaj',
|
||||
59: 'OK',
|
||||
60: 'Czy na pewno chcesz usunąć ten wpis?',
|
||||
60: 'francuski',
|
||||
61: 'Ta wartość nie może być zmieniona po jej ustawieniu',
|
||||
62: 'Kontynuuj',
|
||||
63: 'Kliknij lub upuść plik tutaj',
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
82: 'Monitorowanie',
|
||||
83: 'Logi',
|
||||
84: 'Powiadomienia',
|
||||
85: 'Uruchom interfejs',
|
||||
85: 'Twarde odinstalowanie',
|
||||
86: 'Pokaż kod QR',
|
||||
87: 'Kopiuj URL',
|
||||
88: 'Akcje',
|
||||
@@ -230,9 +230,9 @@ export default {
|
||||
227: 'Nieznany błąd',
|
||||
228: 'Błąd',
|
||||
229: '„Odbuduj kontener” to bezpieczna akcja, która zajmuje tylko kilka sekund. Prawdopodobnie rozwiąże ten problem.',
|
||||
230: '„Odinstaluj serwis” to niebezpieczna akcja, która usunie serwis ze StartOS i trwale usunie wszystkie jego dane.',
|
||||
230: '„Twarde odinstalowanie” to niebezpieczna akcja, która usunie serwis ze StartOS i trwale usunie wszystkie jego dane.',
|
||||
231: 'Odbuduj kontener',
|
||||
232: 'Odinstaluj serwis',
|
||||
232: 'Miękkie odinstalowanie',
|
||||
233: 'Zobacz pełną wiadomość',
|
||||
234: 'Błąd serwisu',
|
||||
235: 'Oczekiwanie na wynik',
|
||||
@@ -247,7 +247,6 @@ export default {
|
||||
244: 'Hosting',
|
||||
245: 'Instalowanie',
|
||||
246: 'Zobacz poniżej',
|
||||
247: 'Sterowanie',
|
||||
248: 'Brak zainstalowanych serwisów',
|
||||
249: 'Uruchomiony',
|
||||
250: 'Zatrzymany',
|
||||
@@ -414,12 +413,12 @@ export default {
|
||||
411: 'Inne sieci',
|
||||
412: 'WiFi jest wyłączone',
|
||||
413: 'Nie wykryto interfejsu bezprzewodowego',
|
||||
414: 'Włączanie WiFi',
|
||||
415: 'Wyłączanie WiFi',
|
||||
414: 'Włączanie',
|
||||
415: 'Wyłączanie',
|
||||
416: 'Łączenie. To może chwilę potrwać',
|
||||
417: 'Ponów próbę',
|
||||
418: 'Pokaż więcej',
|
||||
419: 'Informacje o wydaniu',
|
||||
419: 'Zobacz szczegóły',
|
||||
420: 'Zobacz listę',
|
||||
421: 'Serwisy zależne od',
|
||||
422: 'przestaną działać poprawnie i mogą ulec awarii.',
|
||||
@@ -503,5 +502,20 @@ export default {
|
||||
500: 'Zobacz Rynek',
|
||||
501: 'Witamy w',
|
||||
502: 'suwerenne przetwarzanie',
|
||||
503: 'francuski',
|
||||
503: 'Dostosuj nazwę wyświetlaną na karcie przeglądarki',
|
||||
504: 'Zarządzać',
|
||||
505: 'Czy na pewno chcesz usunąć ten adres?',
|
||||
506: '„Miękkie odinstalowanie” usunie usługę z StartOS, ale zachowa jej dane.',
|
||||
507: 'Brak zapisanych dostawców',
|
||||
508: 'Tryb kiosku',
|
||||
509: 'Włączony',
|
||||
510: 'Wyłącz tryb kiosku, chyba że potrzebujesz podłączyć monitor',
|
||||
511: 'Włącz tryb kiosku, jeśli potrzebujesz podłączyć monitor',
|
||||
512: 'Tryb kiosku jest niedostępny na tym urządzeniu',
|
||||
513: 'Włącz',
|
||||
514: 'Wyłącz',
|
||||
515: 'Obecnie używasz kiosku. Wyłączenie trybu kiosku spowoduje jego rozłączenie.',
|
||||
516: 'Zalecane',
|
||||
517: 'Czy na pewno chcesz odrzucić to zadanie?',
|
||||
518: 'Odrzuć',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -11,8 +11,6 @@ import { I18N, i18nKey } from './i18n.providers'
|
||||
export class i18nPipe implements PipeTransform {
|
||||
private readonly i18n = inject(I18N)
|
||||
|
||||
// @TODO uncomment to make sure translations are present
|
||||
// transform(englishKey: string | null | undefined): string | undefined {
|
||||
transform(englishKey: i18nKey | null | undefined): string | undefined {
|
||||
return englishKey
|
||||
? this.i18n()?.[ENGLISH[englishKey as i18nKey]] || englishKey
|
||||
|
||||
@@ -34,5 +34,11 @@ export class i18nService extends TuiLanguageSwitcherService {
|
||||
}
|
||||
}
|
||||
|
||||
export const languages = ['english', 'spanish', 'polish', 'german', 'french'] as const
|
||||
export const languages = [
|
||||
'english',
|
||||
'spanish',
|
||||
'polish',
|
||||
'german',
|
||||
'french',
|
||||
] as const
|
||||
export type Languages = (typeof languages)[number]
|
||||
|
||||
@@ -12,13 +12,15 @@ export function formatProgress({ phases, overall }: T.FullProgress): {
|
||||
p,
|
||||
): p is {
|
||||
name: string
|
||||
progress: {
|
||||
done: number
|
||||
total: number | null
|
||||
}
|
||||
progress:
|
||||
| false
|
||||
| {
|
||||
done: number
|
||||
total: number | null
|
||||
}
|
||||
} => p.progress !== true && p.progress !== null,
|
||||
)
|
||||
.map(p => `<b>${p.name}</b>${getPhaseBytes(p.progress)}`)
|
||||
.map(p => `<b>${p.name}</b>: (${getPhaseBytes(p.progress)})`)
|
||||
.join(', '),
|
||||
}
|
||||
}
|
||||
@@ -33,8 +35,13 @@ function getDecimal(progress: T.Progress): number {
|
||||
}
|
||||
}
|
||||
|
||||
function getPhaseBytes(progress: T.Progress): string {
|
||||
return progress === true || !progress
|
||||
? ''
|
||||
: `: (${progress.done}/${progress.total})`
|
||||
function getPhaseBytes(
|
||||
progress:
|
||||
| false
|
||||
| {
|
||||
done: number
|
||||
total: number | null
|
||||
},
|
||||
): string {
|
||||
return !progress ? 'unknown' : `${progress.done}/${progress.total}`
|
||||
}
|
||||
|
||||
@@ -277,6 +277,7 @@ body {
|
||||
vertical-align: bottom;
|
||||
animation: ellipsis-dot 1s infinite 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
text-align: left;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
[tuiAppearance][data-appearance='primary-success'] {
|
||||
color: var(--tui-text-primary-on-accent-1);
|
||||
background: var(--tui-status-positive);
|
||||
|
||||
@include appearance-hover {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
@include appearance-active {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
@include appearance-disabled {
|
||||
background: var(--tui-status-neutral);
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
tui-hint[data-appearance='onDark'] {
|
||||
background: white !important;
|
||||
color: #222 !important;
|
||||
|
||||
@@ -48,6 +48,6 @@ export default class InitializingPage {
|
||||
return caught$
|
||||
}),
|
||||
),
|
||||
{ initialValue: { total: 0, message: '' } },
|
||||
{ initialValue: { total: 0, message: 'waiting...' } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ export class LoginPage {
|
||||
} catch (e: any) {
|
||||
// code 7 is for incorrect password
|
||||
this.error = e.code === 7 ? 'Invalid password' : (e.message as i18nKey)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,20 +73,7 @@ export class FormArrayComponent {
|
||||
}
|
||||
|
||||
removeAt(index: number) {
|
||||
this.dialog
|
||||
.openConfirm<boolean>({
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
content: 'Are you sure you want to delete this entry?',
|
||||
yes: 'Delete',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
this.removeItem(index)
|
||||
})
|
||||
this.removeItem(index)
|
||||
}
|
||||
|
||||
private removeItem(index: number) {
|
||||
|
||||
@@ -29,7 +29,7 @@ import { ABOUT } from './about.component'
|
||||
appearance=""
|
||||
tuiHintDirection="bottom"
|
||||
[tuiHint]="open ? '' : ('Start Menu' | i18n)"
|
||||
[tuiHintShowDelay]="1000"
|
||||
[tuiHintShowDelay]="750"
|
||||
[tuiDropdown]="content"
|
||||
[(tuiDropdownOpen)]="open"
|
||||
[tuiDropdownMaxHeight]="9999"
|
||||
|
||||
@@ -23,7 +23,7 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
||||
class="link"
|
||||
routerLinkActive="link_active"
|
||||
tuiHintDirection="bottom"
|
||||
[tuiHintShowDelay]="1000"
|
||||
[tuiHintShowDelay]="750"
|
||||
[routerLink]="item.routerLink"
|
||||
[class.link_system]="item.routerLink === '/portal/system'"
|
||||
[tuiHint]="rla.isActive ? '' : (item.name | i18n)"
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -22,24 +23,30 @@ import { InterfaceComponent } from './interface.component'
|
||||
template: `
|
||||
<div class="desktop">
|
||||
<ng-content />
|
||||
@if (interface.serviceInterface().type === 'ui') {
|
||||
<a
|
||||
@if (interface.value().type === 'ui') {
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[href]="actions()"
|
||||
[disabled]="disabled()"
|
||||
(click)="openUI()"
|
||||
>
|
||||
{{ 'Launch UI' | i18n }}
|
||||
</a>
|
||||
{{ 'Open' | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button tuiIconButton iconStart="@tui.qr-code" (click)="showQR()">
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.qr-code"
|
||||
(click)="showQR()"
|
||||
>
|
||||
{{ 'Show QR' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(actions())"
|
||||
(click)="copyService.copy(href())"
|
||||
>
|
||||
{{ 'Copy URL' | i18n }}
|
||||
</button>
|
||||
@@ -55,27 +62,26 @@ import { InterfaceComponent } from './interface.component'
|
||||
<ng-template #dropdown let-close>
|
||||
<tui-data-list>
|
||||
<tui-opt-group>
|
||||
@if (interface.serviceInterface().type === 'ui') {
|
||||
<a
|
||||
tuiOption
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[href]="actions()"
|
||||
>
|
||||
{{ 'Launch UI' | i18n }}
|
||||
</a>
|
||||
<button tuiOption iconStart="@tui.qr-code" (click)="showQR()">
|
||||
{{ 'Show QR' | i18n }}
|
||||
</button>
|
||||
@if (interface.value().type === 'ui') {
|
||||
<button
|
||||
tuiOption
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(actions()); close()"
|
||||
iconStart="@tui.external-link"
|
||||
[disabled]="disabled()"
|
||||
(click)="openUI()"
|
||||
>
|
||||
{{ 'Copy URL' | i18n }}
|
||||
{{ 'Open' | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button tuiOption iconStart="@tui.qr-code" (click)="showQR()">
|
||||
{{ 'Show QR' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(href()); close()"
|
||||
>
|
||||
{{ 'Copy URL' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group><ng-content select="[tuiOption]" /></tui-opt-group>
|
||||
</tui-data-list>
|
||||
@@ -110,20 +116,27 @@ import { InterfaceComponent } from './interface.component'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceActionsComponent {
|
||||
private readonly document = inject(DOCUMENT)
|
||||
|
||||
readonly isMobile = inject(TUI_IS_MOBILE)
|
||||
readonly dialog = inject(DialogService)
|
||||
readonly copyService = inject(CopyService)
|
||||
readonly interface = inject(InterfaceComponent)
|
||||
|
||||
readonly actions = input.required<string>()
|
||||
readonly href = input.required<string>()
|
||||
readonly disabled = input.required<boolean>()
|
||||
|
||||
showQR() {
|
||||
this.dialog
|
||||
.openComponent(new PolymorpheusComponent(QRModal), {
|
||||
size: 'auto',
|
||||
closeable: this.isMobile,
|
||||
data: this.actions(),
|
||||
data: this.href(),
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
openUI() {
|
||||
this.document.defaultView?.open(this.href(), '_blank', 'noreferrer')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
DialogService,
|
||||
DocsLinkDirective,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
@@ -59,22 +59,12 @@ type ClearnetForm = {
|
||||
}}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
href="/user-manual/connecting-remotely/clearnet.html"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ 'Learn more' | i18n }}
|
||||
</a>
|
||||
</ng-template>
|
||||
<button
|
||||
tuiButton
|
||||
[appearance]="isPublic() ? 'primary-destructive' : 'accent'"
|
||||
[iconStart]="isPublic() ? '@tui.globe-lock' : '@tui.globe'"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="toggle()"
|
||||
>
|
||||
{{ isPublic() ? ('Make private' | i18n) : ('Make public' | i18n) }}
|
||||
</button>
|
||||
@if (clearnet().length) {
|
||||
<button tuiButton iconStart="@tui.plus" (click)="add()">
|
||||
{{ 'Add' | i18n }}
|
||||
@@ -86,18 +76,15 @@ type ClearnetForm = {
|
||||
@for (address of clearnet(); track $index) {
|
||||
<tr>
|
||||
<td [style.width.rem]="12">
|
||||
{{
|
||||
interface.serviceInterface().addSsl
|
||||
? (address.acme | acme)
|
||||
: '-'
|
||||
}}
|
||||
{{ interface.value().addSsl ? (address.acme | acme) : '-' }}
|
||||
</td>
|
||||
<td>{{ address.url | mask }}</td>
|
||||
<td [actions]="address.url">
|
||||
<td actions [href]="address.url" [disabled]="!isRunning()">
|
||||
@if (address.isDomain) {
|
||||
<button
|
||||
tuiButton
|
||||
appearance="primary-destructive"
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="flat-grayscale"
|
||||
[style.margin-inline-end.rem]="0.5"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
@@ -141,6 +128,7 @@ type ClearnetForm = {
|
||||
AcmePipe,
|
||||
InterfaceActionsComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
@@ -152,9 +140,10 @@ export class InterfaceClearnetComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly interface = inject(InterfaceComponent)
|
||||
readonly isPublic = computed(() => this.interface.serviceInterface().public)
|
||||
|
||||
readonly clearnet = input.required<readonly ClearnetAddress[]>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
|
||||
readonly acme = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('serverInfo', 'network', 'acme')
|
||||
@@ -165,7 +154,15 @@ export class InterfaceClearnetComponent {
|
||||
async remove({ url }: ClearnetAddress) {
|
||||
const confirm = await firstValueFrom(
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.openConfirm({
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
yes: 'Delete',
|
||||
no: 'Cancel',
|
||||
content: 'Are you sure you want to delete this address?',
|
||||
},
|
||||
})
|
||||
.pipe(defaultIfEmpty(false)),
|
||||
)
|
||||
|
||||
@@ -181,7 +178,7 @@ export class InterfaceClearnetComponent {
|
||||
await this.api.pkgRemoveDomain({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
host: this.interface.value().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverRemoveDomain(params)
|
||||
@@ -195,33 +192,6 @@ export class InterfaceClearnetComponent {
|
||||
}
|
||||
}
|
||||
|
||||
async toggle() {
|
||||
const loader = this.loader
|
||||
.open(`Making ${this.isPublic() ? 'private' : 'public'}`)
|
||||
.subscribe()
|
||||
|
||||
const params = {
|
||||
internalPort: this.interface.serviceInterface().addressInfo.internalPort,
|
||||
public: !this.isPublic(),
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgBindingSetPubic({
|
||||
...params,
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
package: this.interface.packageId(),
|
||||
})
|
||||
} else {
|
||||
await this.api.serverBindingSetPubic(params)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async add() {
|
||||
const domain = ISB.Value.text({
|
||||
name: 'Domain',
|
||||
@@ -250,9 +220,7 @@ export class InterfaceClearnetComponent {
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of(
|
||||
this.interface.serviceInterface().addSsl
|
||||
? { domain, acme }
|
||||
: { domain },
|
||||
this.interface.value().addSsl ? { domain, acme } : { domain },
|
||||
),
|
||||
),
|
||||
buttons: [
|
||||
@@ -281,7 +249,7 @@ export class InterfaceClearnetComponent {
|
||||
await this.api.pkgAddDomain({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
host: this.interface.value().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddDomain(params)
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { TuiButton, tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { InterfaceClearnetComponent } from 'src/app/routes/portal/components/interfaces/clearnet.component'
|
||||
import { InterfaceLocalComponent } from 'src/app/routes/portal/components/interfaces/local.component'
|
||||
import { InterfaceTorComponent } from 'src/app/routes/portal/components/interfaces/tor.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MappedServiceInterface } from './interface.utils'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-interface',
|
||||
template: `
|
||||
<section [clearnet]="serviceInterface().addresses.clearnet"></section>
|
||||
<section [tor]="serviceInterface().addresses.tor"></section>
|
||||
<section [local]="serviceInterface().addresses.local"></section>
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
[appearance]="value().public ? 'primary-destructive' : 'primary-success'"
|
||||
[iconStart]="value().public ? '@tui.globe-lock' : '@tui.globe'"
|
||||
(click)="toggle()"
|
||||
>
|
||||
{{ value().public ? ('Make private' | i18n) : ('Make public' | i18n) }}
|
||||
</button>
|
||||
<section
|
||||
[clearnet]="value().addresses.clearnet"
|
||||
[isRunning]="isRunning()"
|
||||
></section>
|
||||
<section [tor]="value().addresses.tor" [isRunning]="isRunning()"></section>
|
||||
<section
|
||||
[local]="value().addresses.local"
|
||||
[isRunning]="isRunning()"
|
||||
></section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
@@ -19,6 +41,12 @@ import { MappedServiceInterface } from './interface.utils'
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
color: var(--tui-text-secondary);
|
||||
font: var(--tui-font-text-l);
|
||||
}
|
||||
|
||||
button {
|
||||
margin: -0.5rem auto 0 0;
|
||||
}
|
||||
`,
|
||||
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
|
||||
@@ -27,9 +55,43 @@ import { MappedServiceInterface } from './interface.utils'
|
||||
InterfaceClearnetComponent,
|
||||
InterfaceTorComponent,
|
||||
InterfaceLocalComponent,
|
||||
TuiButton,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export class InterfaceComponent {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly packageId = input('')
|
||||
readonly serviceInterface = input.required<MappedServiceInterface>()
|
||||
readonly value = input.required<MappedServiceInterface>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
|
||||
async toggle() {
|
||||
const loader = this.loader
|
||||
.open(`Making ${this.value().public ? 'private' : 'public'}`)
|
||||
.subscribe()
|
||||
|
||||
const params = {
|
||||
internalPort: this.value().addressInfo.internalPort,
|
||||
public: !this.value().public,
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.packageId()) {
|
||||
await this.api.pkgBindingSetPubic({
|
||||
...params,
|
||||
host: this.value().addressInfo.hostId,
|
||||
package: this.packageId(),
|
||||
})
|
||||
} else {
|
||||
await this.api.serverBindingSetPubic(params)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
<tr>
|
||||
<td [style.width.rem]="12">{{ address.nid }}</td>
|
||||
<td>{{ address.url | mask }}</td>
|
||||
<td [actions]="address.url"></td>
|
||||
<td actions [href]="address.url" [disabled]="!isRunning()"></td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
@@ -49,4 +49,5 @@ import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
})
|
||||
export class InterfaceLocalComponent {
|
||||
readonly local = input.required<readonly LocalAddress[]>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export class MaskPipe implements PipeTransform {
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
transform(value: string): string {
|
||||
return this.interface.serviceInterface().masked
|
||||
return this.interface.value().masked
|
||||
? '●'.repeat(Math.min(64, value.length))
|
||||
: value
|
||||
}
|
||||
|
||||
@@ -9,13 +9,16 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
<tui-badge
|
||||
size="l"
|
||||
[iconStart]="public() ? '@tui.globe' : '@tui.lock'"
|
||||
[style.vertical-align.rem]="-0.125"
|
||||
[style.margin]="'0 0.25rem -0.25rem'"
|
||||
[appearance]="public() ? 'positive' : 'negative'"
|
||||
>
|
||||
{{ public() ? ('Public' | i18n) : ('Private' | i18n) }}
|
||||
</tui-badge>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiBadge, i18nPipe],
|
||||
})
|
||||
|
||||
@@ -76,11 +76,11 @@ type OnionForm = {
|
||||
{{ address.url | mask }}
|
||||
</div>
|
||||
</td>
|
||||
<td [actions]="address.url">
|
||||
<td actions [href]="address.url" [disabled]="!isRunning()">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="primary-destructive"
|
||||
[style.margin-inline-end.rem]="0.5"
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="flat-grayscale"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
@@ -141,11 +141,20 @@ export class InterfaceTorComponent {
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly tor = input.required<readonly TorAddress[]>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
|
||||
async remove({ url }: TorAddress) {
|
||||
const confirm = await firstValueFrom(
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.openConfirm({
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
yes: 'Delete',
|
||||
no: 'Cancel',
|
||||
content: 'Are you sure you want to delete this address?',
|
||||
},
|
||||
})
|
||||
.pipe(defaultIfEmpty(false)),
|
||||
)
|
||||
|
||||
@@ -161,7 +170,7 @@ export class InterfaceTorComponent {
|
||||
await this.api.pkgRemoveOnion({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
host: this.interface.value().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverRemoveOnion(params)
|
||||
@@ -215,7 +224,7 @@ export class InterfaceTorComponent {
|
||||
await this.api.pkgAddOnion({
|
||||
onion,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
host: this.interface.value().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddOnion({ onion })
|
||||
|
||||
@@ -62,6 +62,13 @@ import { HeaderComponent } from './components/header/header.component'
|
||||
flex-direction: column;
|
||||
// @TODO Theme
|
||||
background: url(/assets/img/background_dark.jpeg) fixed center/cover;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
backdrop-filter: blur(0.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
|
||||
@@ -30,7 +30,7 @@ const ROUTES: Routes = [
|
||||
{
|
||||
title: titleResolver,
|
||||
path: 'logs',
|
||||
loadComponent: () => import('./routes/logs/logs.component'),
|
||||
loadChildren: () => import('./routes/logs/logs.routes'),
|
||||
data: toNavigationItem('/portal/logs'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
input,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'logs-header',
|
||||
template: `
|
||||
<ng-container *title>
|
||||
<a tuiIconButton size="m" iconStart="@tui.arrow-left" routerLink="..">
|
||||
{{ 'Back' | i18n }}
|
||||
</a>
|
||||
{{ title() }}
|
||||
</ng-container>
|
||||
<hgroup tuiTitle>
|
||||
<h3>{{ title() }}</h3>
|
||||
<p tuiSubtitle><ng-content /></p>
|
||||
</hgroup>
|
||||
<aside tuiAccessories>
|
||||
<a
|
||||
tuiIconButton
|
||||
appearance="secondary-grayscale"
|
||||
iconStart="@tui.x"
|
||||
size="s"
|
||||
routerLink=".."
|
||||
[style.border-radius.%]="100"
|
||||
>
|
||||
{{ 'Close' | i18n }}
|
||||
</a>
|
||||
</aside>
|
||||
`,
|
||||
styles: `
|
||||
logs-header[tuiHeader] {
|
||||
margin-block-end: 1rem;
|
||||
|
||||
+ logs {
|
||||
height: calc(100% - 5rem);
|
||||
}
|
||||
|
||||
tui-root._mobile & {
|
||||
display: none;
|
||||
|
||||
+ logs {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, TuiTitle, i18nPipe, RouterLink, TitleDirective],
|
||||
hostDirectives: [TuiHeader],
|
||||
})
|
||||
export class LogsHeaderComponent {
|
||||
readonly title = input<string | undefined>()
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import { KeyValuePipe } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiAppearance, TuiButton, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiCardMedium } from '@taiga-ui/layout'
|
||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
|
||||
interface Log {
|
||||
title: i18nKey
|
||||
subtitle: i18nKey
|
||||
icon: string
|
||||
follow: (params: RR.FollowServerLogsReq) => Promise<RR.FollowServerLogsRes>
|
||||
fetch: (params: RR.GetServerLogsReq) => Promise<RR.GetServerLogsRes>
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *title>
|
||||
@if (current(); as key) {
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.arrow-left"
|
||||
(click)="current.set(null)"
|
||||
>
|
||||
{{ 'Back' | i18n }}
|
||||
</button>
|
||||
{{ logs[key]?.title | i18n }}
|
||||
} @else {
|
||||
{{ 'Logs' | i18n }}
|
||||
}
|
||||
</ng-container>
|
||||
@if (current(); as key) {
|
||||
<header tuiTitle>
|
||||
<strong class="title">
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="secondary-grayscale"
|
||||
iconStart="@tui.x"
|
||||
size="s"
|
||||
class="close"
|
||||
(click)="current.set(null)"
|
||||
>
|
||||
{{ 'Close' | i18n }}
|
||||
</button>
|
||||
{{ logs[key]?.title | i18n }}
|
||||
</strong>
|
||||
<p tuiSubtitle>{{ logs[key]?.subtitle | i18n }}</p>
|
||||
</header>
|
||||
@for (log of logs | keyvalue; track $index) {
|
||||
@if (log.key === current()) {
|
||||
<logs
|
||||
[context]="log.key"
|
||||
[followLogs]="log.value.follow"
|
||||
[fetchLogs]="log.value.fetch"
|
||||
/>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
@for (log of logs | keyvalue; track $index) {
|
||||
<button
|
||||
tuiCardMedium
|
||||
tuiAppearance="neutral"
|
||||
(click)="current.set(log.key)"
|
||||
>
|
||||
<tui-icon [icon]="log.value.icon" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ log.value.title | i18n }}</strong>
|
||||
<span tuiSubtitle>{{ log.value.subtitle | i18n }}</span>
|
||||
</span>
|
||||
<tui-icon icon="@tui.chevron-right" />
|
||||
</button>
|
||||
}
|
||||
}
|
||||
`,
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'g-page' },
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
logs {
|
||||
height: calc(100% - 4rem);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
button::before {
|
||||
margin: 0 -0.25rem 0 -0.375rem;
|
||||
--tui-icon-size: 1.5rem;
|
||||
}
|
||||
|
||||
[tuiCardMedium] {
|
||||
height: 14rem;
|
||||
width: 14rem;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--tui-background-neutral-1),
|
||||
var(--tui-shadow-small);
|
||||
|
||||
[tuiSubtitle] {
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
|
||||
tui-icon:last-child {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
logs {
|
||||
height: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
[tuiCardMedium] {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [
|
||||
LogsComponent,
|
||||
TitleDirective,
|
||||
KeyValuePipe,
|
||||
TuiTitle,
|
||||
TuiCardMedium,
|
||||
TuiIcon,
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export default class SystemLogsComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly current = signal<string | null>(null)
|
||||
readonly logs: Record<string, Log> = {
|
||||
os: {
|
||||
title: 'OS Logs',
|
||||
subtitle: 'Raw, unfiltered operating system logs',
|
||||
icon: '@tui.square-dashed-bottom-code',
|
||||
follow: params => this.api.followServerLogs(params),
|
||||
fetch: params => this.api.getServerLogs(params),
|
||||
},
|
||||
kernel: {
|
||||
title: 'Kernel Logs',
|
||||
subtitle: 'Diagnostics for drivers and other kernel processes',
|
||||
icon: '@tui.square-chevron-right',
|
||||
follow: params => this.api.followKernelLogs(params),
|
||||
fetch: params => this.api.getKernelLogs(params),
|
||||
},
|
||||
tor: {
|
||||
title: 'Tor Logs',
|
||||
subtitle: 'Diagnostic logs for the Tor daemon on StartOS',
|
||||
icon: '@tui.globe',
|
||||
follow: params => this.api.followTorLogs(params),
|
||||
fetch: params => this.api.getTorLogs(params),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Routes } from '@angular/router'
|
||||
|
||||
export const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () => import('./routes/outlet.component'),
|
||||
},
|
||||
{
|
||||
path: 'kernel',
|
||||
loadComponent: () => import('./routes/kernel.component'),
|
||||
},
|
||||
{
|
||||
path: 'os',
|
||||
loadComponent: () => import('./routes/os.component'),
|
||||
},
|
||||
{
|
||||
path: 'tor',
|
||||
loadComponent: () => import('./routes/tor.component'),
|
||||
},
|
||||
]
|
||||
|
||||
export default ROUTES
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
import { LogsHeaderComponent } from '../components/header.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<logs-header [title]="'Kernel Logs' | i18n">
|
||||
{{ 'Diagnostics for drivers and other kernel processes' | i18n }}
|
||||
</logs-header>
|
||||
<logs context="kernel" [followLogs]="follow" [fetchLogs]="fetch" />
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
padding: 1rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [LogsComponent, LogsHeaderComponent, i18nPipe],
|
||||
host: { class: 'g-page' },
|
||||
})
|
||||
export default class SystemKernelComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
protected readonly follow = (params: RR.FollowServerLogsReq) =>
|
||||
this.api.followKernelLogs(params)
|
||||
|
||||
protected readonly fetch = (params: RR.GetServerLogsReq) =>
|
||||
this.api.getKernelLogs(params)
|
||||
|
||||
log = {
|
||||
title: 'Kernel Logs',
|
||||
subtitle: 'Diagnostics for drivers and other kernel processes',
|
||||
icon: '@tui.square-chevron-right',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
||||
import { LogsHeaderComponent } from 'src/app/routes/portal/routes/logs/components/header.component'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<logs-header [title]="'OS Logs' | i18n">
|
||||
{{ 'Raw, unfiltered operating system logs' | i18n }}
|
||||
</logs-header>
|
||||
<logs context="os" [followLogs]="follow" [fetchLogs]="fetch" />
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
padding: 1rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [LogsComponent, LogsHeaderComponent, i18nPipe],
|
||||
host: { class: 'g-page' },
|
||||
})
|
||||
export default class SystemOSComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
protected readonly follow = (params: RR.FollowServerLogsReq) =>
|
||||
this.api.followServerLogs(params)
|
||||
|
||||
protected readonly fetch = (params: RR.GetServerLogsReq) =>
|
||||
this.api.getServerLogs(params)
|
||||
|
||||
log = {
|
||||
title: 'Kernel Logs',
|
||||
subtitle: 'Diagnostics for drivers and other kernel processes',
|
||||
icon: '@tui.square-chevron-right',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiAppearance, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiCardMedium } from '@taiga-ui/layout'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *title>{{ 'Logs' | i18n }}</ng-container>
|
||||
@for (log of logs; track $index) {
|
||||
<a tuiCardMedium tuiAppearance="neutral" [routerLink]="log.link">
|
||||
<tui-icon [icon]="log.icon" />
|
||||
<span tuiTitle>
|
||||
{{ log.title | i18n }}
|
||||
<span tuiSubtitle>{{ log.subtitle | i18n }}</span>
|
||||
</span>
|
||||
<tui-icon icon="@tui.chevron-right" />
|
||||
</a>
|
||||
}
|
||||
`,
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'g-page' },
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[tuiCardMedium] {
|
||||
height: 14rem;
|
||||
width: 14rem;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--tui-background-neutral-1),
|
||||
var(--tui-shadow-small);
|
||||
|
||||
[tuiSubtitle] {
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
|
||||
tui-icon:last-child {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
[tuiCardMedium] {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [
|
||||
RouterLink,
|
||||
TitleDirective,
|
||||
TuiTitle,
|
||||
TuiCardMedium,
|
||||
TuiIcon,
|
||||
TuiAppearance,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export default class SystemLogsComponent {
|
||||
readonly logs = [
|
||||
{
|
||||
link: 'os',
|
||||
title: 'OS Logs',
|
||||
subtitle: 'Raw, unfiltered operating system logs',
|
||||
icon: '@tui.square-dashed-bottom-code',
|
||||
},
|
||||
{
|
||||
link: 'kernel',
|
||||
title: 'Kernel Logs',
|
||||
subtitle: 'Diagnostics for drivers and other kernel processes',
|
||||
icon: '@tui.square-chevron-right',
|
||||
},
|
||||
{
|
||||
link: 'tor',
|
||||
title: 'Tor Logs',
|
||||
subtitle: 'Diagnostic logs for the Tor daemon on StartOS',
|
||||
icon: '@tui.globe',
|
||||
},
|
||||
] as const
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
||||
import { LogsHeaderComponent } from 'src/app/routes/portal/routes/logs/components/header.component'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<logs-header [title]="'Tor Logs' | i18n">
|
||||
{{ 'Diagnostic logs for the Tor daemon on StartOS' | i18n }}
|
||||
</logs-header>
|
||||
<logs context="tor" [followLogs]="follow" [fetchLogs]="fetch" />
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
padding: 1rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [LogsComponent, LogsHeaderComponent, i18nPipe],
|
||||
host: { class: 'g-page' },
|
||||
})
|
||||
export default class SystemOSComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
protected readonly follow = (params: RR.FollowServerLogsReq) =>
|
||||
this.api.followServerLogs(params)
|
||||
|
||||
protected readonly fetch = (params: RR.GetServerLogsReq) =>
|
||||
this.api.getServerLogs(params)
|
||||
|
||||
log = {
|
||||
title: 'Kernel Logs',
|
||||
subtitle: 'Diagnostics for drivers and other kernel processes',
|
||||
icon: '@tui.square-chevron-right',
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,15 @@ import { StorageService } from 'src/app/services/storage.service'
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
background: rgb(55 58 63 / 90%)
|
||||
url('/assets/img/background_marketplace.png') no-repeat top right;
|
||||
url('/assets/img/background_marketplace.jpg') no-repeat top right;
|
||||
background-size: cover;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
backdrop-filter: blur(2rem);
|
||||
}
|
||||
}
|
||||
|
||||
.marketplace-content {
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import {
|
||||
DialogService,
|
||||
Exver,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
MARKDOWN,
|
||||
SharedPipesModule,
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
|
||||
@@ -59,10 +59,9 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
<marketplace-additional-item
|
||||
(click)="selectVersion(pkg, version)"
|
||||
[data]="('Click to view all versions' | i18n) || ''"
|
||||
[icon]="versions.length > 1 ? '@tui.chevron-right' : ''"
|
||||
icon="@tui.chevron-right"
|
||||
label="All versions"
|
||||
class="versions"
|
||||
[class.versions_empty]="versions.length < 2"
|
||||
/>
|
||||
<ng-template
|
||||
#version
|
||||
@@ -81,7 +80,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="completeWith(data.value)"
|
||||
(click)="completeWith(data.version)"
|
||||
>
|
||||
{{ 'Ok' | i18n }}
|
||||
</button>
|
||||
@@ -91,7 +90,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
</marketplace-additional>
|
||||
</div>
|
||||
} @else {
|
||||
<tui-loader class="loading" textContent="Loading" />
|
||||
<tui-loader textContent="Loading" [style.height.%]="100" />
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
@@ -114,7 +113,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
}
|
||||
|
||||
.listing {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.8rem;
|
||||
// @TODO theme
|
||||
color: #8059e5;
|
||||
font-weight: 600;
|
||||
@@ -139,16 +138,6 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
::ng-deep label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&_empty {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
min-width: 30rem;
|
||||
height: 100%;
|
||||
place-self: center;
|
||||
}
|
||||
|
||||
marketplace-additional {
|
||||
@@ -254,6 +243,6 @@ export class MarketplacePreviewComponent {
|
||||
data: { version },
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(version => this.version$.next(version))
|
||||
.subscribe(selected => this.version$.next(selected))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<b>{{ value() || '-' }} C°</b>
|
||||
<b>{{ value() ? value() + ' C°' : 'N/A' }}</b>
|
||||
`,
|
||||
styles: `
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
@@ -55,7 +55,11 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
}
|
||||
@if (notificationItem.code === 1 || notificationItem.code === 2) {
|
||||
<button tuiLink (click)="service.viewModal(notificationItem)">
|
||||
{{ 'View report' | i18n }}
|
||||
{{
|
||||
notificationItem.code === 1
|
||||
? ('View report' | i18n)
|
||||
: ('View details' | i18n)
|
||||
}}
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
@@ -66,7 +70,7 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
'[class._new]': '!notificationItem.read',
|
||||
},
|
||||
styles: `
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
|
||||
|
||||
:host {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -90,8 +94,16 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem !important;
|
||||
|
||||
.checkbox {
|
||||
@include fullsize();
|
||||
@include taiga.fullsize();
|
||||
@include taiga.transition(box-shadow);
|
||||
|
||||
&:has(:checked) {
|
||||
box-shadow: inset 0.25rem 0 var(--tui-background-accent-1);
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
@@ -103,8 +115,7 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.service:not(:has(a)) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
OnInit,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
@@ -66,7 +67,7 @@ import { NotificationsTableComponent } from './table.component'
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export default class NotificationsComponent {
|
||||
export default class NotificationsComponent implements OnInit {
|
||||
private readonly router = inject(Router)
|
||||
private readonly route = inject(ActivatedRoute)
|
||||
|
||||
@@ -74,16 +75,19 @@ export default class NotificationsComponent {
|
||||
readonly api = inject(ApiService)
|
||||
readonly errorService = inject(ErrorService)
|
||||
readonly notifications = signal<ServerNotifications | undefined>(undefined)
|
||||
readonly toast = this.route.queryParams.subscribe(params => {
|
||||
this.router.navigate([], { relativeTo: this.route, queryParams: {} })
|
||||
|
||||
if (isEmptyObject(params)) {
|
||||
this.getMore({})
|
||||
}
|
||||
})
|
||||
|
||||
open = false
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.router.navigate([], { relativeTo: this.route, queryParams: {} })
|
||||
|
||||
if (isEmptyObject(params)) {
|
||||
this.getMore({})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getMore(params: RR.GetNotificationsReq) {
|
||||
try {
|
||||
this.notifications.set(undefined)
|
||||
|
||||
@@ -31,7 +31,7 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
/>
|
||||
</th>
|
||||
<th [style.min-width.rem]="12">{{ 'Date' | i18n }}</th>
|
||||
<th [style.min-width.rem]="12">{{ 'Title' | i18n }}</th>
|
||||
<th [style.min-width.rem]="14">{{ 'Title' | i18n }}</th>
|
||||
<th [style.min-width.rem]="8">{{ 'Service' | i18n }}</th>
|
||||
<th>{{ 'Message' | i18n }}</th>
|
||||
</tr>
|
||||
@@ -71,9 +71,13 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
styles: `
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
:host-context(tui-root._mobile) input {
|
||||
@include fullsize();
|
||||
opacity: 0;
|
||||
:host-context(tui-root._mobile) {
|
||||
margin: 0 -1rem;
|
||||
|
||||
input {
|
||||
@include fullsize();
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -14,7 +14,7 @@ interface ActionItem {
|
||||
template: `
|
||||
<div tuiTitle>
|
||||
<strong>{{ action.name }}</strong>
|
||||
<div tuiSubtitle>{{ action.description }}</div>
|
||||
<div tuiSubtitle [innerHTML]="action.description"></div>
|
||||
@if (disabled) {
|
||||
<div tuiSubtitle class="g-warning">{{ disabled }}</div>
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
/>
|
||||
</tui-avatar>
|
||||
<span tuiTitle>
|
||||
{{ d.value.title }}
|
||||
{{ d.value.title || d.key }}
|
||||
@if (getError(d.key); as error) {
|
||||
<span tuiSubtitle class="g-warning">{{ error | i18n }}</span>
|
||||
} @else {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiLineClamp, TuiTooltip } from '@taiga-ui/kit'
|
||||
import { TuiTooltip } from '@taiga-ui/kit'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
@@ -15,12 +15,9 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
standalone: true,
|
||||
selector: 'service-error',
|
||||
template: `
|
||||
<header>{{ 'Error' | i18n }}</header>
|
||||
<tui-line-clamp
|
||||
[linesLimit]="2"
|
||||
[content]="error?.message"
|
||||
(overflownChange)="overflow = $event"
|
||||
/>
|
||||
<header>{{ 'Service Launch Error' | i18n }}</header>
|
||||
<p class="error-message">{{ error?.message }}</p>
|
||||
<p>{{ error?.debug }}</p>
|
||||
<h4>
|
||||
{{ 'Actions' | i18n }}
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
@@ -34,7 +31,13 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
</p>
|
||||
<p>
|
||||
{{
|
||||
'"Uninstall service" is a dangerous action that will remove the service from StartOS and wipe all its data.'
|
||||
'"Soft uninstall" will remove the service from StartOS but preserve its data.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
{{
|
||||
'"Hard uninstall" is a dangerous action that will remove the service from StartOS and wipe all its data.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
@@ -43,8 +46,11 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
<button tuiButton (click)="rebuild()">
|
||||
{{ 'Rebuild container' | i18n }}
|
||||
</button>
|
||||
<button tuiButton appearance="negative" (click)="uninstall()">
|
||||
{{ 'Uninstall service' | i18n }}
|
||||
<button tuiButton appearance="warning" (click)="uninstall()">
|
||||
{{ 'Soft uninstall' | i18n }}
|
||||
</button>
|
||||
<button tuiButton appearance="negative" (click)="uninstall(false)">
|
||||
{{ 'Hard uninstall' | i18n }}
|
||||
</button>
|
||||
@if (overflow) {
|
||||
<button tuiButton appearance="secondary-grayscale" (click)="show()">
|
||||
@@ -55,23 +61,21 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 4;
|
||||
grid-column: span 5;
|
||||
}
|
||||
|
||||
header {
|
||||
--tui-background-neutral-1: var(--tui-status-negative-pale);
|
||||
}
|
||||
|
||||
tui-line-clamp {
|
||||
pointer-events: none;
|
||||
margin: 1rem 0;
|
||||
.error-message {
|
||||
font-size: 1.5rem;
|
||||
color: var(--tui-status-negative);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font: var(--tui-font-text-m);
|
||||
font-weight: bold;
|
||||
color: var(--tui-text-secondary);
|
||||
@@ -80,7 +84,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, TuiIcon, TuiTooltip, TuiLineClamp, i18nPipe],
|
||||
imports: [TuiButton, TuiIcon, TuiTooltip, i18nPipe],
|
||||
})
|
||||
export class ServiceErrorComponent {
|
||||
private readonly dialog = inject(DialogService)
|
||||
@@ -99,8 +103,8 @@ export class ServiceErrorComponent {
|
||||
this.service.rebuild(getManifest(this.pkg).id)
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
this.service.uninstall(getManifest(this.pkg))
|
||||
uninstall(soft = true) {
|
||||
this.service.uninstall(getManifest(this.pkg), { force: true, soft })
|
||||
}
|
||||
|
||||
show() {
|
||||
|
||||
@@ -97,7 +97,7 @@ export class ServiceHealthCheckComponent {
|
||||
case 'starting':
|
||||
return this.i18n.transform('Starting')!
|
||||
case 'success':
|
||||
return `${this.i18n.transform('Success')}: ${this.healthCheck.message}`
|
||||
return `${this.i18n.transform('Success')}: ${this.healthCheck.message || 'health check passing'}`
|
||||
case 'loading':
|
||||
case 'failure':
|
||||
return this.healthCheck.message
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -5,9 +6,8 @@ import {
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiIcon, TuiLink } from '@taiga-ui/core'
|
||||
import { TuiButton, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
@@ -16,16 +16,11 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
selector: 'tr[serviceInterface]',
|
||||
template: `
|
||||
<td>
|
||||
<a tuiLink [routerLink]="info.routerLink">
|
||||
<strong>{{ info.name }}</strong>
|
||||
</a>
|
||||
<strong>{{ info.name }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<tui-badge size="m" [appearance]="appearance">{{ info.type }}</tui-badge>
|
||||
</td>
|
||||
<td class="g-secondary" [style.grid-area]="'2 / span 4'">
|
||||
{{ info.description }}
|
||||
</td>
|
||||
<td [style.text-align]="'center'">
|
||||
@if (info.public) {
|
||||
<tui-icon class="g-positive" icon="@tui.globe" />
|
||||
@@ -33,73 +28,70 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
<tui-icon class="g-negative" icon="@tui.lock" />
|
||||
}
|
||||
</td>
|
||||
<td [style.grid-area]="'span 2'">
|
||||
<td class="g-secondary" [style.grid-area]="'2 / span 4'">
|
||||
{{ info.description }}
|
||||
</td>
|
||||
<td>
|
||||
@if (info.type === 'ui') {
|
||||
<a
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="action"
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
size="s"
|
||||
[style.border-radius.%]="100"
|
||||
[attr.href]="href"
|
||||
(click.stop)="(0)"
|
||||
>
|
||||
{{ 'Open' | i18n }}
|
||||
</a>
|
||||
appearance="flat-grayscale"
|
||||
[disabled]="disabled"
|
||||
(click)="openUI()"
|
||||
></button>
|
||||
}
|
||||
<a
|
||||
tuiIconButton
|
||||
iconStart="@tui.settings"
|
||||
appearance="flat-grayscale"
|
||||
[routerLink]="info.routerLink"
|
||||
></a>
|
||||
</td>
|
||||
`,
|
||||
styles: `
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
:host {
|
||||
cursor: pointer;
|
||||
clip-path: inset(0 round var(--tui-radius-m));
|
||||
@include transition(background);
|
||||
}
|
||||
|
||||
[tuiLink] {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@media ($tui-mouse) {
|
||||
:host:hover {
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tui-badge {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tui-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
grid-area: 3 / span 4;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, min-content) 1fr 2rem;
|
||||
grid-template-columns: repeat(3, min-content) 1fr;
|
||||
align-items: center;
|
||||
padding: 1rem 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiButton, TuiBadge, TuiLink, TuiIcon, RouterLink, i18nPipe],
|
||||
imports: [TuiButton, TuiBadge, TuiIcon, RouterLink],
|
||||
})
|
||||
export class ServiceInterfaceComponent {
|
||||
export class ServiceInterfaceItemComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
private readonly document = inject(DOCUMENT)
|
||||
|
||||
@Input({ required: true })
|
||||
info!: T.ServiceInterface & {
|
||||
@@ -116,17 +108,19 @@ export class ServiceInterfaceComponent {
|
||||
get appearance(): string {
|
||||
switch (this.info.type) {
|
||||
case 'ui':
|
||||
return 'primary'
|
||||
return 'positive'
|
||||
case 'api':
|
||||
return 'accent'
|
||||
return 'info'
|
||||
case 'p2p':
|
||||
return 'primary-grayscale'
|
||||
return 'negative'
|
||||
}
|
||||
}
|
||||
|
||||
get href(): string | null {
|
||||
return this.disabled
|
||||
? null
|
||||
: this.config.launchableAddress(this.info, this.pkg.hosts)
|
||||
get href() {
|
||||
return this.config.launchableAddress(this.info, this.pkg.hosts)
|
||||
}
|
||||
|
||||
openUI() {
|
||||
this.document.defaultView?.open(this.href, '_blank', 'noreferrer')
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,12 @@ import {
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { TuiTable } from '@taiga-ui/addon-table'
|
||||
import { tuiDefaultSort } from '@taiga-ui/cdk'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getAddresses } from '../../../components/interfaces/interface.utils'
|
||||
import { ServiceInterfaceComponent } from './interface.component'
|
||||
import { ServiceInterfaceItemComponent } from './interface-item.component'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
@@ -24,8 +23,8 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
<tr>
|
||||
<th tuiTh>{{ 'Name' | i18n }}</th>
|
||||
<th tuiTh>{{ 'Type' | i18n }}</th>
|
||||
<th tuiTh [style.text-align]="'center'">{{ 'Hosting' | i18n }}</th>
|
||||
<th tuiTh>{{ 'Description' | i18n }}</th>
|
||||
<th tuiTh>{{ 'Hosting' | i18n }}</th>
|
||||
<th tuiTh></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -33,7 +32,6 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
@for (info of interfaces(); track $index) {
|
||||
<tr
|
||||
serviceInterface
|
||||
[routerLink]="info.routerLink"
|
||||
[info]="info"
|
||||
[pkg]="pkg()"
|
||||
[disabled]="disabled()"
|
||||
@@ -44,12 +42,12 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 4;
|
||||
grid-column: span 6;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ServiceInterfaceComponent, TuiTable, RouterLink, i18nPipe],
|
||||
imports: [ServiceInterfaceItemComponent, TuiTable, i18nPipe],
|
||||
})
|
||||
export class ServiceInterfacesComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
`
|
||||
:host {
|
||||
grid-column: span 2;
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -77,6 +78,10 @@ import {
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
:host {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
|
||||
@@ -5,56 +5,84 @@ import {
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiAvatar } from '@taiga-ui/kit'
|
||||
import { TuiAvatar, TuiFade } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'tr[actionRequest]',
|
||||
selector: 'tr[task]',
|
||||
template: `
|
||||
<td>
|
||||
<td tuiFade>
|
||||
<tui-avatar size="xs"><img [src]="pkg()?.icon" alt="" /></tui-avatar>
|
||||
<span>{{ title() }}</span>
|
||||
<span>{{ pkgTitle() }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (actionRequest().severity === 'critical') {
|
||||
{{ pkg()?.actions?.[task().actionId]?.name }}
|
||||
</td>
|
||||
<td>
|
||||
@if (task().severity === 'critical') {
|
||||
<strong [style.color]="'var(--tui-status-warning)'">
|
||||
{{ 'Required' | i18n }}
|
||||
</strong>
|
||||
} @else {
|
||||
} @else if (task().severity === 'important') {
|
||||
<strong [style.color]="'var(--tui-status-info)'">
|
||||
{{ 'Recommended' | i18n }}
|
||||
</strong>
|
||||
} @else {
|
||||
<strong>
|
||||
{{ 'Optional' | i18n }}
|
||||
</strong>
|
||||
}
|
||||
</td>
|
||||
<td
|
||||
[style.color]="'var(--tui-text-secondary)'"
|
||||
[style.grid-area]="'2 / span 2'"
|
||||
[style.grid-area]="'2 / span 4'"
|
||||
>
|
||||
{{ actionRequest().reason || ('No reason provided' | i18n) }}
|
||||
{{ task().reason || ('No reason provided' | i18n) }}
|
||||
</td>
|
||||
<td>
|
||||
<button tuiButton (click)="handle()">
|
||||
{{ pkg()?.actions?.[actionRequest().actionId]?.name }}
|
||||
</button>
|
||||
@if (task().severity !== 'critical') {
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="flat-grayscale"
|
||||
(click)="dismiss()"
|
||||
></button>
|
||||
}
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.play"
|
||||
appearance="flat-grayscale"
|
||||
(click)="handle()"
|
||||
></button>
|
||||
</td>
|
||||
`,
|
||||
styles: `
|
||||
td:first-child {
|
||||
white-space: nowrap;
|
||||
max-width: 10rem;
|
||||
max-width: 15rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
grid-area: 3 / span 4;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
grid-area: span 2;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -64,32 +92,66 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
align-items: center;
|
||||
padding: 1rem 0.5rem;
|
||||
padding: 1rem 0rem 1rem 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, TuiAvatar, i18nPipe],
|
||||
imports: [TuiButton, TuiAvatar, i18nPipe, TuiFade],
|
||||
})
|
||||
export class ServiceTaskComponent {
|
||||
private readonly actionService = inject(ActionService)
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
|
||||
readonly actionRequest = input.required<T.Task>()
|
||||
readonly task = input.required<T.Task & { replayId: string }>()
|
||||
readonly services = input.required<Record<string, PackageDataEntry>>()
|
||||
|
||||
readonly pkg = computed(() => this.services()[this.actionRequest().packageId])
|
||||
readonly title = computed((pkg = this.pkg()) => pkg && getManifest(pkg).title)
|
||||
readonly pkg = computed(() => this.services()[this.task().packageId])
|
||||
readonly pkgTitle = computed(
|
||||
(pkg = this.pkg()) => pkg && getManifest(pkg).title,
|
||||
)
|
||||
|
||||
async dismiss() {
|
||||
this.dialog
|
||||
.openConfirm<boolean>({
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
content: 'Are you sure you want to dismiss this task?',
|
||||
yes: 'Dismiss',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open().subscribe()
|
||||
try {
|
||||
await this.api.clearTask({
|
||||
packageId: this.task().packageId,
|
||||
replayId: this.task().replayId,
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async handle() {
|
||||
const title = this.title()
|
||||
const title = this.pkgTitle()
|
||||
const pkg = this.pkg()
|
||||
const metadata = pkg?.actions[this.actionRequest().actionId]
|
||||
const metadata = pkg?.actions[this.task().actionId]
|
||||
|
||||
if (!title || !pkg || !metadata) {
|
||||
return
|
||||
@@ -97,16 +159,16 @@ export class ServiceTaskComponent {
|
||||
|
||||
this.actionService.present({
|
||||
pkgInfo: {
|
||||
id: this.actionRequest().packageId,
|
||||
id: this.task().packageId,
|
||||
title,
|
||||
mainStatus: pkg.status.main,
|
||||
icon: pkg.icon,
|
||||
},
|
||||
actionInfo: {
|
||||
id: this.actionRequest().actionId,
|
||||
id: this.task().actionId,
|
||||
metadata,
|
||||
},
|
||||
requestInfo: this.actionRequest(),
|
||||
requestInfo: this.task(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,18 +19,19 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
<thead>
|
||||
<tr>
|
||||
<th tuiTh>{{ 'Service' | i18n }}</th>
|
||||
<th tuiTh>{{ 'Type' | i18n }}</th>
|
||||
<th tuiTh>{{ 'Action' }}</th>
|
||||
<th tuiTh>{{ 'Severity' }}</th>
|
||||
<th tuiTh>{{ 'Description' | i18n }}</th>
|
||||
<th tuiTh></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (item of requests(); track $index) {
|
||||
<tr [actionRequest]="item.task" [services]="services()"></tr>
|
||||
@for (item of tasks(); track $index) {
|
||||
<tr [task]="item.task" [services]="services()"></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@if (!requests().length) {
|
||||
@if (!tasks().length) {
|
||||
<app-placeholder icon="@tui.list-checks">
|
||||
{{ 'All tasks complete' | i18n }}
|
||||
</app-placeholder>
|
||||
@@ -50,8 +51,12 @@ export class ServiceTasksComponent {
|
||||
readonly pkg = input.required<PackageDataEntry>()
|
||||
readonly services = input.required<Record<string, PackageDataEntry>>()
|
||||
|
||||
readonly requests = computed(() =>
|
||||
Object.values(this.pkg().tasks)
|
||||
readonly tasks = computed(() =>
|
||||
Object.entries(this.pkg().tasks)
|
||||
.map(([replayId, entry]) => ({
|
||||
...entry,
|
||||
task: { ...entry.task, replayId },
|
||||
}))
|
||||
.filter(
|
||||
t =>
|
||||
this.services()[t.task.packageId]?.actions[t.task.actionId] &&
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { map, timer } from 'rxjs'
|
||||
import { distinctUntilChanged } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'service-uptime',
|
||||
template: `
|
||||
<header>{{ 'Uptime' | i18n }}</header>
|
||||
<section>
|
||||
@if (uptime$ | async; as time) {
|
||||
<div>
|
||||
<label>{{ time.days }}</label>
|
||||
{{ 'Days' | i18n }}
|
||||
</div>
|
||||
<div>
|
||||
<label>{{ time.hours }}</label>
|
||||
{{ 'Hours' | i18n }}
|
||||
</div>
|
||||
<div>
|
||||
<label>{{ time.minutes }}</label>
|
||||
{{ 'Minutes' | i18n }}
|
||||
</div>
|
||||
<div>
|
||||
<label>{{ time.seconds }}</label>
|
||||
{{ 'Seconds' | i18n }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font: var(--tui-font-heading-4);
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section {
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
place-content: center;
|
||||
margin: auto;
|
||||
padding: 1rem 0;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
color: var(--tui-text-secondary);
|
||||
font: var(--tui-font-text-ui-xs);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: min(6vw, 2.5rem);
|
||||
margin: 1rem 0;
|
||||
color: var(--tui-text-primary);
|
||||
}
|
||||
`,
|
||||
],
|
||||
host: { class: 'g-card' },
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [i18nPipe, AsyncPipe],
|
||||
})
|
||||
export class ServiceUptimeComponent {
|
||||
protected readonly uptime$ = timer(0, 1000).pipe(
|
||||
map(() =>
|
||||
this.started()
|
||||
? Math.max(Date.now() - new Date(this.started()).getTime(), 0)
|
||||
: 0,
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
map(delta => ({
|
||||
seconds: Math.floor(delta / 1000) % 60,
|
||||
minutes: Math.floor(delta / (1000 * 60)) % 60,
|
||||
hours: Math.floor(delta / (1000 * 60 * 60)) % 24,
|
||||
days: Math.floor(delta / (1000 * 60 * 60 * 24)),
|
||||
})),
|
||||
)
|
||||
|
||||
readonly started = input('')
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { UILaunchComponent } from './ui.component'
|
||||
import { UILaunchComponent } from './ui-launch.component'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
const RUNNING = ['running', 'starting', 'restarting']
|
||||
@@ -23,6 +23,7 @@ const RUNNING = ['running', 'starting', 'restarting']
|
||||
standalone: true,
|
||||
selector: 'fieldset[appControls]',
|
||||
template: `
|
||||
<app-ui-launch [pkg]="pkg()" />
|
||||
@if (running()) {
|
||||
<button
|
||||
tuiIconButton
|
||||
@@ -42,8 +43,6 @@ const RUNNING = ['running', 'starting', 'restarting']
|
||||
{{ 'Start' | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
<app-ui-launch [pkg]="pkg()" />
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, UILaunchComponent, TuiLet, AsyncPipe, i18nPipe],
|
||||
@@ -53,6 +52,7 @@ const RUNNING = ['running', 'starting', 'restarting']
|
||||
padding: 0;
|
||||
border: none;
|
||||
cursor: default;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
|
||||
@@ -35,9 +35,7 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
<th tuiTh [requiredSort]="true" [sorter]="status">
|
||||
{{ 'Status' | i18n }}
|
||||
</th>
|
||||
<th [style.width.rem]="8" [style.text-indent.rem]="1.5">
|
||||
{{ 'Controls' | i18n }}
|
||||
</th>
|
||||
<th [style.width.rem]="8" [style.text-indent.rem]="1.5"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -57,7 +57,8 @@ export class StatusComponent {
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
get healthy(): boolean {
|
||||
return !this.hasDepErrors && this.getStatus(this.pkg).health !== 'failure'
|
||||
const { primary, health } = this.getStatus(this.pkg)
|
||||
return !this.hasDepErrors && primary !== 'error' && health !== 'failure'
|
||||
}
|
||||
|
||||
get loading(): boolean {
|
||||
@@ -66,7 +67,7 @@ export class StatusComponent {
|
||||
|
||||
@tuiPure
|
||||
getStatus(pkg: PackageDataEntry) {
|
||||
return renderPkgStatus(pkg, {})
|
||||
return renderPkgStatus(pkg)
|
||||
}
|
||||
|
||||
get status(): i18nKey {
|
||||
@@ -95,6 +96,8 @@ export class StatusComponent {
|
||||
return 'Removing'
|
||||
case 'restoring':
|
||||
return 'Restoring'
|
||||
case 'error':
|
||||
return 'Error'
|
||||
default:
|
||||
return 'Unknown'
|
||||
}
|
||||
@@ -120,6 +123,8 @@ export class StatusComponent {
|
||||
return 'var(--tui-status-positive)'
|
||||
case 'actionRequired':
|
||||
return 'var(--tui-status-warning)'
|
||||
case 'error':
|
||||
return 'var(--tui-status-negative)'
|
||||
case 'installing':
|
||||
case 'updating':
|
||||
case 'stopping':
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -23,7 +24,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
[disabled]="!isRunning"
|
||||
[tuiDropdown]="content"
|
||||
>
|
||||
{{ 'Launch UI' | i18n }}
|
||||
{{ 'Open' | i18n }}
|
||||
</button>
|
||||
<ng-template #content>
|
||||
<tui-data-list>
|
||||
@@ -39,16 +40,15 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
}
|
||||
</tui-data-list>
|
||||
</ng-template>
|
||||
} @else {
|
||||
<a
|
||||
} @else if (interfaces[0]) {
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[attr.href]="getHref(first)"
|
||||
[disabled]="!isRunning"
|
||||
(click)="openUI(interfaces[0])"
|
||||
>
|
||||
{{ first?.name }}
|
||||
</a>
|
||||
{{ interfaces[0].name }}
|
||||
</button>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
@@ -61,6 +61,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
})
|
||||
export class UILaunchComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
private readonly document = inject(DOCUMENT)
|
||||
|
||||
@Input()
|
||||
pkg!: PackageDataEntry
|
||||
@@ -73,10 +74,6 @@ export class UILaunchComponent {
|
||||
return this.pkg.status.main === 'running'
|
||||
}
|
||||
|
||||
get first(): T.ServiceInterface | undefined {
|
||||
return this.interfaces[0]
|
||||
}
|
||||
|
||||
@tuiPure
|
||||
getInterfaces(pkg?: PackageDataEntry): T.ServiceInterface[] {
|
||||
return pkg
|
||||
@@ -89,9 +86,11 @@ export class UILaunchComponent {
|
||||
: []
|
||||
}
|
||||
|
||||
getHref(ui?: T.ServiceInterface): string | null {
|
||||
return ui && this.isRunning
|
||||
? this.config.launchableAddress(ui, this.pkg.hosts)
|
||||
: null
|
||||
getHref(ui: T.ServiceInterface): string {
|
||||
return this.config.launchableAddress(ui, this.pkg.hosts)
|
||||
}
|
||||
|
||||
openUI(ui: T.ServiceInterface) {
|
||||
this.document.defaultView?.open(this.getHref(ui), '_blank', 'noreferrer')
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,9 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
standalone: true,
|
||||
selector: 'app-action-success-single',
|
||||
template: `
|
||||
<p class="qr"><ng-container *ngTemplateOutlet="qr" /></p>
|
||||
@if (single.qr) {
|
||||
<p class="qr"><ng-container *ngTemplateOutlet="qr" /></p>
|
||||
}
|
||||
<tui-input
|
||||
[readOnly]="true"
|
||||
[ngModel]="single.value"
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { getPkgId, i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiItem } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiLink } from '@taiga-ui/core'
|
||||
import { TuiBreadcrumbs } from '@taiga-ui/kit'
|
||||
import { TuiBadge, TuiBreadcrumbs } from '@taiga-ui/kit'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||
@@ -26,21 +28,34 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
{{ 'Back' | i18n }}
|
||||
</a>
|
||||
{{ interface()?.name }}
|
||||
<interface-status [public]="!!interface()?.public" />
|
||||
<interface-status
|
||||
[style.margin-left.rem]="0.5"
|
||||
[public]="!!interface()?.public"
|
||||
/>
|
||||
</ng-container>
|
||||
<tui-breadcrumbs size="l" [style.margin-block-end.rem]="1">
|
||||
<tui-breadcrumbs size="l">
|
||||
<a *tuiItem tuiLink appearance="action-grayscale" routerLink="../..">
|
||||
{{ 'Dashboard' | i18n }}
|
||||
</a>
|
||||
<span *tuiItem class="g-primary">
|
||||
{{ interface()?.name }}
|
||||
<interface-status [public]="!!interface()?.public" />
|
||||
</span>
|
||||
<span *tuiItem class="g-primary">{{ interface()?.name }}</span>
|
||||
</tui-breadcrumbs>
|
||||
@if (interface(); as serviceInterface) {
|
||||
@if (interface(); as value) {
|
||||
<header tuiHeader [style.margin-bottom.rem]="1">
|
||||
<hgroup>
|
||||
<h3>
|
||||
{{ value.name }}
|
||||
<tui-badge size="l" [appearance]="getAppearance(value.type)">
|
||||
{{ value.type }}
|
||||
</tui-badge>
|
||||
<interface-status [public]="value.public" />
|
||||
</h3>
|
||||
<p tuiSubtitle>{{ value.description }}</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
<app-interface
|
||||
[packageId]="pkgId"
|
||||
[serviceInterface]="serviceInterface"
|
||||
[value]="value"
|
||||
[isRunning]="isRunning()"
|
||||
/>
|
||||
}
|
||||
`,
|
||||
@@ -48,6 +63,19 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
:host-context(tui-root._mobile) tui-breadcrumbs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
font-size: 2.4rem;
|
||||
|
||||
tui-badge {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-subpage' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -62,6 +90,8 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
TuiLink,
|
||||
InterfaceStatusComponent,
|
||||
i18nPipe,
|
||||
TuiBadge,
|
||||
TuiHeader,
|
||||
],
|
||||
})
|
||||
export default class ServiceInterfaceRoute {
|
||||
@@ -74,6 +104,10 @@ export default class ServiceInterfaceRoute {
|
||||
inject<PatchDB<DataModel>>(PatchDB).watch$('packageData', this.pkgId),
|
||||
)
|
||||
|
||||
readonly isRunning = computed(() => {
|
||||
return this.pkg()?.status.main === 'running'
|
||||
})
|
||||
|
||||
readonly interface = computed(() => {
|
||||
const pkg = this.pkg()
|
||||
const id = this.interfaceId()
|
||||
@@ -99,4 +133,15 @@ export default class ServiceInterfaceRoute {
|
||||
addresses: getAddresses(item, host, this.config),
|
||||
}
|
||||
})
|
||||
|
||||
getAppearance(type: T.ServiceInterfaceType = 'ui'): string {
|
||||
switch (type) {
|
||||
case 'ui':
|
||||
return 'positive'
|
||||
case 'api':
|
||||
return 'info'
|
||||
case 'p2p':
|
||||
return 'negative'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,21 @@ import { TuiCell } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PrimaryStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
const INACTIVE: PrimaryStatus[] = [
|
||||
'installing',
|
||||
'updating',
|
||||
'removing',
|
||||
'restoring',
|
||||
'backingUp',
|
||||
]
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (service()) {
|
||||
@@ -34,7 +46,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
<span tuiSubtitle>{{ manifest()?.version }}</span>
|
||||
</span>
|
||||
</header>
|
||||
<nav>
|
||||
<nav [attr.inert]="isInactive() ? '' : null">
|
||||
@for (item of nav; track $index) {
|
||||
<a
|
||||
tuiCell
|
||||
@@ -76,6 +88,10 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
margin: 0 -0.5rem;
|
||||
}
|
||||
|
||||
nav[inert] a:not(:first-child) {
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
}
|
||||
|
||||
a a {
|
||||
display: none;
|
||||
}
|
||||
@@ -178,4 +194,9 @@ export class ServiceOutletComponent {
|
||||
protected readonly manifest = computed(
|
||||
(pkg = this.service()) => pkg && getManifest(pkg),
|
||||
)
|
||||
|
||||
protected readonly isInactive = computed(
|
||||
(pkg = this.service()) =>
|
||||
!pkg || INACTIVE.includes(renderPkgStatus(pkg).primary),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
import { WaIntersectionObserver } from '@ng-web-apis/intersection-observer'
|
||||
import { i18nPipe, isEmptyObject } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiElement } from '@taiga-ui/cdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map, of } from 'rxjs'
|
||||
import { UptimeComponent } from 'src/app/routes/portal/components/uptime.component'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
import {
|
||||
@@ -27,14 +29,14 @@ import { ServiceHealthChecksComponent } from '../components/health-checks.compon
|
||||
import { ServiceInterfacesComponent } from '../components/interfaces.component'
|
||||
import { ServiceInstallProgressComponent } from '../components/progress.component'
|
||||
import { ServiceStatusComponent } from '../components/status.component'
|
||||
import { ServiceUptimeComponent } from '../components/uptime.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (pkg(); as pkg) {
|
||||
@if (pkg.status.main === 'error') {
|
||||
<service-error [pkg]="pkg" />
|
||||
}
|
||||
@if (installing()) {
|
||||
} @else if (installing()) {
|
||||
<service-install-progress [pkg]="pkg" />
|
||||
} @else if (installed()) {
|
||||
<service-status
|
||||
@@ -42,16 +44,13 @@ import { ServiceStatusComponent } from '../components/status.component'
|
||||
[installingInfo]="pkg.stateInfo.installingInfo"
|
||||
[status]="status()"
|
||||
>
|
||||
@if ($any(pkg.status)?.started; as started) {
|
||||
<p class="g-secondary" [appUptime]="started"></p>
|
||||
}
|
||||
|
||||
@if (connected()) {
|
||||
<service-controls [pkg]="pkg" [status]="status()" />
|
||||
}
|
||||
</service-status>
|
||||
|
||||
@if (status() !== 'backingUp') {
|
||||
<service-uptime [started]="$any(pkg.status)?.started" />
|
||||
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
|
||||
|
||||
@if (errors() | async; as errors) {
|
||||
@@ -63,7 +62,30 @@ import { ServiceStatusComponent } from '../components/status.component'
|
||||
}
|
||||
|
||||
<service-health-checks [checks]="health()" />
|
||||
<service-tasks [pkg]="pkg" [services]="services() || {}" />
|
||||
<service-tasks
|
||||
#tasks="elementRef"
|
||||
tuiElement
|
||||
waIntersectionObserver
|
||||
waIntersectionThreshold="0.5"
|
||||
(waIntersectionObservee)="scrolled = $event.at(-1)?.isIntersecting"
|
||||
[pkg]="pkg"
|
||||
[services]="services() || {}"
|
||||
/>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.arrow-down"
|
||||
tabindex="-1"
|
||||
class="arrow"
|
||||
[class.arrow_hidden]="scrolled"
|
||||
(click)="
|
||||
tasks.nativeElement.scrollIntoView({
|
||||
block: 'end',
|
||||
behavior: 'smooth',
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ 'Tasks' | i18n }}
|
||||
</button>
|
||||
}
|
||||
} @else if (removing()) {
|
||||
<service-status
|
||||
@@ -74,6 +96,14 @@ import { ServiceStatusComponent } from '../components/status.component'
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
|
||||
|
||||
@keyframes bounce {
|
||||
to {
|
||||
transform: translateY(-1rem);
|
||||
}
|
||||
}
|
||||
|
||||
:host {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
@@ -86,6 +116,23 @@ import { ServiceStatusComponent } from '../components/status.component'
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
@include taiga.transition(opacity);
|
||||
position: sticky;
|
||||
bottom: 1rem;
|
||||
border-radius: 100%;
|
||||
place-self: center;
|
||||
grid-area: auto / span 6;
|
||||
box-shadow: inset 0 0 0 2rem var(--tui-status-warning);
|
||||
animation: bounce 1s infinite alternate;
|
||||
|
||||
&_hidden,
|
||||
:host:has(::ng-deep service-tasks app-placeholder) & {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -99,6 +146,10 @@ import { ServiceStatusComponent } from '../components/status.component'
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
TuiElement,
|
||||
TuiButton,
|
||||
WaIntersectionObserver,
|
||||
i18nPipe,
|
||||
ServiceInstallProgressComponent,
|
||||
ServiceStatusComponent,
|
||||
ServiceControlsComponent,
|
||||
@@ -107,13 +158,15 @@ import { ServiceStatusComponent } from '../components/status.component'
|
||||
ServiceDependenciesComponent,
|
||||
ServiceErrorComponent,
|
||||
ServiceTasksComponent,
|
||||
UptimeComponent,
|
||||
ServiceUptimeComponent,
|
||||
],
|
||||
})
|
||||
export class ServiceRoute {
|
||||
private readonly errorService = inject(DepErrorService)
|
||||
protected readonly connected = toSignal(inject(ConnectionService))
|
||||
|
||||
protected scrolled?: boolean
|
||||
|
||||
protected readonly id = toSignal(
|
||||
inject(ActivatedRoute).paramMap.pipe(map(params => params.get('pkgId'))),
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { TuiCell, TuiHeader } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.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'
|
||||
@@ -89,6 +90,10 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
} @empty {
|
||||
<app-placeholder icon="@tui.shield-question">
|
||||
{{ 'No saved providers' | i18n }}
|
||||
</app-placeholder>
|
||||
}
|
||||
} @else {
|
||||
<tui-loader [style.height.rem]="5" />
|
||||
@@ -113,6 +118,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
TitleDirective,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
PlaceholderComponent,
|
||||
],
|
||||
})
|
||||
export default class SystemAcmeComponent {
|
||||
|
||||
@@ -75,7 +75,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
<button
|
||||
tuiButton
|
||||
size="l"
|
||||
[disabled]="form.invalid"
|
||||
[disabled]="form.invalid || form.pristine"
|
||||
(click)="save(form.value)"
|
||||
>
|
||||
{{ 'Save' | i18n }}
|
||||
@@ -98,7 +98,6 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
<footer>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
size="l"
|
||||
[disabled]="!testAddress || form.invalid"
|
||||
(click)="sendTestEmail(form.value)"
|
||||
@@ -188,11 +187,14 @@ export default class SystemEmailComponent {
|
||||
async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) {
|
||||
const loader = this.loader.open('Sending email').subscribe()
|
||||
const success =
|
||||
`${this.i18n.transform('A test email has been sent to')} ${this.testAddress}.<br /><br /><b>${this.i18n.transform('Check your spam folder and mark as not spam.')}</b>` as i18nKey
|
||||
`${this.i18n.transform('A test email has been sent to')} ${this.testAddress}. <i>${this.i18n.transform('Check your spam folder and mark as not spam.')}</i>` as i18nKey
|
||||
|
||||
try {
|
||||
await this.api.testSmtp({ to: this.testAddress, ...value })
|
||||
this.dialog.openAlert(success, { label: 'Success' }).subscribe()
|
||||
this.dialog
|
||||
.openAlert(success, { label: 'Success', size: 's' })
|
||||
.subscribe()
|
||||
this.testAddress = ''
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Title } from '@angular/platform-browser'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import {
|
||||
DialogService,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
TuiTitle,
|
||||
} from '@taiga-ui/core'
|
||||
import {
|
||||
TuiBadge,
|
||||
TuiButtonLoading,
|
||||
TuiButtonSelect,
|
||||
TuiDataListWrapper,
|
||||
@@ -92,7 +94,9 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
<tui-icon icon="@tui.app-window" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Browser Tab Title' | i18n }}</strong>
|
||||
<span tuiSubtitle>{{ name() }}</span>
|
||||
<span tuiSubtitle>
|
||||
{{ 'Customize the name appearing in your browser tab' | i18n }}
|
||||
</span>
|
||||
</span>
|
||||
<button tuiButton (click)="onTitle()">{{ 'Change' | i18n }}</button>
|
||||
</div>
|
||||
@@ -134,6 +138,43 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
{{ 'Download' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.monitor" />
|
||||
<span tuiTitle>
|
||||
<strong>
|
||||
{{ 'Kiosk Mode' | i18n }}
|
||||
<tui-badge
|
||||
size="m"
|
||||
[appearance]="
|
||||
server.kiosk ? 'primary-success' : 'primary-destructive'
|
||||
"
|
||||
>
|
||||
{{ server.kiosk ? ('Enabled' | i18n) : ('Disabled' | i18n) }}
|
||||
</tui-badge>
|
||||
</strong>
|
||||
<span tuiSubtitle>
|
||||
{{
|
||||
server.kiosk === true
|
||||
? ('Disable Kiosk Mode unless you need to attach a monitor'
|
||||
| i18n)
|
||||
: server.kiosk === false
|
||||
? ('Enable Kiosk Mode if you need to attach a monitor' | i18n)
|
||||
: ('Kiosk Mode is unavailable on this device' | i18n)
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
@if (server.kiosk !== null) {
|
||||
<button
|
||||
tuiButton
|
||||
[appearance]="
|
||||
server.kiosk ? 'primary-destructive' : 'primary-success'
|
||||
"
|
||||
(click)="tryToggleKiosk()"
|
||||
>
|
||||
{{ server.kiosk ? ('Disable' | i18n) : ('Enable' | i18n) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.circle-power" (click)="count = count + 1" />
|
||||
<span tuiTitle>
|
||||
@@ -190,11 +231,6 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
[tuiCell] {
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
|
||||
[tuiSubtitle],
|
||||
tui-data-list-wrapper ::ng-deep [tuiOption] {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`,
|
||||
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -217,9 +253,11 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
TuiTextfield,
|
||||
FormsModule,
|
||||
SnekDirective,
|
||||
TuiBadge,
|
||||
],
|
||||
})
|
||||
export default class SystemGeneralComponent {
|
||||
private readonly title = inject(Title)
|
||||
private readonly dialogs = inject(TuiResponsiveDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
@@ -276,9 +314,11 @@ export default class SystemGeneralComponent {
|
||||
})
|
||||
.subscribe(async name => {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
const title = `${name || 'StartOS'} — ${this.i18n.transform('System')}`
|
||||
|
||||
try {
|
||||
await this.api.setDbValue(['name'], name || null)
|
||||
this.title.setTitle(title)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
@@ -310,6 +350,28 @@ export default class SystemGeneralComponent {
|
||||
this.document.getElementById('download-ca')?.click()
|
||||
}
|
||||
|
||||
async tryToggleKiosk() {
|
||||
if (
|
||||
this.server()?.kiosk &&
|
||||
['localhost', '127.0.0.1'].includes(this.document.location.hostname)
|
||||
) {
|
||||
return this.dialog
|
||||
.openConfirm({
|
||||
label: 'Warning',
|
||||
data: {
|
||||
content:
|
||||
'You are currently using a kiosk. Disabling Kiosk Mode will result in the kiosk disconnecting.',
|
||||
yes: 'Disable',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => this.toggleKiosk())
|
||||
}
|
||||
|
||||
this.toggleKiosk()
|
||||
}
|
||||
|
||||
async onRepair() {
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
@@ -332,6 +394,22 @@ export default class SystemGeneralComponent {
|
||||
})
|
||||
}
|
||||
|
||||
private async toggleKiosk() {
|
||||
const kiosk = this.server()?.kiosk
|
||||
|
||||
const loader = this.loader
|
||||
.open(kiosk ? 'Disabling' : 'Enabling')
|
||||
.subscribe()
|
||||
|
||||
try {
|
||||
await this.api.toggleKiosk(!kiosk)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async resetTor(wipeState: boolean) {
|
||||
const loader = this.loader.open('Resetting Tor').subscribe()
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import { firstValueFrom } from 'rxjs'
|
||||
template: `
|
||||
<h2 style="margin-top: 0">StartOS {{ versions[0]?.version }}</h2>
|
||||
<h3 style="color: var(--tui-text-secondary); font-weight: normal">
|
||||
{{ 'Release Notes' | i18n }}
|
||||
{{ 'Release notes' | i18n }}
|
||||
</h3>
|
||||
<tui-scrollbar style="margin-bottom: 24px; max-height: 50vh;">
|
||||
@for (v of versions; track $index) {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
viewChild,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { from, map, merge, Observable, Subject } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { Session } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { SessionsTableComponent } from './table.component'
|
||||
|
||||
@@ -36,18 +41,16 @@ import { SessionsTableComponent } from './table.component'
|
||||
<section *tuiLet="other$ | async as others" class="g-card">
|
||||
<header>
|
||||
{{ 'Other sessions' | i18n }}
|
||||
@if (table.selected$ | async; as selected) {
|
||||
<button
|
||||
tuiButton
|
||||
size="xs"
|
||||
appearance="primary-destructive"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
[disabled]="!selected.length"
|
||||
(click)="terminate(selected, others || [])"
|
||||
>
|
||||
{{ 'Terminate selected' | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
tuiButton
|
||||
size="xs"
|
||||
appearance="primary-destructive"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
[disabled]="!(sessions()?.selected$ | async)?.length"
|
||||
(click)="terminate(others || [])"
|
||||
>
|
||||
{{ 'Terminate selected' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
<div #table [sessions]="others"></div>
|
||||
</section>
|
||||
@@ -73,6 +76,8 @@ export default class SystemSessionsComponent {
|
||||
private readonly sessions$ = from(this.api.getSessions({}))
|
||||
private readonly local$ = new Subject<readonly SessionWithId[]>()
|
||||
|
||||
protected sessions = viewChild<SessionsTableComponent<SessionWithId>>('table')
|
||||
|
||||
readonly current$ = this.sessions$.pipe(
|
||||
map(s => {
|
||||
const current = s.sessions[s.current]
|
||||
@@ -99,16 +104,14 @@ export default class SystemSessionsComponent {
|
||||
),
|
||||
)
|
||||
|
||||
async terminate(
|
||||
sessions: readonly SessionWithId[],
|
||||
all: readonly SessionWithId[],
|
||||
) {
|
||||
const ids = sessions.map(s => s.id)
|
||||
async terminate(all: readonly SessionWithId[]) {
|
||||
const ids = this.sessions()?.selected$.value.map(s => s.id) || []
|
||||
const loader = this.loader.open('Terminating sessions').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.killSessions({ ids })
|
||||
this.local$.next(all.filter(s => !ids.includes(s.id)))
|
||||
this.sessions()?.selected$.next([])
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -31,7 +31,7 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
(ngModelChange)="onToggle(session)"
|
||||
/>
|
||||
}
|
||||
<span tuiFade class="agent">{{ session.userAgent }}</span>
|
||||
<span tuiFade class="agent">{{ session.userAgent || '-' }}</span>
|
||||
</label>
|
||||
</td>
|
||||
@if (session.userAgent | platformInfo; as platform) {
|
||||
|
||||
@@ -25,11 +25,11 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
|
||||
{{ 'Back' | i18n }}
|
||||
</a>
|
||||
StartOS UI
|
||||
<interface-status [public]="public()" />
|
||||
{{ iface.name }}
|
||||
<interface-status [style.margin-left.rem]="0.5" [public]="public()" />
|
||||
</ng-container>
|
||||
<header tuiHeader>
|
||||
<hgroup tuiTitle>
|
||||
<hgroup>
|
||||
<h3>
|
||||
{{ iface.name }}
|
||||
<interface-status [public]="public()" />
|
||||
@@ -38,9 +38,24 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
</hgroup>
|
||||
</header>
|
||||
@if (ui(); as ui) {
|
||||
<app-interface [serviceInterface]="ui" />
|
||||
<app-interface [value]="ui" [isRunning]="true" />
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
font-size: 2.4rem;
|
||||
|
||||
tui-badge {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-subpage' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
@@ -49,7 +64,6 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
TuiButton,
|
||||
TitleDirective,
|
||||
TuiHeader,
|
||||
TuiTitle,
|
||||
InterfaceStatusComponent,
|
||||
i18nPipe,
|
||||
],
|
||||
@@ -149,7 +149,7 @@ export default class SystemWifiComponent {
|
||||
|
||||
async onToggle(enable: boolean) {
|
||||
const loader = this.loader
|
||||
.open(enable ? 'Enabling WiFi' : 'Disabling WiFi')
|
||||
.open(enable ? 'Enabling' : 'Disabling')
|
||||
.subscribe()
|
||||
|
||||
try {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { BadgeService } from 'src/app/services/badge.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { SYSTEM_MENU } from './system.const'
|
||||
import { map } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -128,10 +129,7 @@ import { SYSTEM_MENU } from './system.const'
|
||||
export class SystemComponent {
|
||||
readonly menu = SYSTEM_MENU
|
||||
readonly badge = toSignal(inject(BadgeService).getCount('/portal/system'))
|
||||
readonly wifiEnabled$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
|
||||
'serverInfo',
|
||||
'network',
|
||||
'wifi',
|
||||
'enabled',
|
||||
)
|
||||
readonly wifiEnabled$ = inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('serverInfo', 'network', 'wifi')
|
||||
.pipe(map(wifi => !!wifi.interface && wifi.enabled))
|
||||
}
|
||||
|
||||
@@ -7,11 +7,6 @@ export const SYSTEM_MENU = [
|
||||
item: 'General',
|
||||
link: 'general',
|
||||
},
|
||||
{
|
||||
icon: '@tui.mail',
|
||||
item: 'Email',
|
||||
link: 'email',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -36,6 +31,11 @@ export const SYSTEM_MENU = [
|
||||
item: 'ACME',
|
||||
link: 'acme',
|
||||
},
|
||||
{
|
||||
icon: '@tui.mail',
|
||||
item: 'Email',
|
||||
link: 'email',
|
||||
},
|
||||
{
|
||||
icon: '@tui.wifi',
|
||||
item: 'WiFi',
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Routes,
|
||||
} from '@angular/router'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
import { titleResolver } from 'src/app/utils/title-resolver'
|
||||
import { SystemComponent } from './system.component'
|
||||
|
||||
export default [
|
||||
@@ -21,40 +22,49 @@ export default [
|
||||
children: [
|
||||
{
|
||||
path: 'general',
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/general/general.component'),
|
||||
},
|
||||
{
|
||||
path: 'email',
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/email/email.component'),
|
||||
},
|
||||
{
|
||||
path: 'backup',
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/backups/backups.component'),
|
||||
data: { type: 'create' },
|
||||
},
|
||||
{
|
||||
path: 'restore',
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/backups/backups.component'),
|
||||
data: { type: 'restore' },
|
||||
},
|
||||
{
|
||||
path: 'interfaces',
|
||||
loadComponent: () => import('./routes/interfaces/interfaces.component'),
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/startos-ui/startos-ui.component'),
|
||||
},
|
||||
{
|
||||
path: 'acme',
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/acme/acme.component'),
|
||||
},
|
||||
{
|
||||
path: 'wifi',
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/wifi/wifi.component'),
|
||||
},
|
||||
{
|
||||
path: 'sessions',
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/sessions/sessions.component'),
|
||||
},
|
||||
{
|
||||
path: 'password',
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/password/password.component'),
|
||||
},
|
||||
// {
|
||||
|
||||
@@ -212,6 +212,11 @@ import UpdatesComponent from './updates.component'
|
||||
|
||||
.mobile {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
||||
[tuiSubtitle] {
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map, tap } from 'rxjs'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import {
|
||||
@@ -112,7 +113,9 @@ interface UpdatesData {
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
{{ 'All services are up to date!' | i18n }}
|
||||
<app-placeholder icon="@tui.circle-check">
|
||||
{{ 'All services are up to date!' | i18n }}
|
||||
</app-placeholder>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -161,6 +164,11 @@ interface UpdatesData {
|
||||
clip-path: inset(0.5rem round var(--tui-radius-s));
|
||||
}
|
||||
|
||||
.g-subpage,
|
||||
.g-card {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
aside {
|
||||
width: 100%;
|
||||
@@ -214,6 +222,7 @@ interface UpdatesData {
|
||||
TitleDirective,
|
||||
TableComponent,
|
||||
i18nPipe,
|
||||
PlaceholderComponent,
|
||||
],
|
||||
})
|
||||
export default class UpdatesComponent {
|
||||
@@ -224,7 +233,7 @@ export default class UpdatesComponent {
|
||||
|
||||
readonly data = toSignal<UpdatesData>(
|
||||
combineLatest({
|
||||
hosts: this.marketplaceService.filteredRegistries$.pipe(
|
||||
hosts: this.marketplaceService.registries$.pipe(
|
||||
tap(
|
||||
([registry]) =>
|
||||
!this.isMobile && registry && this.current.set(registry),
|
||||
|
||||
@@ -27,7 +27,7 @@ const routes: Routes = [
|
||||
loadChildren: () => import('./routes/portal/portal.routes'),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
path: '**',
|
||||
redirectTo: 'portal',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ const mockMerkleArchiveCommitment: T.MerkleArchiveCommitment = {
|
||||
|
||||
const mockDescription = {
|
||||
short: 'Lorem ipsum dolor sit amet',
|
||||
long: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
|
||||
long: 'Lorem ipsum dolor sit amet, <p>consectetur adipiscing elit</p>, sed do eiusmod <i>tempor</i> incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
|
||||
}
|
||||
|
||||
export namespace Mock {
|
||||
@@ -632,55 +632,6 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
'=0.3.2.6:0': {
|
||||
best: {
|
||||
'0.3.2.6:0': {
|
||||
title: 'Bitcoin Proxy',
|
||||
description: mockDescription,
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
|
||||
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
|
||||
supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues',
|
||||
marketingSite: '',
|
||||
releaseNotes: 'Upstream release and minor fixes.',
|
||||
osVersion: '0.3.6',
|
||||
gitHash: 'fakehash',
|
||||
icon: PROXY_ICON,
|
||||
sourceVersion: null,
|
||||
dependencyMetadata: {
|
||||
bitcoind: {
|
||||
title: 'Bitcoin Core',
|
||||
icon: BTC_ICON,
|
||||
description: 'Used for RPC requests',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
donationUrl: null,
|
||||
alerts: {
|
||||
install: 'test',
|
||||
uninstall: 'test',
|
||||
start: 'test',
|
||||
stop: 'test',
|
||||
restore: 'test',
|
||||
},
|
||||
s9pk: {
|
||||
url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7.1/btc-rpc-proxy.s9pk',
|
||||
commitment: mockMerkleArchiveCommitment,
|
||||
signatures: {},
|
||||
publishedAt: Date.now().toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
categories: ['bitcoin'],
|
||||
otherVersions: {
|
||||
'0.3.2.7:0': {
|
||||
releaseNotes: 'Upstream release and minor fixes.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const RegistryPackages: GetPackagesRes = {
|
||||
@@ -857,11 +808,7 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
categories: ['bitcoin'],
|
||||
otherVersions: {
|
||||
'0.3.2.6:0': {
|
||||
releaseNotes: 'Upstream release and minor fixes.',
|
||||
},
|
||||
},
|
||||
otherVersions: {},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -891,7 +838,7 @@ export namespace Mock {
|
||||
id: 2,
|
||||
packageId: null,
|
||||
createdAt: '2019-12-26T14:20:30.872Z',
|
||||
code: 2,
|
||||
code: 0,
|
||||
level: 'warning',
|
||||
title: 'SSH Key Added',
|
||||
message: 'A new SSH key was added. If you did not do this, shit is bad.',
|
||||
@@ -902,7 +849,7 @@ export namespace Mock {
|
||||
id: 3,
|
||||
packageId: null,
|
||||
createdAt: '2019-12-26T14:20:30.872Z',
|
||||
code: 3,
|
||||
code: 0,
|
||||
level: 'info',
|
||||
title: 'SSH Key Removed',
|
||||
message: 'A SSH key was removed.',
|
||||
@@ -913,7 +860,7 @@ export namespace Mock {
|
||||
id: 4,
|
||||
packageId: 'bitcoind',
|
||||
createdAt: '2019-12-26T14:20:30.872Z',
|
||||
code: 4,
|
||||
code: 0,
|
||||
level: 'error',
|
||||
title: 'Service Crashed',
|
||||
message: new Array(3)
|
||||
@@ -1339,7 +1286,7 @@ export namespace Mock {
|
||||
result: {
|
||||
type: 'single',
|
||||
copyable: true,
|
||||
qr: true,
|
||||
qr: false,
|
||||
masked: true,
|
||||
value: 'iwejdoiewdhbew',
|
||||
},
|
||||
|
||||
@@ -335,6 +335,12 @@ export namespace RR {
|
||||
} // package.action.run
|
||||
export type ActionRes = (T.ActionResult & { version: '1' }) | null
|
||||
|
||||
export type ClearTaskReq = {
|
||||
packageId: string
|
||||
replayId: string
|
||||
} // package.action.clear-task
|
||||
export type ClearTaskRes = null
|
||||
|
||||
export type RestorePackagesReq = {
|
||||
// package.backup.restore
|
||||
ids: string[]
|
||||
@@ -356,7 +362,11 @@ export namespace RR {
|
||||
export type RebuildPackageReq = { id: string } // package.rebuild
|
||||
export type RebuildPackageRes = null
|
||||
|
||||
export type UninstallPackageReq = { id: string } // package.uninstall
|
||||
export type UninstallPackageReq = {
|
||||
id: string
|
||||
force: boolean
|
||||
soft: boolean
|
||||
} // package.uninstall
|
||||
export type UninstallPackageRes = null
|
||||
|
||||
export type SideloadPackageReq = {
|
||||
|
||||
@@ -120,6 +120,8 @@ export abstract class ApiService {
|
||||
|
||||
abstract repairDisk(params: RR.DiskRepairReq): Promise<RR.DiskRepairRes>
|
||||
|
||||
abstract toggleKiosk(enable: boolean): Promise<null>
|
||||
|
||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
||||
|
||||
// @TODO 041
|
||||
@@ -335,6 +337,8 @@ export abstract class ApiService {
|
||||
|
||||
abstract runAction(params: RR.ActionReq): Promise<RR.ActionRes>
|
||||
|
||||
abstract clearTask(params: RR.ClearTaskReq): Promise<RR.ClearTaskRes>
|
||||
|
||||
abstract restorePackages(
|
||||
params: RR.RestorePackagesReq,
|
||||
): Promise<RR.RestorePackagesRes>
|
||||
|
||||
@@ -261,6 +261,13 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'disk.repair', params })
|
||||
}
|
||||
|
||||
async toggleKiosk(enable: boolean): Promise<null> {
|
||||
return this.rpcRequest({
|
||||
method: enable ? 'kiosk.enable' : 'kiosk.disable',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
|
||||
return this.rpcRequest({ method: 'net.tor.reset', params })
|
||||
}
|
||||
@@ -577,6 +584,10 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.action.run', params })
|
||||
}
|
||||
|
||||
async clearTask(params: RR.ClearTaskReq): Promise<RR.ClearTaskRes> {
|
||||
return this.rpcRequest({ method: 'package.action.clear-task', params })
|
||||
}
|
||||
|
||||
async restorePackages(
|
||||
params: RR.RestorePackagesReq,
|
||||
): Promise<RR.RestorePackagesRes> {
|
||||
|
||||
@@ -22,11 +22,7 @@ import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
GetPackageRes,
|
||||
GetPackagesRes,
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||
import { toAcmeUrl } from 'src/app/utils/acme'
|
||||
@@ -166,7 +162,6 @@ export class MockApiService extends ApiService {
|
||||
pathArr: Array<string | number>,
|
||||
value: T,
|
||||
): Promise<RR.SetDBValueRes> {
|
||||
console.warn(pathArr, value)
|
||||
const pointer = pathFromArray(pathArr)
|
||||
const params: RR.SetDBValueReq<T> = { pointer, value }
|
||||
await pauseFor(2000)
|
||||
@@ -449,6 +444,21 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async toggleKiosk(enable: boolean): Promise<null> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/kiosk',
|
||||
value: enable,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
@@ -1103,23 +1113,32 @@ export class MockApiService extends ApiService {
|
||||
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
if (params.actionId === 'properties') {
|
||||
// return Mock.ActionResGroup
|
||||
return Mock.ActionResMessage
|
||||
// return Mock.ActionResSingle
|
||||
} else if (params.actionId === 'config') {
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.packageId}/requestedActions/${params.packageId}-config`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
return null
|
||||
} else {
|
||||
return Mock.ActionResMessage
|
||||
// return Mock.ActionResSingle
|
||||
}
|
||||
const patch: ReplaceOperation<{ [key: string]: T.TaskEntry }>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.packageId}/tasks`,
|
||||
value: {},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
// return Mock.ActionResGroup
|
||||
return Mock.ActionResMessage
|
||||
// return Mock.ActionResSingle
|
||||
}
|
||||
|
||||
async clearTask(params: RR.ClearTaskReq): Promise<RR.ClearTaskRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.packageId}/tasks/${params.replayId}`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async restorePackages(
|
||||
|
||||
@@ -183,13 +183,7 @@ export const mockPatchData: DataModel = {
|
||||
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
||||
caFingerprint: '63:2B:11:99:44:40:17:DF:37:FC:C3:DF:0F:3D:15',
|
||||
ntpSynced: false,
|
||||
smtp: {
|
||||
server: '',
|
||||
port: 587,
|
||||
from: '',
|
||||
login: '',
|
||||
password: '',
|
||||
},
|
||||
smtp: null,
|
||||
platform: 'x86_64-nonfree',
|
||||
zram: true,
|
||||
governor: 'performance',
|
||||
@@ -221,7 +215,7 @@ export const mockPatchData: DataModel = {
|
||||
actions: {
|
||||
config: {
|
||||
name: 'Set Config',
|
||||
description: 'edit bitcoin.conf',
|
||||
description: 'edit bitcoin.conf, <b>soo cool!</b>',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { ReplaySubject } from 'rxjs'
|
||||
import { StorageService } from './storage.service'
|
||||
|
||||
const SHOW_DEV_TOOLS = 'SHOW_DEV_TOOLS'
|
||||
const SHOW_DISK_REPAIR = 'SHOW_DISK_REPAIR'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ClientStorageService {
|
||||
private readonly storage = inject(StorageService)
|
||||
readonly showDevTools$ = new ReplaySubject<boolean>(1)
|
||||
readonly showDiskRepair$ = new ReplaySubject<boolean>(1)
|
||||
|
||||
constructor(private readonly storage: StorageService) {}
|
||||
|
||||
init() {
|
||||
this.showDevTools$.next(!!this.storage.get(SHOW_DEV_TOOLS))
|
||||
this.showDiskRepair$.next(!!this.storage.get(SHOW_DISK_REPAIR))
|
||||
}
|
||||
|
||||
toggleShowDevTools(): boolean {
|
||||
@@ -25,11 +21,4 @@ export class ClientStorageService {
|
||||
this.showDevTools$.next(newVal)
|
||||
return newVal
|
||||
}
|
||||
|
||||
toggleShowDiskRepair(): boolean {
|
||||
const newVal = !this.storage.get(SHOW_DISK_REPAIR)
|
||||
this.storage.set(SHOW_DISK_REPAIR, newVal)
|
||||
this.showDiskRepair$.next(newVal)
|
||||
return newVal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { ClientStorageService } from './client-storage.service'
|
||||
|
||||
const { start9, community } = defaultRegistries
|
||||
|
||||
@@ -55,20 +54,6 @@ export class MarketplaceService {
|
||||
]),
|
||||
)
|
||||
|
||||
// option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL
|
||||
readonly filteredRegistries$: Observable<StoreIdentity[]> = combineLatest([
|
||||
inject(ClientStorageService).showDevTools$,
|
||||
this.registries$,
|
||||
]).pipe(
|
||||
map(([devMode, registries]) =>
|
||||
devMode
|
||||
? registries
|
||||
: registries.filter(
|
||||
({ url }) => !url.includes('alpha') && !url.includes('beta'),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
readonly currentRegistryUrl$ = new ReplaySubject<string>(1)
|
||||
|
||||
readonly requestErrors$ = new BehaviorSubject<string[]>([])
|
||||
@@ -252,7 +237,6 @@ export class MarketplaceService {
|
||||
oldName: string | null,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
console.warn(oldName, newName)
|
||||
if (oldName !== newName) {
|
||||
this.api.setDbValue<string>(['registries', url], newName)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export class NotificationService {
|
||||
{ data, createdAt, code, title, message }: ServerNotification<number>,
|
||||
full = false,
|
||||
) {
|
||||
const label = full || code === 2 ? title : 'Backup Report'
|
||||
const label = code === 1 ? 'Backup Report' : title
|
||||
const component = code === 1 ? REPORT : MARKDOWN
|
||||
const content = code === 1 ? data : of(data)
|
||||
|
||||
@@ -104,6 +104,7 @@ export class NotificationService {
|
||||
content,
|
||||
timestamp: createdAt,
|
||||
},
|
||||
size: code === 1 ? 'm' : 'l',
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user