chore: comments (#2863)

This commit is contained in:
Alex Inkin
2025-04-06 17:18:01 +04:00
committed by GitHub
parent f51dcf23d6
commit 31856d9895
45 changed files with 362 additions and 503 deletions

View File

@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { Exver, MarkdownPipeModule } from '@start9labs/shared' import { Exver, MarkdownPipe } from '@start9labs/shared'
import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core' import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core'
import { TuiAccordion } from '@taiga-ui/kit' import { TuiAccordion } from '@taiga-ui/kit'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@@ -21,13 +21,7 @@ import { MarketplacePkg } from '../../src/types'
</tui-accordion> </tui-accordion>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [CommonModule, TuiButton, TuiLoader, TuiAccordion, MarkdownPipe],
CommonModule,
TuiButton,
TuiLoader,
TuiAccordion,
MarkdownPipeModule,
],
}) })
export class ReleaseNotesComponent { export class ReleaseNotesComponent {
private readonly exver = inject(Exver) private readonly exver = inject(Exver)

View File

@@ -5,7 +5,7 @@ import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { AboutComponent } from './about.component' import { AboutComponent } from './about.component'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { MarkdownPipeModule, SafeLinksDirective } from '@start9labs/shared' import { MarkdownPipe, SafeLinksDirective } from '@start9labs/shared'
@NgModule({ @NgModule({
imports: [ imports: [
@@ -14,7 +14,7 @@ import { MarkdownPipeModule, SafeLinksDirective } from '@start9labs/shared'
TuiTagModule, TuiTagModule,
NgDompurifyModule, NgDompurifyModule,
SafeLinksDirective, SafeLinksDirective,
MarkdownPipeModule, MarkdownPipe,
TuiButton, TuiButton,
], ],
declarations: [AboutComponent], declarations: [AboutComponent],

View File

@@ -1,14 +1,13 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute, Data } from '@angular/router'
import { import { TuiDialogContext, TuiLoader, TuiNotification } from '@taiga-ui/core'
getErrorMessage, import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
MarkdownPipeModule,
SafeLinksDirective,
} from '@start9labs/shared'
import { TuiLoader, TuiNotification } from '@taiga-ui/core'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { catchError, ignoreElements, of } from 'rxjs' import { catchError, ignoreElements, of } from 'rxjs'
import { SafeLinksDirective } from '../directives/safe-links.directive'
import { MarkdownPipe } from '../pipes/markdown.pipe'
import { getErrorMessage } from '../services/error.service'
@Component({ @Component({
template: ` template: `
@@ -21,7 +20,7 @@ import { catchError, ignoreElements, of } from 'rxjs'
@if (content(); as result) { @if (content(); as result) {
<div safeLinks [innerHTML]="result | markdown | dompurify"></div> <div safeLinks [innerHTML]="result | markdown | dompurify"></div>
} @else { } @else {
<tui-loader textContent="Loading" /> <tui-loader textContent="Loading" [style.height.%]="100" />
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -30,13 +29,15 @@ import { catchError, ignoreElements, of } from 'rxjs'
imports: [ imports: [
TuiNotification, TuiNotification,
TuiLoader, TuiLoader,
MarkdownPipeModule,
NgDompurifyModule, NgDompurifyModule,
MarkdownPipe,
SafeLinksDirective, SafeLinksDirective,
], ],
}) })
export default class ServiceMarkdownRoute { export class MarkdownComponent {
private readonly data = inject(ActivatedRoute).snapshot.data private readonly data =
injectContext<TuiDialogContext<void, Data>>({ optional: true })?.data ||
inject(ActivatedRoute).snapshot.data
readonly content = toSignal<string>(this.data['content']) readonly content = toSignal<string>(this.data['content'])
readonly error = toSignal( readonly error = toSignal(
@@ -46,3 +47,5 @@ export default class ServiceMarkdownRoute {
), ),
) )
} }
export const MARKDOWN = new PolymorpheusComponent(MarkdownComponent)

View File

@@ -1,18 +0,0 @@
<tui-notification
*ngIf="error$ | async as error"
appearance="negative"
safeLinks
>
{{ error }}
</tui-notification>
<div
*ngIf="content$ | async as result; else loading"
safeLinks
class="content-padding"
[innerHTML]="result | markdown | dompurify"
></div>
<ng-template #loading>
<tui-loader [textContent]="'Loading ' + title | titlecase" />
</ng-template>

View File

@@ -1,22 +0,0 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { TuiLoader, TuiNotification } from '@taiga-ui/core'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { SafeLinksDirective } from '../../directives/safe-links.directive'
import { MarkdownPipeModule } from '../../pipes/markdown/markdown.module'
import { MarkdownComponent } from './markdown.component'
@NgModule({
declarations: [MarkdownComponent],
imports: [
CommonModule,
MarkdownPipeModule,
SafeLinksDirective,
NgDompurifyModule,
TuiLoader,
TuiNotification,
],
exports: [MarkdownComponent],
})
export class MarkdownModule {}

View File

@@ -1,18 +0,0 @@
.content-padding {
padding: 0 16px 16px 16px;
}
:host ::ng-deep img {
border-radius: 0 !important;
}
:host ::ng-deep h1,
:host ::ng-deep h2,
:host ::ng-deep h3,
:host ::ng-deep h4,
:host ::ng-deep h5,
:host ::ng-deep h6,
:host ::ng-deep hr,
:host ::ng-deep p {
margin: revert;
}

View File

@@ -1,49 +0,0 @@
import { Component, Inject } from '@angular/core'
import { TuiDialogContext } from '@taiga-ui/core'
import {
POLYMORPHEUS_CONTEXT,
PolymorpheusComponent,
} from '@taiga-ui/polymorpheus'
import {
catchError,
ignoreElements,
share,
defer,
isObservable,
Observable,
of,
} from 'rxjs'
import { getErrorMessage } from '../../services/error.service'
@Component({
selector: 'markdown',
templateUrl: './markdown.component.html',
styleUrls: ['./markdown.component.scss'],
})
export class MarkdownComponent {
readonly content$ = defer(() =>
isObservable(this.context.data.content)
? this.context.data.content
: of(this.context.data.content),
).pipe(share())
readonly error$ = this.content$.pipe(
ignoreElements(),
catchError(e => of(getErrorMessage(e))),
)
constructor(
@Inject(POLYMORPHEUS_CONTEXT)
private readonly context: TuiDialogContext<
void,
{ content: string | Observable<string> }
>,
) {}
get title(): string {
return this.context.label || ''
}
}
export const MARKDOWN = new PolymorpheusComponent(MarkdownComponent)

View File

@@ -2,6 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core'
import { marked } from 'marked' import { marked } from 'marked'
@Pipe({ @Pipe({
standalone: true,
name: 'markdown', name: 'markdown',
}) })
export class MarkdownPipe implements PipeTransform { export class MarkdownPipe implements PipeTransform {

View File

@@ -1,8 +0,0 @@
import { NgModule } from '@angular/core'
import { MarkdownPipe } from './markdown.pipe'
@NgModule({
declarations: [MarkdownPipe],
exports: [MarkdownPipe],
})
export class MarkdownPipeModule {}

View File

@@ -10,11 +10,10 @@ export * from './components/initializing/initializing.component'
export * from './components/loading/loading.component' export * from './components/loading/loading.component'
export * from './components/loading/loading.component' export * from './components/loading/loading.component'
export * from './components/loading/loading.service' export * from './components/loading/loading.service'
export * from './components/markdown/markdown.component'
export * from './components/markdown/markdown.component.module'
export * from './components/ticker/ticker.component' export * from './components/ticker/ticker.component'
export * from './components/ticker/ticker.module' export * from './components/ticker/ticker.module'
export * from './components/drive.component' export * from './components/drive.component'
export * from './components/markdown.component'
export * from './components/server.component' export * from './components/server.component'
export * from './directives/drag-scroller.directive' export * from './directives/drag-scroller.directive'
@@ -22,14 +21,13 @@ export * from './directives/safe-links.directive'
export * from './pipes/exver/exver.module' export * from './pipes/exver/exver.module'
export * from './pipes/exver/exver.pipe' export * from './pipes/exver/exver.pipe'
export * from './pipes/markdown/markdown.module'
export * from './pipes/markdown/markdown.pipe'
export * from './pipes/shared/shared.module' export * from './pipes/shared/shared.module'
export * from './pipes/shared/empty.pipe' export * from './pipes/shared/empty.pipe'
export * from './pipes/shared/includes.pipe' export * from './pipes/shared/includes.pipe'
export * from './pipes/shared/trust.pipe' export * from './pipes/shared/trust.pipe'
export * from './pipes/unit-conversion/unit-conversion.module' export * from './pipes/unit-conversion/unit-conversion.module'
export * from './pipes/unit-conversion/unit-conversion.pipe' export * from './pipes/unit-conversion/unit-conversion.pipe'
export * from './pipes/markdown.pipe'
export * from './services/copy.service' export * from './services/copy.service'
export * from './services/download-html.service' export * from './services/download-html.service'

View File

@@ -1,55 +1,47 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component } from '@angular/core'
import { TuiDialogContext, TuiIcon } from '@taiga-ui/core' import { TuiDialogContext, TuiIcon, TuiTitle } from '@taiga-ui/core'
import { import { TuiCell } from '@taiga-ui/layout'
POLYMORPHEUS_CONTEXT, import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
PolymorpheusComponent,
} from '@taiga-ui/polymorpheus'
import { BackupReport } from 'src/app/services/api/api.types' import { BackupReport } from 'src/app/services/api/api.types'
@Component({ @Component({
template: ` template: `
<h3 class="g-title">Completed: {{ timestamp | date: 'medium' }}</h3> <h3 class="g-title">Completed: {{ data.createdAt | date: 'medium' }}</h3>
<div class="g-action"> <div tuiCell>
<div [style.flex]="1"> <div tuiTitle>
<strong>System data</strong> <strong>System data</strong>
<div [style.color]="system.color">{{ system.result }}</div> <div tuiSubtitle [style.color]="system.color">{{ system.result }}</div>
</div> </div>
<tui-icon [icon]="system.icon" [style.color]="system.color" /> <tui-icon [icon]="system.icon" [style.color]="system.color" />
</div> </div>
<div *ngFor="let pkg of report?.packages | keyvalue" class="g-action"> @for (pkg of data.content.packages | keyvalue; track $index) {
<div [style.flex]="1"> <div tuiCell>
<strong>{{ pkg.key }}</strong> <div tuiTitle>
<div [style.color]="getColor(pkg.value.error)"> <strong>{{ pkg.key }}</strong>
{{ pkg.value.error ? 'Failed: ' + pkg.value.error : 'Succeeded' }} <div tuiSubtitle [style.color]="getColor(pkg.value.error)">
{{ pkg.value.error ? 'Failed: ' + pkg.value.error : 'Succeeded' }}
</div>
</div> </div>
<tui-icon
[icon]="getIcon(pkg.value.error)"
[style.color]="getColor(pkg.value.error)"
/>
</div> </div>
<tui-icon }
[icon]="getIcon(pkg.value.error)"
[style.color]="getColor(pkg.value.error)"
/>
</div>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [CommonModule, TuiIcon], imports: [CommonModule, TuiIcon, TuiCell, TuiTitle],
}) })
export class BackupsReportModal { export class BackupsReportModal {
private readonly context = readonly data =
inject< injectContext<
TuiDialogContext<void, { content: BackupReport; timestamp: string }> TuiDialogContext<void, { content: BackupReport; createdAt: string }>
>(POLYMORPHEUS_CONTEXT) >().data
readonly system = this.getSystem() readonly system = this.getSystem()
get report(): BackupReport {
return this.context.data.content
}
get timestamp(): string {
return this.context.data.timestamp
}
getColor(error: unknown) { getColor(error: unknown) {
return error ? 'var(--tui-text-negative)' : 'var(--tui-text-positive)' return error ? 'var(--tui-text-negative)' : 'var(--tui-text-positive)'
} }
@@ -59,7 +51,7 @@ export class BackupsReportModal {
} }
private getSystem() { private getSystem() {
if (!this.report.server.attempted) { if (!this.data.content.server.attempted) {
return { return {
result: 'Not Attempted', result: 'Not Attempted',
icon: '@tui.minus', icon: '@tui.minus',
@@ -67,9 +59,9 @@ export class BackupsReportModal {
} }
} }
if (this.report.server.error) { if (this.data.content.server.error) {
return { return {
result: `Failed: ${this.report.server.error}`, result: `Failed: ${this.data.content.server.error}`,
icon: '@tui.circle-minus', icon: '@tui.circle-minus',
color: 'var(--tui-text-negative)', color: 'var(--tui-text-negative)',
} }

View File

@@ -44,11 +44,6 @@ export default {
check: 'Check for updates', check: 'Check for updates',
}, },
}, },
sync: {
title: 'Clock sync failure',
subtitle:
'This will cause connectivity issues. To resolve it, refer to the',
},
}, },
}, },
} }

View File

@@ -46,11 +46,6 @@ export default {
check: 'Buscar actualizaciones', check: 'Buscar actualizaciones',
}, },
}, },
sync: {
title: 'Fallo en la sincronización del reloj',
subtitle:
'Esto causará problemas de conectividad. Para resolverlo, consulta la',
},
}, },
}, },
} satisfies i18n } satisfies i18n

