misc fixes (#2961)

* fix backup reports modal

* chore: fix comments

---------

Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
Matt Hill
2025-06-17 11:22:32 -06:00
committed by GitHub
parent 2464d255d5
commit f5688e077a
14 changed files with 279 additions and 242 deletions

View File

@@ -17,20 +17,14 @@ import { StateService } from 'src/app/services/state.service'
@Component({
template: `
<canvas matrix></canvas>
@if (stateService.kiosk) {
<section tuiCardLarge>
<h1 class="heading">
<tui-icon icon="@tui.check-square" class="g-positive" />
Setup Complete!
</h1>
<section tuiCardLarge>
<h1 class="heading">
<tui-icon icon="@tui.circle-check-big" class="g-positive" />
Setup Complete!
</h1>
@if (stateService.kiosk) {
<button tuiButton (click)="exitKiosk()">Continue to Login</button>
</section>
} @else if (lanAddress) {
<section tuiCardLarge>
<h1 class="heading">
<tui-icon icon="@tui.check-square" class="g-positive" />
Setup Complete!
</h1>
} @else if (lanAddress) {
@if (stateService.setupType === 'restore') {
<h3>You can now safely unplug your backup drive</h3>
} @else if (stateService.setupType === 'transfer') {
@@ -65,8 +59,8 @@ import { StateService } from 'src/app/services/state.service'
</strong>
</a>
<app-documentation hidden [lanAddress]="lanAddress" />
</section>
}
}
</section>
`,
styles: `
.heading {

View File

@@ -11,10 +11,10 @@ import { TuiDialogContext, TuiIcon, TuiTitle } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client'
import { scan } from 'rxjs'
import { map } from 'rxjs'
import { BackupReport } from 'src/app/services/api/api.types'
import { getManifest } from '../utils/get-package-data'
import { T } from '@start9labs/start-sdk'
import { DataModel } from '../services/patch-db/data-model'
@Component({
template: `
@@ -56,7 +56,7 @@ import { T } from '@start9labs/start-sdk'
})
export class BackupsReportModal {
private readonly i18n = inject(i18nPipe)
private readonly patch = inject(PatchDB)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly data =
injectContext<
@@ -65,13 +65,15 @@ export class BackupsReportModal {
readonly pkgTitles = toSignal(
this.patch.watch$('packageData').pipe(
scan<T.PackageDataEntry, Record<string, string>>((acc, pkg) => {
const { id, title } = getManifest(pkg)
return {
...acc,
[id]: title,
}
}, {}),
map(allPkgs =>
Object.values(allPkgs).reduce<Record<string, string>>((acc, pkg) => {
const { id, title } = getManifest(pkg)
return {
...acc,
[id]: title,
}
}, {}),
),
),
)

View File

@@ -91,7 +91,7 @@ type ClearnetForm = {
<td [style.width.rem]="12">
{{ interface.value().addSsl ? (address.acme | acme) : '-' }}
</td>
<td>{{ address.url | mask }}</td>
<td [style.order]="-1">{{ address.url | mask }}</td>
<td
actions
[href]="address.url"
@@ -102,7 +102,6 @@ type ClearnetForm = {
tuiIconButton
iconStart="@tui.trash"
appearance="flat-grayscale"
[style.margin-inline-end.rem]="0.5"
(click)="remove(address)"
>
{{ 'Delete' | i18n }}
@@ -131,6 +130,19 @@ type ClearnetForm = {
</app-placeholder>
}
`,
styles: `
:host-context(tui-root._mobile) {
td {
font-weight: bold;
color: var(--tui-text-primary);
&:first-child {
font-weight: normal;
color: var(--tui-text-secondary);
}
}
}
`,
host: { class: 'g-card' },
imports: [
TuiButton,

View File

@@ -6,6 +6,7 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
template: `
<thead>
<tr>
<ng-content select="th" />
@for (header of appTable(); track $index) {
<th>{{ header | i18n }}</th>
}

View File

@@ -1,3 +1,4 @@
import { NgTemplateOutlet } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
@@ -16,44 +17,71 @@ import { NotificationsTableComponent } from './table.component'
@Component({
template: `
<ng-container *title>{{ 'Notifications' | i18n }}</ng-container>
<h3 class="g-title">
<button
appearance="primary"
iconEnd="@tui.chevron-down"
tuiButton
size="xs"
type="button"
tuiDropdownAlign="right"
tuiDropdownSided
[disabled]="!table.selected().length"
[tuiDropdown]="dropdown"
[tuiDropdownEnabled]="!!table.selected().length"
[(tuiDropdownOpen)]="open"
>
{{ 'Batch action' | i18n }}
</button>
<ng-template #dropdown>
<tui-data-list>
<button
tuiOption
(click)="markSeen(notifications(), table.selected())"
>
{{ 'Mark seen' | i18n }}
</button>
<button
tuiOption
(click)="markUnseen(notifications(), table.selected())"
>
{{ 'Mark unseen' | i18n }}
</button>
<button tuiOption (click)="remove(notifications(), table.selected())">
{{ 'Delete' | i18n }}
</button>
</tui-data-list>
<ng-container *title>
{{ 'Notifications' | i18n }}
<ng-container *ngTemplateOutlet="button" />
</ng-container>
<section class="g-card">
<header>
{{ 'Notifications' | i18n }}
<ng-container *ngTemplateOutlet="button" />
</header>
<table #table class="g-table" [notifications]="notifications()"></table>
<ng-template #button>
<button
appearance="primary"
iconEnd="@tui.chevron-down"
tuiButton
size="xs"
type="button"
tuiDropdownOpen
tuiDropdownAlign="right"
[tuiDropdown]="dropdown"
[tuiDropdownEnabled]="!!table.selected().length"
[style.margin-inline-start]="'auto'"
[disabled]="!table.selected().length"
>
{{ 'Batch action' | i18n }}
<ng-template #dropdown let-close>
<tui-data-list (click)="close()">
<button
tuiOption
(click)="markSeen(notifications(), table.selected())"
>
{{ 'Mark seen' | i18n }}
</button>
<button
tuiOption
(click)="markUnseen(notifications(), table.selected())"
>
{{ 'Mark unseen' | i18n }}
</button>
<button
tuiOption
(click)="remove(notifications(), table.selected())"
>
{{ 'Delete' | i18n }}
</button>
</tui-data-list>
</ng-template>
</button>
</ng-template>
</h3>
<table #table class="g-table" [notifications]="notifications()"></table>
</section>
`,
styles: `
:host {
padding: 1rem;
}
:host-context(tui-root._mobile) {
header {
display: none;
}
section {
padding-block: 0;
}
}
`,
host: { class: 'g-page' },
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -64,6 +92,7 @@ import { NotificationsTableComponent } from './table.component'
NotificationsTableComponent,
TitleDirective,
i18nPipe,
NgTemplateOutlet,
],
})
export default class NotificationsComponent implements OnInit {
@@ -75,8 +104,6 @@ export default class NotificationsComponent implements OnInit {
readonly errorService = inject(ErrorService)
readonly notifications = signal<ServerNotifications | undefined>(undefined)
open = false
ngOnInit() {
this.route.queryParams.subscribe(params => {
this.router.navigate([], { relativeTo: this.route, queryParams: {} })
@@ -100,8 +127,6 @@ export default class NotificationsComponent implements OnInit {
current: ServerNotifications = [],
toUpdate: ServerNotifications = [],
) {
this.open = false
this.notifications.set(
current.map(c => ({
...c,
@@ -116,8 +141,6 @@ export default class NotificationsComponent implements OnInit {
current: ServerNotifications = [],
toUpdate: ServerNotifications = [],
) {
this.open = false
this.notifications.set(
current.map(c => ({
...c,
@@ -132,8 +155,6 @@ export default class NotificationsComponent implements OnInit {
current: ServerNotifications = [],
toDelete: ServerNotifications = [],
) {
this.open = false
this.notifications.set(
current.filter(c => !toDelete.some(n => n.id === c.id)),
)

View File

@@ -10,7 +10,7 @@ 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 { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core'
import { TuiBadge, TuiBreadcrumbs } from '@taiga-ui/kit'
import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client'
@@ -41,7 +41,7 @@ import { TitleDirective } from 'src/app/services/title.service'
</tui-breadcrumbs>
@if (interface(); as value) {
<header tuiHeader [style.margin-bottom.rem]="1">
<hgroup>
<hgroup tuiTitle>
<h3>
{{ value.name }}
<tui-badge size="l" [appearance]="getAppearance(value.type)">
@@ -60,7 +60,8 @@ import { TitleDirective } from 'src/app/services/title.service'
}
`,
styles: `
:host-context(tui-root._mobile) tui-breadcrumbs {
:host-context(tui-root._mobile) tui-breadcrumbs,
:host-context(tui-root._mobile) h3 {
display: none;
}
@@ -68,8 +69,6 @@ import { TitleDirective } from 'src/app/services/title.service'
display: flex;
align-items: center;
gap: 0.5rem;
margin: 1rem 0 0.5rem 0;
font-size: 2.4rem;
tui-badge {
text-transform: uppercase;
@@ -91,6 +90,7 @@ import { TitleDirective } from 'src/app/services/title.service'
i18nPipe,
TuiBadge,
TuiHeader,
TuiTitle,
],
})
export default class ServiceInterfaceRoute {

View File

@@ -35,7 +35,7 @@ const ERROR =
{{ 'Network Folders' | i18n }}
<tui-icon [tuiTooltip]="cifs" />
<ng-template #cifs><ng-content /></ng-template>
<button tuiButton size="s" iconStart="@tui.plus" (click)="add()">
<button tuiButton size="xs" iconStart="@tui.plus" (click)="add()">
{{ 'Open New' | i18n }}
</button>
</header>
@@ -100,11 +100,11 @@ const ERROR =
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
tr {
cursor: pointer;
@include taiga.transition(background);
@media (taiga.$tui-mouse) {
&:hover {
&:not(:has(app-placeholder)):hover {
cursor: pointer;
background: var(--tui-background-neutral-1-hover);
}
}

View File

@@ -66,11 +66,11 @@ import { BackupStatusComponent } from './status.component'
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
tr {
cursor: pointer;
@include taiga.transition(background);
@media (taiga.$tui-mouse) {
&:hover {
&:not(:has(app-placeholder)):hover {
cursor: pointer;
background: var(--tui-background-neutral-1-hover);
}
}

View File

@@ -46,7 +46,7 @@ import { SessionsTableComponent } from './table.component'
size="xs"
appearance="primary-destructive"
[style.margin-inline-start]="'auto'"
[disabled]="!(sessions()?.selected$ | async)?.length"
[disabled]="!sessions()?.selected()?.length"
(click)="terminate(others || [])"
>
{{ 'Terminate selected' | i18n }}
@@ -104,13 +104,16 @@ export default class SystemSessionsComponent {
)
async terminate(all: readonly SessionWithId[]) {
const ids = this.sessions()?.selected$.value.map(s => s.id) || []
const ids =
this.sessions()
?.selected()
.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([])
this.sessions()?.selected.set([])
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -2,8 +2,11 @@ import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
computed,
input,
Input,
OnChanges,
signal,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiIcon } from '@taiga-ui/core'
@@ -17,17 +20,36 @@ import { i18nPipe } from '@start9labs/shared'
@Component({
selector: '[sessions]',
template: `
<table [appTable]="['User Agent', 'Platform', 'Last Active']">
@for (session of sessions; track $index) {
<table
[appTable]="
single()
? ['User Agent', 'Platform', 'Last Active']
: ['Platform', 'Last Active']
"
>
@if (!single()) {
<th [style.text-indent.rem]="1.75">
<input
tuiCheckbox
size="s"
type="checkbox"
[disabled]="!sessions()"
[ngModel]="all()"
(ngModelChange)="selected.set(($event && sessions()) || [])"
/>
{{ 'User Agent' | i18n }}
</th>
}
@for (session of sessions(); track $index) {
<tr>
<td [style.padding-left.rem]="single ? null : 2.5">
<td [style.padding-left.rem]="single() ? null : 2.5">
<label>
@if (!single) {
@if (!single()) {
<input
tuiCheckbox
size="s"
type="checkbox"
[ngModel]="selected$.value.includes(session)"
[ngModel]="selected().includes(session)"
(ngModelChange)="onToggle(session)"
/>
}
@@ -43,12 +65,12 @@ import { i18nPipe } from '@start9labs/shared'
<td class="date">{{ session.lastActive | date: 'medium' }}</td>
</tr>
} @empty {
@if (sessions) {
@if (sessions()) {
<tr>
<td colspan="3">{{ 'No sessions' | i18n }}</td>
</tr>
} @else {
@for (item of single ? [''] : ['', '']; track $index) {
@for (item of single() ? [''] : ['', '']; track $index) {
<tr>
<td colspan="3">
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
@@ -144,25 +166,25 @@ import { i18nPipe } from '@start9labs/shared'
],
})
export class SessionsTableComponent<T extends Session> implements OnChanges {
readonly selected$ = new BehaviorSubject<readonly T[]>([])
readonly sessions = input<readonly T[] | null>(null)
readonly single = input(false)
@Input()
sessions: readonly T[] | null = null
@Input()
single = false
readonly selected = signal<readonly T[]>([])
readonly all = computed(
() =>
!!this.selected()?.length &&
(this.selected().length === this.sessions()?.length || null),
)
ngOnChanges() {
this.selected$.next([])
this.selected.set([])
}
onToggle(session: T) {
const selected = this.selected$.value
if (selected.includes(session)) {
this.selected$.next(selected.filter(s => s !== session))
if (this.selected().includes(session)) {
this.selected.update(selected => selected.filter(s => s !== session))
} else {
this.selected$.next([...selected, session])
this.selected.update(selected => [...selected, session])
}
}
}

View File

@@ -29,7 +29,7 @@ import { TitleDirective } from 'src/app/services/title.service'
<interface-status [style.margin-left.rem]="0.5" [public]="public()" />
</ng-container>
<header tuiHeader>
<hgroup>
<hgroup tuiTitle>
<h3>
{{ iface.name }}
<interface-status [public]="public()" />
@@ -41,20 +41,6 @@ import { TitleDirective } from 'src/app/services/title.service'
<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,
imports: [
@@ -63,6 +49,7 @@ import { TitleDirective } from 'src/app/services/title.service'
TuiButton,
TitleDirective,
TuiHeader,
TuiTitle,
InterfaceStatusComponent,
i18nPipe,
],

View File

@@ -87,12 +87,7 @@ hr {
padding: 0.5rem;
text-transform: capitalize;
box-shadow: 1px 0 var(--tui-border-normal);
backdrop-filter: blur(1rem);
background-color: color-mix(
in hsl,
var(--tui-background-base) 90%,
transparent
);
background: var(--tui-background-base);
}
.g-card {