View File

@@ -6,13 +6,9 @@ import {
viewChild, viewChild,
ViewContainerRef, ViewContainerRef,
} from '@angular/core' } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleService } from 'src/app/services/title.service' import { TitleService } from 'src/app/services/title.service'
import { HeaderMenuComponent } from './menu.component' import { HeaderMenuComponent } from './menu.component'
import { HeaderNavigationComponent } from './navigation.component' import { HeaderNavigationComponent } from './navigation.component'
import { HeaderSnekDirective } from './snek.directive'
import { HeaderStatusComponent } from './status.component' import { HeaderStatusComponent } from './status.component'
@Component({ @Component({
@@ -21,12 +17,6 @@ import { HeaderStatusComponent } from './status.component'
<header-navigation /> <header-navigation />
<div class="item item_center"> <div class="item item_center">
<div class="mobile"><ng-container #vcr /></div> <div class="mobile"><ng-container #vcr /></div>
<img
[appSnek]="snekScore()"
class="snek"
alt="Play Snake"
src="assets/img/icons/snek.png"
/>
</div> </div>
<header-status class="item item_connection" /> <header-status class="item item_connection" />
<header-menu class="item item_corner" /> <header-menu class="item item_corner" />
@@ -105,19 +95,6 @@ import { HeaderStatusComponent } from './status.component'
} }
} }
.snek {
@include center-top();
@include transition(opacity);
right: 2rem;
width: 1rem;
opacity: 0.2;
cursor: pointer;
&:hover {
opacity: 1;
}
}
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
.item_center::before { .item_center::before {
left: -2rem; left: -2rem;
@@ -144,7 +121,6 @@ import { HeaderStatusComponent } from './status.component'
imports: [ imports: [
HeaderStatusComponent, HeaderStatusComponent,
HeaderNavigationComponent, HeaderNavigationComponent,
HeaderSnekDirective,
HeaderMenuComponent, HeaderMenuComponent,
], ],
}) })
@@ -152,15 +128,6 @@ export class HeaderComponent implements OnInit {
private readonly title = inject(TitleService) private readonly title = inject(TitleService)
readonly vcr = viewChild.required('vcr', { read: ViewContainerRef }) readonly vcr = viewChild.required('vcr', { read: ViewContainerRef })
readonly snekScore = toSignal(
inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui',
'gaming',
'snake',
'highScore',
),
{ initialValue: 0 },
)
ngOnInit() { ngOnInit() {
this.title.register(this.vcr()) this.title.register(this.vcr())

View File

@@ -63,7 +63,7 @@ type ClearnetForm = {
</ng-template> </ng-template>
<button <button
tuiButton tuiButton
appearance="accent" [appearance]="isPublic() ? 'primary-destructive' : 'accent'"
[iconStart]="isPublic() ? '@tui.globe-lock' : '@tui.globe'" [iconStart]="isPublic() ? '@tui.globe-lock' : '@tui.globe'"
[style.margin-inline-start]="'auto'" [style.margin-inline-start]="'auto'"
(click)="toggle()" (click)="toggle()"

View File

@@ -6,8 +6,20 @@ import { tuiInjectElement } from '@taiga-ui/cdk'
selector: '[appUptime]', selector: '[appUptime]',
template: '', template: '',
styles: ` styles: `
:host {
&::before {
content: 'Uptime: ';
}
&:empty::after {
content: '-';
}
}
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
display: none; grid-row: 2;
margin: 0;
font: var(--tui-font-text-ui-s);
} }
`, `,
}) })
@@ -22,7 +34,7 @@ export class UptimeComponent implements OnChanges, OnDestroy {
clearInterval(this.interval) clearInterval(this.interval)
if (!this.appUptime) { if (!this.appUptime) {
this.el.textContent = '-' this.el.textContent = ''
} else { } else {
this.el.textContent = uptime(new Date(this.appUptime)) this.el.textContent = uptime(new Date(this.appUptime))
this.interval = setInterval(() => { this.interval = setInterval(() => {

View File

@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { TuiDialogService, TuiIcon } from '@taiga-ui/core' import { TuiDialogService, TuiIcon, TuiTitle } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout'
import { BackupsUpcomingComponent } from './components/upcoming.component' import { BackupsUpcomingComponent } from './components/upcoming.component'
import { HISTORY } from './modals/history.component' import { HISTORY } from './modals/history.component'
import { JOBS } from './modals/jobs.component' import { JOBS } from './modals/jobs.component'
@@ -12,12 +13,12 @@ import { BackupsRestoreService } from './services/restore.service'
<section> <section>
<h3 class="g-title">Options</h3> <h3 class="g-title">Options</h3>
@for (option of options; track $index) { @for (option of options; track $index) {
<button class="g-action" (click)="option.action()"> <button tuiCell (click)="option.action()">
<tui-icon [icon]="option.icon" /> <tui-icon [icon]="option.icon" />
<div> <span tuiTitle>
<strong>{{ option.name }}</strong> <strong>{{ option.name }}</strong>
<div>{{ option.description }}</div> <span tuiSubtitle>{{ option.description }}</span>
</div> </span>
</button> </button>
} }
</section> </section>
@@ -27,7 +28,7 @@ import { BackupsRestoreService } from './services/restore.service'
host: { class: 'g-page' }, host: { class: 'g-page' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [BackupsUpcomingComponent, TuiIcon], imports: [BackupsUpcomingComponent, TuiIcon, TuiCell, TuiTitle],
}) })
export default class BackupsComponent { export default class BackupsComponent {
private readonly dialogs = inject(TuiDialogService) private readonly dialogs = inject(TuiDialogService)

View File

@@ -31,10 +31,8 @@ interface Package {
@if (pkgs) { @if (pkgs) {
@for (pkg of pkgs; track $index) { @for (pkg of pkgs; track $index) {
<label tuiBlock> <label tuiBlock>
<div class="g-action"> <img class="icon" alt="" [src]="pkg.icon" />
<img class="icon" alt="" [src]="pkg.icon" /> {{ pkg.title }}
{{ pkg.title }}
</div>
<input <input
type="checkbox" type="checkbox"
tuiCheckbox tuiCheckbox

View File

@@ -228,7 +228,7 @@ export class BackupsHistoryModal {
label: 'Backup Report', label: 'Backup Report',
data: { data: {
content: report, content: report,
timestamp: completedAt, createdAt: completedAt,
}, },
}) })
.subscribe() .subscribe()

View File

@@ -14,7 +14,9 @@ import {
TuiDialogService, TuiDialogService,
TuiIcon, TuiIcon,
TuiLoader, TuiLoader,
TuiTitle,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { BackupTarget } from 'src/app/services/api/api.types' import { BackupTarget } from 'src/app/services/api/api.types'
@@ -34,25 +36,22 @@ import { TARGETS } from './targets.component'
<h3 class="g-title">Saved Targets</h3> <h3 class="g-title">Saved Targets</h3>
@for (target of targets | keyvalue; track $index) { @for (target of targets | keyvalue; track $index) {
<button <button
class="g-action" tuiCell
[disabled]="isDisabled(target.value)" [disabled]="isDisabled(target.value)"
(click)="select(target.value, target.key)" (click)="select(target.value, target.key)"
> >
@if (target.value | getDisplayInfo; as displayInfo) { @if (target.value | getDisplayInfo; as displayInfo) {
<tui-icon [icon]="displayInfo.icon" /> <tui-icon [icon]="displayInfo.icon" />
<div> <span tuiTitle>
<strong>{{ displayInfo.name }}</strong> <strong>{{ displayInfo.name }}</strong>
<backups-status <backups-status
[type]="context.data.type" [type]="context.data.type"
[mountable]="target.value.mountable" [mountable]="target.value.mountable"
[hasBackup]="hasBackup(target.value)" [hasBackup]="hasBackup(target.value)"
/> />
<div [style.color]="'var(--tui-text-secondary'"> <span tuiSubtitle>{{ displayInfo.description }}</span>
{{ displayInfo.description }} <span tuiSubtitle>{{ displayInfo.path }}</span>
<br /> </span>
{{ displayInfo.path }}
</div>
</div>
} }
</button> </button>
} @empty { } @empty {
@@ -70,6 +69,8 @@ import { TARGETS } from './targets.component'
BackupsStatusComponent, BackupsStatusComponent,
GetDisplayInfoPipe, GetDisplayInfoPipe,
KeyValuePipe, KeyValuePipe,
TuiCell,
TuiTitle,
], ],
}) })
export class BackupsTargetModal { export class BackupsTargetModal {

View File

@@ -32,7 +32,7 @@ const LABELS = {
.cpu { .cpu {
position: relative; position: relative;
margin: 1rem auto; margin: 1rem auto;
width: 7rem; width: 8rem;
aspect-ratio: 1; aspect-ratio: 1;
} }

View File

@@ -24,7 +24,7 @@ const LABELS = {
selector: 'metrics-memory', selector: 'metrics-memory',
template: ` template: `
<label tuiProgressLabel> <label tuiProgressLabel>
<tui-progress-circle size="l" [max]="100" [value]="used()" /> <tui-progress-circle size="xl" [max]="100" [value]="used()" />
{{ value()?.percentageUsed?.value | value }}% {{ value()?.percentageUsed?.value | value }}%
</label> </label>
<metrics-data [labels]="labels" [value]="value()" /> <metrics-data [labels]="labels" [value]="value()" />

View File

@@ -30,7 +30,7 @@ const LABELS = {
progress { progress {
height: 1.5rem; height: 1.5rem;
width: 80%; width: 80%;
margin: 3.75rem auto; margin: 4.25rem auto;
border-radius: 0; border-radius: 0;
clip-path: none; clip-path: none;
mask: linear-gradient(to right, #000 80%, transparent 80%); mask: linear-gradient(to right, #000 80%, transparent 80%);

View File

@@ -56,6 +56,7 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'
:host { :host {
height: 100%; height: 100%;
min-height: 7.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View File

@@ -1,7 +1,14 @@
import { DatePipe } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { TuiNotification, TuiTitle } from '@taiga-ui/core' import {
TuiHint,
TuiIcon,
TuiLink,
TuiNotification,
TuiTitle,
} from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout'
import { TimeService } from 'src/app/services/time.service' import { TimeService } from 'src/app/services/time.service'
@Component({ @Component({
@@ -11,18 +18,45 @@ import { TimeService } from 'src/app/services/time.service'
@if (now(); as time) { @if (now(); as time) {
@if (!time.synced) { @if (!time.synced) {
<tui-notification appearance="warning"> <tui-notification appearance="warning">
NTP not synced, time could be wrong <ng-container *ngTemplateOutlet="hint" />
</tui-notification> </tui-notification>
} }
<div tuiTitle> <div tuiCell>
<div tuiSubtitle class="g-secondary"> <div tuiTitle [style.text-align]="'center'">
{{ time.now | date: 'h:mm a z' : 'UTC' }} <div tuiSubtitle class="g-secondary">
{{ time.now | date: 'h:mm a z' : 'UTC' }}
</div>
<b>{{ time.now | date: 'MMMM d, y' : 'UTC' }}</b>
</div> </div>
<b>{{ time.now | date: 'MMMM d, y' : 'UTC' }}</b> @if (!time.synced) {
<tui-icon
icon="@tui.circle-alert"
class="g-warning"
[tuiHint]="hint"
/>
}
</div> </div>
} @else { } @else {
Loading... Loading...
} }
<ng-template #hint>
<div tuiTitle>
Clock sync failure
<div tuiSubtitle>
To resolve it, refer to
<a
tuiLink
iconEnd="@tui.external-link"
appearance=""
href="https://docs.start9.com/0.3.5.x/support/common-issues#clock-sync-failure"
target="_blank"
rel="noreferrer"
[pseudo]="true"
[textContent]="'the docs'"
></a>
</div>
</div>
</ng-template>
`, `,
styles: ` styles: `
:host { :host {
@@ -32,14 +66,39 @@ import { TimeService } from 'src/app/services/time.service'
justify-content: center; justify-content: center;
gap: 1rem; gap: 1rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
[tuiCell],
[tuiTitle],
[tuiSubtitle] {
margin: 0;
justify-content: center;
}
} }
[tuiTitle] { tui-icon {
text-align: center; display: none;
}
:host-context(tui-root._mobile) {
tui-notification {
display: none;
}
tui-icon {
display: block;
}
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiNotification, DatePipe, TuiTitle], imports: [
CommonModule,
TuiNotification,
TuiTitle,
TuiLink,
TuiCell,
TuiIcon,
TuiHint,
],
}) })
export class TimeComponent { export class TimeComponent {
readonly now = toSignal(inject(TimeService).now$) readonly now = toSignal(inject(TimeService).now$)

View File

@@ -19,7 +19,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
@if (['running', 'starting', 'restarting'].includes(status)) { @if (['running', 'starting', 'restarting'].includes(status)) {
<button <button
tuiButton tuiButton
appearance="outline-destructive" appearance="secondary-destructive"
iconStart="@tui.square" iconStart="@tui.square"
(click)="actions.stop(manifest)" (click)="actions.stop(manifest)"
> >
@@ -30,7 +30,6 @@ import { getManifest } from 'src/app/utils/get-package-data'
@if (status === 'running') { @if (status === 'running') {
<button <button
tuiButton tuiButton
appearance="outline"
iconStart="@tui.rotate-cw" iconStart="@tui.rotate-cw"
(click)="actions.restart(manifest)" (click)="actions.restart(manifest)"
> >
@@ -41,7 +40,6 @@ import { getManifest } from 'src/app/utils/get-package-data'
@if (status === 'stopped') { @if (status === 'stopped') {
<button <button
tuiButton tuiButton
appearance="outline"
iconStart="@tui.play" iconStart="@tui.play"
(click)="actions.start(manifest, hasUnmet(dependencies))" (click)="actions.start(manifest, hasUnmet(dependencies))"
> >
@@ -52,19 +50,21 @@ import { getManifest } from 'src/app/utils/get-package-data'
styles: [ styles: [
` `
:host { :host {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); flex-wrap: wrap;
gap: 1rem; gap: 1rem;
justify-content: center; justify-content: center;
inline-size: 20rem;
max-inline-size: 100%; max-inline-size: 100%;
margin-block-start: 1rem; margin-block-start: 1rem;
&:nth-child(3) {
grid-row: span 2;
}
} }
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
display: flex; display: flex;
margin: 0; margin: 0;
inline-size: min-content;
[tuiButton] { [tuiButton] {
font-size: 0; font-size: 0;

View File

@@ -5,13 +5,10 @@ import {
Input, Input,
} from '@angular/core' } from '@angular/core'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiButton, TuiLink } from '@taiga-ui/core' import { TuiButton, TuiLink } from '@taiga-ui/core'
import { TuiBadge } from '@taiga-ui/kit' import { TuiBadge } from '@taiga-ui/kit'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { getManifest } from '../../../../../utils/get-package-data'
import { MappedInterface } from '../types/mapped-interface' import { MappedInterface } from '../types/mapped-interface'
@Component({ @Component({
@@ -30,25 +27,21 @@ import { MappedInterface } from '../types/mapped-interface'
</td> </td>
<td> <td>
@if (info.public) { @if (info.public) {
<button <a
tuiButton class="hosting"
size="s" tuiLink
iconStart="@tui.globe" iconStart="@tui.globe"
appearance="positive" appearance="positive"
(click)="toggle()" [textContent]="'Public'"
> ></a>
Public
</button>
} @else { } @else {
<button <a
tuiButton class="hosting"
size="s" tuiLink
iconStart="@tui.lock" iconStart="@tui.lock"
appearance="negative" appearance="negative"
(click)="toggle()" [textContent]="'Private'"
> ></a>
Private
</button>
} }
</td> </td>
<td [style.grid-area]="'span 2'"> <td [style.grid-area]="'span 2'">
@@ -70,6 +63,24 @@ import { MappedInterface } from '../types/mapped-interface'
</td> </td>
`, `,
styles: ` 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 { strong {
white-space: nowrap; white-space: nowrap;
} }
@@ -88,6 +99,10 @@ import { MappedInterface } from '../types/mapped-interface'
td { td {
padding: 0; padding: 0;
} }
.hosting {
font-size: 0;
}
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -96,9 +111,6 @@ import { MappedInterface } from '../types/mapped-interface'
}) })
export class ServiceInterfaceComponent { export class ServiceInterfaceComponent {
private readonly config = inject(ConfigService) private readonly config = inject(ConfigService)
private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService)
private readonly api = inject(ApiService)
@Input({ required: true }) @Input({ required: true })
info!: MappedInterface info!: MappedInterface
@@ -125,31 +137,4 @@ export class ServiceInterfaceComponent {
? 'null' ? 'null'
: this.config.launchableAddress(this.info, this.pkg.hosts) : this.config.launchableAddress(this.info, this.pkg.hosts)
} }
async toggle() {
const loader = this.loader
.open(`Making ${this.info.public ? 'private' : 'public'}`)
.subscribe()
const params = {
internalPort: this.info.addressInfo.internalPort,
public: !this.info.public,
}
try {
if (!this.info.public) {
await this.api.pkgBindingSetPubic({
...params,
host: this.info.addressInfo.hostId,
package: getManifest(this.pkg).id,
})
} else {
await this.api.serverBindingSetPubic(params)
}
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
} }

View File

@@ -5,6 +5,7 @@ import {
inject, inject,
input, input,
} from '@angular/core' } from '@angular/core'
import { RouterLink } from '@angular/router'
import { TuiTable } from '@taiga-ui/addon-table' import { TuiTable } from '@taiga-ui/addon-table'
import { tuiDefaultSort } from '@taiga-ui/cdk' import { tuiDefaultSort } from '@taiga-ui/cdk'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
@@ -31,6 +32,7 @@ import { ServiceInterfaceComponent } from './interface.component'
@for (info of interfaces(); track $index) { @for (info of interfaces(); track $index) {
<tr <tr
serviceInterface serviceInterface
[routerLink]="info.routerLink"
[info]="info" [info]="info"
[pkg]="pkg()" [pkg]="pkg()"
[disabled]="disabled()" [disabled]="disabled()"
@@ -46,7 +48,7 @@ import { ServiceInterfaceComponent } from './interface.component'
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ServiceInterfaceComponent, TuiTable], imports: [ServiceInterfaceComponent, TuiTable, RouterLink],
}) })
export class ServiceInterfacesComponent { export class ServiceInterfacesComponent {
private readonly config = inject(ConfigService) private readonly config = inject(ConfigService)

View File

@@ -70,8 +70,8 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
div { div {
flex-direction: row; display: grid;
justify-content: space-between; grid-template-columns: 1fr max-content;
padding: 0.5rem 0; padding: 0.5rem 0;
} }

View File

@@ -27,12 +27,16 @@ import { StatusComponent } from './status.component'
<a [routerLink]="routerLink">{{ manifest.title }}</a> <a [routerLink]="routerLink">{{ manifest.title }}</a>
</td> </td>
<td [style.grid-area]="'2 / 2'">{{ manifest.version }}</td> <td [style.grid-area]="'2 / 2'">{{ manifest.version }}</td>
<td [appUptime]="$any(pkg.status).started"></td>
<td <td
[style.grid-area]="'3 / 2'" [appUptime]="$any(pkg.status).started"
[style.grid-column]="2"
[style.grid-row]="4"
></td>
<td
appStatus appStatus
[pkg]="pkg" [pkg]="pkg"
[hasDepErrors]="hasError(depErrors)" [hasDepErrors]="hasError(depErrors)"
[style.grid-area]="'3 / 2'"
></td> ></td>
<td [style.grid-area]="'2 / 3'" [style.text-align]="'center'"> <td [style.grid-area]="'2 / 3'" [style.text-align]="'center'">
<fieldset <fieldset
@@ -56,6 +60,10 @@ import { StatusComponent } from './status.component'
} }
} }
td::before {
display: none;
}
img { img {
display: block; display: block;
height: 2rem; height: 2rem;
@@ -75,7 +83,7 @@ import { StatusComponent } from './status.component'
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
position: relative; position: relative;
display: grid; display: grid;
grid-template: 2rem 2rem 2rem/6rem 1fr 2rem; grid-template: 1.25rem 1.75rem 1.5rem 1.25rem/6rem 1fr 2rem;
align-items: center; align-items: center;
padding: 1rem; padding: 1rem;
@@ -90,6 +98,15 @@ import { StatusComponent } from './status.component'
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: var(--tui-text-secondary);
&::before {
display: inline;
}
&:empty {
display: none;
}
} }
} }
`, `,

View File

@@ -5,7 +5,7 @@ import {
INJECTOR, INJECTOR,
} from '@angular/core' } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { CopyService, getPkgId } from '@start9labs/shared' import { CopyService, getPkgId, MarkdownComponent } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core' import { TuiDialogService } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout' import { TuiCell } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@@ -17,7 +17,6 @@ import {
FALLBACK_URL, FALLBACK_URL,
ServiceAdditionalItemComponent, ServiceAdditionalItemComponent,
} from '../components/additional-item.component' } from '../components/additional-item.component'
import ServiceMarkdownRoute from './markdown.component'
@Component({ @Component({
template: ` template: `
@@ -52,7 +51,7 @@ import ServiceMarkdownRoute from './markdown.component'
export default class ServiceAboutRoute { export default class ServiceAboutRoute {
private readonly copyService = inject(CopyService) private readonly copyService = inject(CopyService)
private readonly markdown = inject(TuiDialogService).open( private readonly markdown = inject(TuiDialogService).open(
new PolymorpheusComponent(ServiceMarkdownRoute, inject(INJECTOR)), new PolymorpheusComponent(MarkdownComponent, inject(INJECTOR)),
{ label: 'License', size: 'l' }, { label: 'License', size: 'l' },
) )

View File

@@ -1,3 +1,4 @@
import { NgTemplateOutlet } from '@angular/common'
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@@ -10,7 +11,7 @@ import { RouterLink } from '@angular/router'
import { getPkgId } from '@start9labs/shared' import { getPkgId } from '@start9labs/shared'
import { TuiItem } from '@taiga-ui/cdk' import { TuiItem } from '@taiga-ui/cdk'
import { TuiButton, TuiLink } from '@taiga-ui/core' import { TuiButton, TuiLink } from '@taiga-ui/core'
import { TuiBreadcrumbs } from '@taiga-ui/kit' import { TuiBadge, TuiBreadcrumbs } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component' import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils' import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
@@ -23,12 +24,16 @@ import { TitleDirective } from 'src/app/services/title.service'
<ng-container *title> <ng-container *title>
<a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">Back</a>
{{ interface()?.name }} {{ interface()?.name }}
<ng-container *ngTemplateOutlet="badge" />
</ng-container> </ng-container>
<tui-breadcrumbs size="l" [style.margin-block-end.rem]="1"> <tui-breadcrumbs size="l" [style.margin-block-end.rem]="1">
<a *tuiItem tuiLink appearance="action-grayscale" routerLink="../.."> <a *tuiItem tuiLink appearance="action-grayscale" routerLink="../..">
Dashboard Dashboard
</a> </a>
<span *tuiItem class="g-primary">{{ interface()?.name }}</span> <span *tuiItem class="g-primary">
{{ interface()?.name }}
<ng-container *ngTemplateOutlet="badge" />
</span>
</tui-breadcrumbs> </tui-breadcrumbs>
@if (interface(); as serviceInterface) { @if (interface(); as serviceInterface) {
<app-interface <app-interface
@@ -36,6 +41,16 @@ import { TitleDirective } from 'src/app/services/title.service'
[serviceInterface]="serviceInterface" [serviceInterface]="serviceInterface"
/> />
} }
<ng-template #badge>
<tui-badge
[iconStart]="interface()?.public ? '@tui.globe' : '@tui.lock'"
[style.vertical-align.rem]="-0.125"
[style.margin]="'0 0.25rem -0.25rem'"
[appearance]="interface()?.public ? 'positive' : 'negative'"
>
{{ interface()?.public ? 'Public' : 'Private' }}
</tui-badge>
</ng-template>
`, `,
styles: ` styles: `
:host-context(tui-root._mobile) tui-breadcrumbs { :host-context(tui-root._mobile) tui-breadcrumbs {
@@ -53,6 +68,8 @@ import { TitleDirective } from 'src/app/services/title.service'
TuiBreadcrumbs, TuiBreadcrumbs,
TuiItem, TuiItem,
TuiLink, TuiLink,
TuiBadge,
NgTemplateOutlet,
], ],
}) })
export default class ServiceInterfaceRoute { export default class ServiceInterfaceRoute {

View File

@@ -1,5 +1,6 @@
import { inject } from '@angular/core' import { inject } from '@angular/core'
import { ActivatedRouteSnapshot, ResolveFn, Routes } from '@angular/router' import { ActivatedRouteSnapshot, ResolveFn, Routes } from '@angular/router'
import { MarkdownComponent } from '@start9labs/shared'
import { defer, map, Observable, of } from 'rxjs' import { defer, map, Observable, of } from 'rxjs'
import { share } from 'rxjs/operators' import { share } from 'rxjs/operators'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -22,7 +23,7 @@ export const ROUTES: Routes = [
}, },
{ {
path: 'instructions', path: 'instructions',
loadComponent: () => import('./routes/markdown.component'), component: MarkdownComponent,
resolve: { content: getStatic('instructions.md') }, resolve: { content: getStatic('instructions.md') },
canActivate: [ canActivate: [
({ paramMap }: ActivatedRouteSnapshot) => { ({ paramMap }: ActivatedRouteSnapshot) => {

View File

@@ -38,7 +38,7 @@ import { ConfigService } from 'src/app/services/config.service'
import { EOSService } from 'src/app/services/eos.service' import { EOSService } from 'src/app/services/eos.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { SystemSyncComponent } from './sync.component' import { SnekDirective } from './snek.directive'
import { UPDATE } from './update.component' import { UPDATE } from './update.component'
import { SystemWipeComponent } from './wipe.component' import { SystemWipeComponent } from './wipe.component'
@@ -61,9 +61,6 @@ import { SystemWipeComponent } from './wipe.component'
</hgroup> </hgroup>
</header> </header>
@if (server(); as server) { @if (server(); as server) {
@if (!server.ntpSynced) {
<system-sync />
}
<div tuiCell tuiAppearance="outline-grayscale"> <div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.zap" /> <tui-icon icon="@tui.zap" />
<span tuiTitle> <span tuiTitle>
@@ -75,6 +72,7 @@ import { SystemWipeComponent } from './wipe.component'
<button <button
tuiButton tuiButton
appearance="accent" appearance="accent"
iconStart="@tui.refresh-cw"
[disabled]="eos.updatingOrBackingUp$ | async" [disabled]="eos.updatingOrBackingUp$ | async"
(click)="onUpdate()" (click)="onUpdate()"
> >
@@ -125,7 +123,7 @@ import { SystemWipeComponent } from './wipe.component'
</button> </button>
</div> </div>
<div tuiCell tuiAppearance="outline-grayscale"> <div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.download" /> <tui-icon icon="@tui.award" />
<span tuiTitle> <span tuiTitle>
<strong> <strong>
{{ 'system.general.ca.title' | i18n }} {{ 'system.general.ca.title' | i18n }}
@@ -134,7 +132,7 @@ import { SystemWipeComponent } from './wipe.component'
{{ 'system.general.ca.subtitle' | i18n }} {{ 'system.general.ca.subtitle' | i18n }}
</span> </span>
</span> </span>
<button tuiButton (click)="downloadCA()"> <button tuiButton iconStart="@tui.download" (click)="downloadCA()">
{{ 'system.general.ca.button' | i18n }} {{ 'system.general.ca.button' | i18n }}
</button> </button>
</div> </div>
@@ -168,6 +166,12 @@ import { SystemWipeComponent } from './wipe.component'
</button> </button>
</div> </div>
} }
<img
[snek]="score()"
class="snek"
alt="Play Snake"
src="assets/img/icons/snek.png"
/>
} }
<!-- hidden element for downloading cert --> <!-- hidden element for downloading cert -->
<a id="download-ca" href="/static/local-root-ca.crt"></a> <a id="download-ca" href="/static/local-root-ca.crt"></a>
@@ -177,6 +181,16 @@ import { SystemWipeComponent } from './wipe.component'
max-inline-size: 40rem; max-inline-size: 40rem;
} }
.snek {
width: 1rem;
opacity: 0.2;
cursor: pointer;
&:hover {
opacity: 1;
}
}
strong { strong {
line-height: 1.25rem; line-height: 1.25rem;
} }
@@ -205,12 +219,12 @@ import { SystemWipeComponent } from './wipe.component'
TuiButton, TuiButton,
TuiIcon, TuiIcon,
TitleDirective, TitleDirective,
SystemSyncComponent,
TuiButtonLoading, TuiButtonLoading,
TuiButtonSelect, TuiButtonSelect,
TuiDataListWrapper, TuiDataListWrapper,
TuiTextfield, TuiTextfield,
FormsModule, FormsModule,
SnekDirective,
], ],
}) })
export default class SystemGeneralComponent { export default class SystemGeneralComponent {
@@ -235,6 +249,10 @@ export default class SystemGeneralComponent {
readonly eos = inject(EOSService) readonly eos = inject(EOSService)
readonly i18n = inject(i18nService) readonly i18n = inject(i18nService)
readonly languages = ['english', 'spanish'] readonly languages = ['english', 'spanish']
readonly score = toSignal(
this.patch.watch$('ui', 'gaming', 'snake', 'highScore'),
{ initialValue: 0 },
)
onUpdate() { onUpdate() {
if (this.server()?.statusInfo.updated) { if (this.server()?.statusInfo.updated) {

View File

@@ -42,7 +42,7 @@ import { injectContext } from '@taiga-ui/polymorpheus'
], ],
imports: [TuiButton], imports: [TuiButton],
}) })
export class HeaderSnekComponent implements AfterViewInit, OnDestroy { export class SnekComponent implements AfterViewInit, OnDestroy {
private readonly document = inject(DOCUMENT) private readonly document = inject(DOCUMENT)
private readonly dialog = injectContext<TuiDialogContext<number, number>>() private readonly dialog = injectContext<TuiDialogContext<number, number>>()

View File

@@ -4,31 +4,31 @@ import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { filter } from 'rxjs' import { filter } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { HeaderSnekComponent } from './snek.component' import { SnekComponent } from './snek.component'
@Directive({ @Directive({
standalone: true, standalone: true,
selector: 'img[appSnek]', selector: 'img[snek]',
}) })
export class HeaderSnekDirective { export class SnekDirective {
private readonly dialogs = inject(TuiDialogService) private readonly dialogs = inject(TuiDialogService)
private readonly loader = inject(LoadingService) private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
@Input() @Input()
appSnek = 0 snek = 0
@HostListener('click') @HostListener('click')
async onClick() { async onClick() {
this.dialogs this.dialogs
.open<number>(new PolymorpheusComponent(HeaderSnekComponent), { .open<number>(new PolymorpheusComponent(SnekComponent), {
label: 'Snake!', label: 'Snake!',
closeable: false, closeable: false,
dismissible: false, dismissible: false,
data: this.appSnek, data: this.snek,
}) })
.pipe(filter(score => score > this.appSnek)) .pipe(filter(score => score > this.snek))
.subscribe(async score => { .subscribe(async score => {
const loader = this.loader.open('Saving high score...').subscribe() const loader = this.loader.open('Saving high score...').subscribe()

View File

@@ -1,29 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { TuiLink, TuiNotification, TuiTitle } from '@taiga-ui/core'
import { i18nPipe } from 'src/app/i18n/i18n.pipe'
@Component({
selector: 'system-sync',
template: `
<tui-notification appearance="warning">
<div tuiTitle>
{{ 'system.general.sync.title' | i18n }}
<div tuiSubtitle>
{{ 'system.general.sync.subtitle' | i18n }}
<a
tuiLink
iconEnd="@tui.external-link"
href="https://docs.start9.com/0.3.5.x/support/common-issues#clock-sync-failure"
target="_blank"
rel="noreferrer"
[textContent]="'StartOS docs'"
></a>
</div>
</div>
</tui-notification>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiNotification, TuiTitle, TuiLink, i18nPipe],
})
export class SystemSyncComponent {}

View File

@@ -4,7 +4,7 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { import {
ErrorService, ErrorService,
LoadingService, LoadingService,
MarkdownPipeModule, MarkdownPipe,
SafeLinksDirective, SafeLinksDirective,
} from '@start9labs/shared' } from '@start9labs/shared'
import { import {
@@ -36,7 +36,7 @@ import { EOSService } from 'src/app/services/eos.service'
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
MarkdownPipeModule, MarkdownPipe,
NgDompurifyModule, NgDompurifyModule,
SafeLinksDirective, SafeLinksDirective,
TuiAutoFocus, TuiAutoFocus,

View File

@@ -39,7 +39,7 @@ import { SessionsTableComponent } from './table.component'
<button <button
tuiButton tuiButton
size="xs" size="xs"
appearance="negative" appearance="primary-destructive"
[style.margin-inline-start]="'auto'" [style.margin-inline-start]="'auto'"
[disabled]="!selected.length" [disabled]="!selected.length"
(click)="terminate(selected, others || [])" (click)="terminate(selected, others || [])"
@@ -60,7 +60,6 @@ import { SessionsTableComponent } from './table.component'
TuiLet, TuiLet,
RouterLink, RouterLink,
TitleDirective, TitleDirective,
TuiTable,
TuiHeader, TuiHeader,
TuiTitle, TuiTitle,
], ],

View File

@@ -87,15 +87,19 @@ import { PlatformInfoPipe } from './platform-info.pipe'
grid-template-columns: 2.5rem 1fr; grid-template-columns: 2.5rem 1fr;
&:has(:checked) .platform { &:has(:checked) .platform {
color: var(--tui-text-action); visibility: hidden;
} }
} }
input { input {
@include fullsize(); left: 0.25rem;
z-index: 1;
opacity: 0; &:not(:checked) {
transform: none; @include fullsize();
z-index: 1;
visibility: hidden;
transform: none;
}
} }
td { td {

View File

@@ -8,19 +8,14 @@ import {
} from '@angular/core' } from '@angular/core'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { MarketplacePkg } from '@start9labs/marketplace' import { MarketplacePkg } from '@start9labs/marketplace'
import { MarkdownPipeModule, SafeLinksDirective } from '@start9labs/shared' import { MarkdownPipe, SafeLinksDirective } from '@start9labs/shared'
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
import { import { TuiButton, TuiIcon, TuiLink, TuiTitle } from '@taiga-ui/core'
TuiButton,
TuiIcon,
TuiLink,
TuiLoader,
TuiTitle,
} from '@taiga-ui/core'
import { TuiExpand } from '@taiga-ui/experimental' import { TuiExpand } from '@taiga-ui/experimental'
import { import {
TUI_CONFIRM, TUI_CONFIRM,
TuiAvatar, TuiAvatar,
TuiButtonLoading,
TuiChevron, TuiChevron,
TuiFade, TuiFade,
TuiProgressCircle, TuiProgressCircle,
@@ -84,20 +79,22 @@ import UpdatesComponent from './updates.component'
" "
/> />
} @else { } @else {
@if (ready()) { <button
<button tuiButton
tuiButton size="s"
iconStart="@tui.arrow-big-up-dash" [loading]="!ready()"
[appearance]="error() ? 'destructive' : 'primary'" [appearance]="error() ? 'destructive' : 'primary'"
(click.stop)="onClick()" (click.stop)="onClick()"
> >
{{ error() ? 'Retry' : 'Update' }} {{ error() ? 'Retry' : 'Update' }}
</button> </button>
} @else {
<tui-loader [style.width.rem]="2" [inheritColor]="true" />
}
} }
<button tuiIconButton appearance="icon" [tuiChevron]="expanded()"> <button
tuiIconButton
size="s"
appearance="icon"
[tuiChevron]="expanded()"
>
Show more Show more
</button> </button>
</div> </div>
@@ -119,15 +116,16 @@ import UpdatesComponent from './updates.component'
</p> </p>
<p tuiTitle> <p tuiTitle>
<span> <span>
<b>What's new</b>
(
<a <a
tuiLink tuiLink
iconEnd="@tui.external-link" iconEnd="@tui.external-link"
routerLink="/portal/marketplace" routerLink="/portal/marketplace"
[queryParams]="{ url: parent.current()?.url, id: item().id }" [queryParams]="{ url: parent.current()?.url, id: item().id }"
> [textContent]="'View listing'"
View listing ></a>
</a> )
<b>What's new</b>
</span> </span>
</p> </p>
<p <p
@@ -139,8 +137,6 @@ import UpdatesComponent from './updates.component'
</tr> </tr>
`, `,
styles: ` styles: `
@import '@taiga-ui/core/styles/taiga-ui-local';
:host { :host {
display: contents; display: contents;
} }
@@ -161,13 +157,6 @@ import UpdatesComponent from './updates.component'
word-break: break-word; word-break: break-word;
clip-path: inset(0 round var(--tui-radius-s)); clip-path: inset(0 round var(--tui-radius-s));
cursor: pointer; cursor: pointer;
@include transition(background);
@media ($tui-mouse) {
&:hover {
background: var(--tui-background-neutral-1);
}
}
} }
td { td {
@@ -190,10 +179,6 @@ import UpdatesComponent from './updates.component'
&[colspan]:only-child { &[colspan]:only-child {
padding: 0 3rem; padding: 0 3rem;
text-align: left; text-align: left;
[tuiLink] {
float: right;
}
} }
} }
@@ -217,11 +202,6 @@ import UpdatesComponent from './updates.component'
padding: 0 0.5rem; padding: 0 0.5rem;
} }
[tuiButton] {
font-size: 0;
gap: 0;
}
.desktop { .desktop {
display: none; display: none;
} }
@@ -236,19 +216,19 @@ import UpdatesComponent from './updates.component'
RouterLink, RouterLink,
TuiExpand, TuiExpand,
TuiButton, TuiButton,
TuiButtonLoading,
TuiChevron, TuiChevron,
TuiAvatar, TuiAvatar,
TuiLink, TuiLink,
TuiIcon, TuiIcon,
TuiLoader,
TuiProgressCircle, TuiProgressCircle,
TuiTitle, TuiTitle,
MarkdownPipeModule, TuiFade,
MarkdownPipe,
NgDompurifyModule, NgDompurifyModule,
SafeLinksDirective, SafeLinksDirective,
DatePipe, DatePipe,
InstallingProgressPipe, InstallingProgressPipe,
TuiFade,
], ],
}) })
export class UpdatesItemComponent { export class UpdatesItemComponent {

View File

@@ -22,6 +22,7 @@ import {
import { TuiCell } from '@taiga-ui/layout' import { TuiCell } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { combineLatest, map, tap } from 'rxjs' import { combineLatest, map, tap } from 'rxjs'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { import {
@@ -91,43 +92,35 @@ interface UpdatesData {
Request Failed Request Failed
</tui-notification> </tui-notification>
} }
<section class="g-card" [style.padding]="'0 1rem 1rem'"> <section class="g-card">
<table tuiTable class="g-table"> <header>{{ current()?.name }}</header>
<thead> <table
<tr> [appTable]="['Name', 'Version', 'Package Hash', 'Published', '']"
<th tuiTh>Name</th> >
<th tuiTh>Version</th> @if (
<th tuiTh>Package Hash</th> data()?.marketplace?.[current()?.url || '']?.packages;
<th tuiTh>Published</th> as packages
<th tuiTh></th> ) {
</tr> @if (packages | filterUpdates: data()?.localPkgs; as updates) {
</thead> @for (pkg of updates; track $index) {
<tbody> <updates-item
@if ( [item]="pkg"
data()?.marketplace?.[current()?.url || '']?.packages; [local]="data()?.localPkgs?.[pkg.id]!"
as packages />
) { } @empty {
@if (packages | filterUpdates: data()?.localPkgs; as updates) { <tr>
@for (pkg of updates; track $index) { <td colspan="5">All services are up to date!</td>
<updates-item </tr>
[item]="pkg"
[local]="data()?.localPkgs?.[pkg.id]!"
/>
} @empty {
<tr>
<td colspan="5">All services are up to date!</td>
</tr>
}
} }
} @else {
<tr>
<td colspan="5" [tuiSkeleton]="true">Loading</td>
</tr>
<tr>
<td colspan="5" [tuiSkeleton]="true">Loading</td>
</tr>
} }
</tbody> } @else {
<tr>
<td colspan="5" [tuiSkeleton]="true">Loading</td>
</tr>
<tr>
<td colspan="5" [tuiSkeleton]="true">Loading</td>
</tr>
}
</table> </table>
</section> </section>
</section> </section>
@@ -172,6 +165,11 @@ interface UpdatesData {
section { section {
background: none; background: none;
box-shadow: none; box-shadow: none;
padding: 0.5rem;
}
header {
display: none;
} }
[tuiCell] { [tuiCell] {
@@ -203,7 +201,6 @@ interface UpdatesData {
TuiTitle, TuiTitle,
TuiNotification, TuiNotification,
TuiSkeleton, TuiSkeleton,
TuiTable,
TuiBadgeNotification, TuiBadgeNotification,
TuiFade, TuiFade,
TuiButton, TuiButton,
@@ -211,6 +208,7 @@ interface UpdatesData {
FilterUpdatesPipe, FilterUpdatesPipe,
UpdatesItemComponent, UpdatesItemComponent,
TitleDirective, TitleDirective,
TableComponent,
], ],
}) })
export default class UpdatesComponent { export default class UpdatesComponent {

View File

@@ -27,10 +27,12 @@ export class BadgeService {
private readonly notifications = inject(NotificationService) private readonly notifications = inject(NotificationService)
private readonly exver = inject(Exver) private readonly exver = inject(Exver)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly system$ = combineLatest([ private readonly system$ = inject(EOSService).updateAvailable$.pipe(
this.patch.watch$('serverInfo', 'ntpSynced'), map(Number),
inject(EOSService).updateAvailable$, )
]).pipe(map(([synced, update]) => Number(!synced) + Number(update))) private readonly metrics$ = this.patch
.watch$('serverInfo', 'ntpSynced')
.pipe(map(synced => Number(!synced)))
private readonly marketplaceService = inject(MarketplaceService) private readonly marketplaceService = inject(MarketplaceService)
private readonly local$ = inject(ConnectionService).pipe( private readonly local$ = inject(ConnectionService).pipe(
@@ -86,6 +88,8 @@ export class BadgeService {
return this.updates$ return this.updates$
case '/portal/system': case '/portal/system':
return this.system$ return this.system$
case '/portal/metrics':
return this.metrics$
case '/portal/notifications': case '/portal/notifications':
return this.notifications.unreadCount$ return this.notifications.unreadCount$
default: default:

View File

@@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core'
import { ErrorService, MARKDOWN } from '@start9labs/shared' import { ErrorService, MARKDOWN } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core' import { TuiDialogService } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { firstValueFrom, merge, shareReplay, Subject } from 'rxjs' import { firstValueFrom, merge, of, shareReplay, Subject } from 'rxjs'
import { REPORT } from 'src/app/components/report.component' import { REPORT } from 'src/app/components/report.component'
import { import {
ServerNotification, ServerNotification,
@@ -94,13 +94,14 @@ export class NotificationService {
full = false, full = false,
) { ) {
const label = full || code === 2 ? title : 'Backup Report' const label = full || code === 2 ? title : 'Backup Report'
const content = code === 1 ? REPORT : MARKDOWN const component = code === 1 ? REPORT : MARKDOWN
const content = code === 1 ? data : of(data)
this.dialogs this.dialogs
.open(full ? message : content, { .open(full ? message : component, {
label, label,
data: { data: {
content: data, content,
timestamp: createdAt, timestamp: createdAt,
}, },
}) })

View File

@@ -147,11 +147,6 @@ hr {
> table[tuiTable] { > table[tuiTable] {
margin: 0 -0.5rem; margin: 0 -0.5rem;
td:empty,
th:empty {
display: none;
}
} }
> header { > header {
@@ -183,6 +178,7 @@ hr {
border-radius: var(--tui-radius-s); border-radius: var(--tui-radius-s);
overflow: hidden; overflow: hidden;
box-shadow: inset 0 0 0 1px var(--tui-background-neutral-1); box-shadow: inset 0 0 0 1px var(--tui-background-neutral-1);
clip-path: inset(0 round var(--tui-radius-s));
td, td,
th { th {
@@ -301,41 +297,6 @@ hr {
margin-top: 24px; margin-top: 24px;
} }
.g-action {
@include transition(background);
@include button-clear();
display: flex;
align-items: center;
width: stretch;
gap: 1rem;
text-align: left;
font-size: 0.85rem;
padding: 0.5rem 1rem;
margin: 0 -1rem;
line-height: 1.25rem;
border-radius: 0.5rem;
color: var(--tui-text-primary);
}
a.g-action,
button.g-action {
cursor: pointer;
&:disabled {
pointer-events: none;
opacity: var(--tui-disabled-opacity);
}
&:hover {
background: var(--tui-background-neutral-1);
}
&:not(:last-child) {
box-shadow: 0 calc(0.5rem + 1px) 0 -0.5rem var(--tui-background-neutral-1);
}
}
.g-toggle { .g-toggle {
height: var(--tui-height-l); height: var(--tui-height-l);
display: flex; display: flex;