mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Merge pull request #2632 from Start9Labs/tables
feat: add mobile view for all the tables
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
.header-title span {
|
.header-title span {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.header-title span:before {
|
.header-title span::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
|
|||||||
2
patch-db
2
patch-db
Submodule patch-db updated: 3dc11afd46...7aa53249f9
@@ -214,7 +214,7 @@ body {
|
|||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-dots:after {
|
.loading-dots::after {
|
||||||
content: '...';
|
content: '...';
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -303,4 +303,4 @@ h5,
|
|||||||
h6,
|
h6,
|
||||||
hr {
|
hr {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
tui-root._mobile &:after {
|
tui-root._mobile &::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ tui-dialog {
|
|||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
tui-opt-group[data-label^='⚠️']:before {
|
tui-opt-group[data-label^='⚠️']::before {
|
||||||
color: var(--tui-warning-fill);
|
color: var(--tui-warning-fill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
@include transition(opacity);
|
@include transition(opacity);
|
||||||
|
|
||||||
content: '';
|
content: '';
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host-context(tui-root._mobile) *:before {
|
:host-context(tui-root._mobile) *::before {
|
||||||
font-size: 1.5rem !important;
|
font-size: 1.5rem !important;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -1,44 +1,34 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { TuiDialogService, TuiSvgModule } from '@taiga-ui/core'
|
import { TuiDialogService } from '@taiga-ui/core'
|
||||||
import { TuiFadeModule } from '@taiga-ui/experimental'
|
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||||
import { BackupsCreateService } from './services/create.service'
|
|
||||||
import { BackupsRestoreService } from './services/restore.service'
|
|
||||||
import { BackupsUpcomingComponent } from './components/upcoming.component'
|
import { BackupsUpcomingComponent } from './components/upcoming.component'
|
||||||
import { TARGETS } from './modals/targets.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'
|
||||||
|
import { TARGETS } from './modals/targets.component'
|
||||||
|
import { BackupsCreateService } from './services/create.service'
|
||||||
|
import { BackupsRestoreService } from './services/restore.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<section>
|
<section>
|
||||||
<h3 class="g-title">Options</h3>
|
<h3 class="g-title">Options</h3>
|
||||||
<button
|
@for (option of options; track $index) {
|
||||||
*ngFor="let option of options"
|
<button class="g-action" (click)="option.action()">
|
||||||
class="g-action"
|
<tui-icon [icon]="option.icon" />
|
||||||
(click)="option.action()"
|
<div>
|
||||||
>
|
<strong>{{ option.name }}</strong>
|
||||||
<tui-svg [src]="option.icon"></tui-svg>
|
<div>{{ option.description }}</div>
|
||||||
<div>
|
</div>
|
||||||
<strong>{{ option.name }}</strong>
|
</button>
|
||||||
<div>{{ option.description }}</div>
|
}
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</section>
|
</section>
|
||||||
<h3 class="g-title">Upcoming Jobs</h3>
|
<h3 class="g-title">Upcoming Jobs</h3>
|
||||||
<div tuiFade class="g-hidden-scrollbar">
|
<table backupsUpcoming class="g-table"></table>
|
||||||
<table backupsUpcoming class="g-table"></table>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-page' },
|
host: { class: 'g-page' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [BackupsUpcomingComponent, TuiIconModule],
|
||||||
CommonModule,
|
|
||||||
TuiSvgModule,
|
|
||||||
TuiFadeModule,
|
|
||||||
BackupsUpcomingComponent,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export default class BackupsComponent {
|
export default class BackupsComponent {
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
|
|||||||
@@ -25,51 +25,94 @@ import { UnknownDisk } from 'src/app/services/api/api.types'
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let disk of backupsPhysical; else: loading; empty: blank">
|
@for (disk of backupsPhysical; track $index) {
|
||||||
<td>
|
|
||||||
{{ disk.vendor || 'unknown make' }},
|
|
||||||
{{ disk.model || 'unknown model' }}
|
|
||||||
</td>
|
|
||||||
<td>{{ disk.label }}</td>
|
|
||||||
<td>{{ disk.capacity | convertBytes }}</td>
|
|
||||||
<td>{{ disk.used ? (disk.used | convertBytes) : 'Unknown' }}</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
tuiButton
|
|
||||||
size="xs"
|
|
||||||
icon="tuiIconPlus"
|
|
||||||
(click)="add.emit(disk)"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<ng-template #loading>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5">
|
<td class="model">
|
||||||
<div class="tui-skeleton">Loading</div>
|
{{ disk.vendor || 'unknown make' }},
|
||||||
|
{{ disk.model || 'unknown model' }}
|
||||||
|
</td>
|
||||||
|
<td class="title">{{ disk.label }}</td>
|
||||||
|
<td class="capacity">{{ disk.capacity | convertBytes }}</td>
|
||||||
|
<td class="used">
|
||||||
|
{{ disk.used ? (disk.used | convertBytes) : 'Unknown' }}
|
||||||
|
</td>
|
||||||
|
<td class="actions">
|
||||||
|
<button
|
||||||
|
tuiButton
|
||||||
|
size="xs"
|
||||||
|
iconLeft="tuiIconPlus"
|
||||||
|
(click)="add.emit(disk)"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
} @empty {
|
||||||
<ng-template #blank>
|
@if (backupsPhysical) {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
To add a new physical backup target, connect the drive and click
|
To add a new physical backup target, connect the drive and click
|
||||||
refresh.
|
refresh.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
} @else {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
`,
|
`,
|
||||||
|
styles: `
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
tr {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:only-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model {
|
||||||
|
order: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
order: 2;
|
||||||
|
padding: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
order: 3;
|
||||||
|
grid-column: span 2;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity {
|
||||||
|
order: 4;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: 'Capacity: ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.used {
|
||||||
|
order: 5;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: 'Used: ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [TuiButtonModule, UnitConversionPipesModule],
|
||||||
CommonModule,
|
|
||||||
TuiForModule,
|
|
||||||
TuiSvgModule,
|
|
||||||
TuiButtonModule,
|
|
||||||
UnitConversionPipesModule,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class BackupsPhysicalComponent {
|
export class BackupsPhysicalComponent {
|
||||||
@Input()
|
@Input()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -7,13 +6,8 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { TuiForModule } from '@taiga-ui/cdk'
|
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
|
||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
|
||||||
import {
|
|
||||||
TuiDialogOptions,
|
|
||||||
TuiDialogService,
|
|
||||||
TuiSvgModule,
|
|
||||||
} from '@taiga-ui/core'
|
|
||||||
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
||||||
import { filter, map, Subject, switchMap } from 'rxjs'
|
import { filter, map, Subject, switchMap } from 'rxjs'
|
||||||
import { BackupTarget } from 'src/app/services/api/api.types'
|
import { BackupTarget } from 'src/app/services/api/api.types'
|
||||||
@@ -32,69 +26,114 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let target of backupsTargets; else: loading; empty: blank">
|
@for (target of backupsTargets; track $index) {
|
||||||
<td>{{ target.name }}</td>
|
<tr>
|
||||||
<td>
|
<td class="title">{{ target.name }}</td>
|
||||||
<tui-svg [src]="target.type | getBackupIcon"></tui-svg>
|
<td class="type">
|
||||||
{{ target.type | titlecase }}
|
<tui-icon [icon]="target.type | getBackupIcon" />
|
||||||
</td>
|
{{ target.type }}
|
||||||
<td>
|
</td>
|
||||||
<tui-svg
|
<td class="available">
|
||||||
[src]="target.mountable ? 'tuiIconCheck' : 'tuiIconClose'"
|
<tui-icon
|
||||||
[style.color]="
|
[icon]="target.mountable ? 'tuiIconCheck' : 'tuiIconClose'"
|
||||||
target.mountable ? 'var(--tui-positive)' : 'var(--tui-negative)'
|
[class]="target.mountable ? 'g-success' : 'g-error'"
|
||||||
"
|
/>
|
||||||
></tui-svg>
|
</td>
|
||||||
</td>
|
<td class="path">{{ target.path }}</td>
|
||||||
<td>{{ target.path }}</td>
|
<td class="actions">
|
||||||
<td>
|
<button
|
||||||
<button
|
tuiIconButton
|
||||||
tuiIconButton
|
size="xs"
|
||||||
size="xs"
|
appearance="icon"
|
||||||
appearance="icon"
|
iconLeft="tuiIconEdit2"
|
||||||
iconLeft="tuiIconEdit2"
|
(click)="update.emit(target)"
|
||||||
(click)="update.emit(target)"
|
>
|
||||||
>
|
Update
|
||||||
Update
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
tuiIconButton
|
||||||
tuiIconButton
|
size="xs"
|
||||||
size="xs"
|
appearance="icon"
|
||||||
appearance="icon"
|
iconLeft="tuiIconTrash2"
|
||||||
iconLeft="tuiIconTrash2"
|
(click)="delete$.next(target.id)"
|
||||||
(click)="delete$.next(target.id)"
|
>
|
||||||
>
|
Delete
|
||||||
Delete
|
</button>
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<ng-template #loading>
|
|
||||||
<tr *ngFor="let i of ['', '']">
|
|
||||||
<td colspan="5">
|
|
||||||
<div class="tui-skeleton">Loading</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
} @empty {
|
||||||
<ng-template #blank>
|
@if (backupsTargets) {
|
||||||
<tr><td colspan="5">No saved backup targets.</td></tr>
|
<tr><td colspan="5">No saved backup targets.</td></tr>
|
||||||
</ng-template>
|
} @else {
|
||||||
|
@for (i of ['', '']; track $index) {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
`,
|
`,
|
||||||
|
styles: `
|
||||||
|
tui-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: sub;
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
tr {
|
||||||
|
grid-template-columns: 1.5rem 2fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:only-child {
|
||||||
|
grid-column: span 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
order: 1;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
grid-column: span 3;
|
||||||
|
|
||||||
|
tui-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.available {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
order: 3;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
order: 4;
|
||||||
|
padding: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
order: 5;
|
||||||
|
color: var(--tui-text-03);
|
||||||
|
grid-column: span 2;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [TuiButtonModule, GetBackupIconPipe, TuiIconModule],
|
||||||
CommonModule,
|
|
||||||
TuiForModule,
|
|
||||||
TuiSvgModule,
|
|
||||||
TuiButtonModule,
|
|
||||||
GetBackupIconPipe,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class BackupsTargetsComponent {
|
export class BackupsTargetsComponent {
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
|
|
||||||
readonly delete$ = new Subject<string>()
|
readonly delete$ = new Subject<string>()
|
||||||
readonly update$ = new Subject<BackupTarget>()
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
backupsTargets: readonly BackupTarget[] | null = null
|
backupsTargets: readonly BackupTarget[] | null = null
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { DatePipe } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { TuiForModule } from '@taiga-ui/cdk'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { TuiSvgModule } from '@taiga-ui/core'
|
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||||
|
import { CronJob } from 'cron'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { from, map } from 'rxjs'
|
import { from, map } from 'rxjs'
|
||||||
import { CronJob } from 'cron'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -20,58 +20,101 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
|||||||
<th>Packages</th>
|
<th>Packages</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody *ngIf="current$ | async as current">
|
@if (current(); as current) {
|
||||||
<tr *ngFor="let job of upcoming$ | async; else: loading; empty: blank">
|
<tbody>
|
||||||
<td>
|
@for (job of upcoming(); track $index) {
|
||||||
<span
|
<tr>
|
||||||
*ngIf="current.id === job.id; else notRunning"
|
<td class="date">
|
||||||
[style.color]="'var(--tui-positive)'"
|
@if (current.id === job.id) {
|
||||||
>
|
<span [style.color]="'var(--tui-positive)'">Running</span>
|
||||||
Running
|
} @else {
|
||||||
</span>
|
{{ job.next | date: 'medium' }}
|
||||||
<ng-template #notRunning>
|
}
|
||||||
{{ job.next | date: 'MMM d, y, h:mm a' }}
|
</td>
|
||||||
</ng-template>
|
<td class="name">{{ job.name }}</td>
|
||||||
</td>
|
<td>
|
||||||
<td>{{ job.name }}</td>
|
<tui-icon [icon]="job.target.type | getBackupIcon" />
|
||||||
<td>
|
{{ job.target.name }}
|
||||||
<tui-svg [src]="job.target.type | getBackupIcon"></tui-svg>
|
</td>
|
||||||
{{ job.target.name }}
|
<td class="packages">Packages: {{ job.packageIds.length }}</td>
|
||||||
</td>
|
</tr>
|
||||||
<td>Packages: {{ job.packageIds.length }}</td>
|
} @empty {
|
||||||
</tr>
|
@if (upcoming()) {
|
||||||
<ng-template #blank>
|
<tr>
|
||||||
<tr><td colspan="5">You have no active or upcoming backup jobs</td></tr>
|
<td colspan="5">You have no active or upcoming backup jobs</td>
|
||||||
</ng-template>
|
</tr>
|
||||||
<ng-template #loading>
|
} @else {
|
||||||
<tr *ngFor="let row of ['', '']">
|
@for (row of ['', '']; track $index) {
|
||||||
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
<tr>
|
||||||
</tr>
|
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
||||||
</ng-template>
|
</tr>
|
||||||
</tbody>
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
:host {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
order: 1;
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
tui-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: sub;
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
.date {
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packages {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TuiForModule, TuiSvgModule, GetBackupIconPipe],
|
imports: [GetBackupIconPipe, DatePipe, TuiIconModule],
|
||||||
})
|
})
|
||||||
export class BackupsUpcomingComponent {
|
export class BackupsUpcomingComponent {
|
||||||
readonly current$ = inject(PatchDB<DataModel>)
|
readonly current = toSignal(
|
||||||
.watch$('serverInfo', 'statusInfo', 'currentBackup', 'job')
|
inject(PatchDB<DataModel>)
|
||||||
.pipe(map(job => job || {}))
|
.watch$('serverInfo', 'statusInfo', 'currentBackup', 'job')
|
||||||
|
.pipe(map(job => job || {})),
|
||||||
|
)
|
||||||
|
|
||||||
readonly upcoming$ = from(inject(ApiService).getBackupJobs({})).pipe(
|
readonly upcoming = toSignal(
|
||||||
map(jobs =>
|
from(inject(ApiService).getBackupJobs({})).pipe(
|
||||||
jobs
|
map(jobs =>
|
||||||
.map(job => {
|
jobs
|
||||||
const nextDate = new CronJob(job.cron, () => {}).nextDate()
|
.map(job => {
|
||||||
|
const nextDate = new CronJob(job.cron, () => {}).nextDate()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...job,
|
...job,
|
||||||
next: nextDate.toISO(),
|
next: nextDate.toISO(),
|
||||||
diff: nextDate.diffNow().milliseconds,
|
diff: nextDate.diffNow().milliseconds,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.diff - b.diff),
|
.sort((a, b) => a.diff - b.diff),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
import { ALWAYS_FALSE_HANDLER, ALWAYS_TRUE_HANDLER } from '@taiga-ui/cdk'
|
||||||
|
import { TuiDialogService, TuiLinkModule } from '@taiga-ui/core'
|
||||||
import {
|
import {
|
||||||
ALWAYS_FALSE_HANDLER,
|
TuiButtonModule,
|
||||||
ALWAYS_TRUE_HANDLER,
|
TuiCheckboxModule,
|
||||||
TuiForModule,
|
TuiIconModule,
|
||||||
} from '@taiga-ui/cdk'
|
} from '@taiga-ui/experimental'
|
||||||
import { TuiDialogService, TuiLinkModule, TuiSvgModule } from '@taiga-ui/core'
|
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||||
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
|
import { REPORT } from 'src/app/components/report.component'
|
||||||
import { TuiCheckboxModule } from '@taiga-ui/kit'
|
|
||||||
import { BehaviorSubject } from 'rxjs'
|
|
||||||
import { BackupRun } from 'src/app/services/api/api.types'
|
import { BackupRun } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { REPORT } from 'src/app/components/report.component'
|
|
||||||
import { DurationPipe } from '../pipes/duration.pipe'
|
import { DurationPipe } from '../pipes/duration.pipe'
|
||||||
import { HasErrorPipe } from '../pipes/has-error.pipe'
|
|
||||||
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
||||||
|
import { HasErrorPipe } from '../pipes/has-error.pipe'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<ng-container *ngIf="loading$ | async"></ng-container>
|
|
||||||
<h3 class="g-title">
|
<h3 class="g-title">
|
||||||
Past Events
|
Past Events
|
||||||
<button
|
<button
|
||||||
@@ -33,83 +35,133 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
|||||||
Delete Selected
|
Delete Selected
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
<div tuiFade class="g-hidden-scrollbar">
|
<table class="g-table">
|
||||||
<table class="g-table">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
|
<th>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
size="s"
|
||||||
|
tuiCheckbox
|
||||||
|
[disabled]="!selected.length"
|
||||||
|
[ngModel]="all"
|
||||||
|
(ngModelChange)="toggle()"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th>Started At</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Job</th>
|
||||||
|
<th>Result</th>
|
||||||
|
<th>Target</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (run of runs(); track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<td class="checkbox">
|
||||||
<tui-checkbox
|
<input
|
||||||
[disabled]="!selected.length"
|
type="checkbox"
|
||||||
[ngModel]="all"
|
tuiCheckbox
|
||||||
(ngModelChange)="toggle()"
|
size="s"
|
||||||
></tui-checkbox>
|
[(ngModel)]="selected[$index]"
|
||||||
</th>
|
/>
|
||||||
<th>Started At</th>
|
</td>
|
||||||
<th>Duration</th>
|
<td class="date">{{ run.startedAt | date: 'medium' }}</td>
|
||||||
<th>Result</th>
|
<td class="duration">
|
||||||
<th>Job</th>
|
{{ run.startedAt | duration: run.completedAt }} minutes
|
||||||
<th>Target</th>
|
</td>
|
||||||
</tr>
|
<td class="title">{{ run.job.name || 'No job' }}</td>
|
||||||
</thead>
|
<td class="result">
|
||||||
<tbody>
|
@if (run.report | hasError) {
|
||||||
<tr
|
<tui-icon icon="tuiIconClose" class="g-error" />
|
||||||
*ngFor="
|
} @else {
|
||||||
let run of runs;
|
<tui-icon icon="tuiIconCheck" class="g-success" />
|
||||||
let index = index;
|
}
|
||||||
else: loading;
|
|
||||||
empty: blank
|
|
||||||
"
|
|
||||||
[style.background]="selected[index] ? 'var(--tui-clear)' : ''"
|
|
||||||
>
|
|
||||||
<td><tui-checkbox [(ngModel)]="selected[index]"></tui-checkbox></td>
|
|
||||||
<td>{{ run.startedAt | date: 'medium' }}</td>
|
|
||||||
<td>{{ run.startedAt | duration: run.completedAt }} Minutes</td>
|
|
||||||
<td>
|
|
||||||
<tui-svg
|
|
||||||
*ngIf="run.report | hasError; else noError"
|
|
||||||
src="tuiIconClose"
|
|
||||||
[style.color]="'var(--tui-negative)'"
|
|
||||||
></tui-svg>
|
|
||||||
<ng-template #noError>
|
|
||||||
<tui-svg
|
|
||||||
src="tuiIconCheck"
|
|
||||||
[style.color]="'var(--tui-positive)'"
|
|
||||||
></tui-svg>
|
|
||||||
</ng-template>
|
|
||||||
<button tuiLink (click)="showReport(run)">Report</button>
|
<button tuiLink (click)="showReport(run)">Report</button>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ run.job.name || 'No job' }}</td>
|
<td [style.grid-column]="'span 2'">
|
||||||
<td>
|
<tui-icon [icon]="run.job.target.type | getBackupIcon" />
|
||||||
<tui-svg [src]="run.job.target.type | getBackupIcon"></tui-svg>
|
|
||||||
{{ run.job.target.name }}
|
{{ run.job.target.name }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template #loading>
|
} @empty {
|
||||||
<tr *ngFor="let row of ['', '', '']">
|
@if (runs()) {
|
||||||
<td colspan="6"><div class="tui-skeleton">Loading</div></td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template #blank>
|
|
||||||
<tr><td colspan="6">No backups have been run yet.</td></tr>
|
<tr><td colspan="6">No backups have been run yet.</td></tr>
|
||||||
</ng-template>
|
} @else {
|
||||||
</tbody>
|
@for (row of ['', '']; track $index) {
|
||||||
</table>
|
<tr>
|
||||||
</div>
|
<td colspan="6"><div class="tui-skeleton">Loading</div></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||||
|
|
||||||
|
tui-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: sub;
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[tuiCheckbox] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
tr {
|
||||||
|
grid-template-columns: 1fr 7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:only-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
@include fullsize();
|
||||||
|
|
||||||
|
[tuiCheckbox] {
|
||||||
|
@include fullsize();
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date,
|
||||||
|
.duration {
|
||||||
|
order: 1;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration,
|
||||||
|
.result {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
TuiForModule,
|
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
TuiCheckboxModule,
|
TuiIconModule,
|
||||||
TuiSvgModule,
|
|
||||||
TuiLinkModule,
|
TuiLinkModule,
|
||||||
TuiFadeModule,
|
|
||||||
DurationPipe,
|
DurationPipe,
|
||||||
HasErrorPipe,
|
HasErrorPipe,
|
||||||
GetBackupIconPipe,
|
GetBackupIconPipe,
|
||||||
|
TuiCheckboxModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class BackupsHistoryModal {
|
export class BackupsHistoryModal {
|
||||||
@@ -118,9 +170,7 @@ export class BackupsHistoryModal {
|
|||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
|
|
||||||
readonly loading$ = new BehaviorSubject(true)
|
runs = signal<BackupRun[] | null>(null)
|
||||||
|
|
||||||
runs: BackupRun[] | null = null
|
|
||||||
selected: boolean[] = []
|
selected: boolean[] = []
|
||||||
|
|
||||||
get all(): boolean | null {
|
get all(): boolean | null {
|
||||||
@@ -143,13 +193,11 @@ export class BackupsHistoryModal {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
try {
|
try {
|
||||||
this.runs = await this.api.getBackupRuns({})
|
this.runs.set(await this.api.getBackupRuns({}))
|
||||||
this.selected = this.runs.map(ALWAYS_FALSE_HANDLER)
|
this.selected = this.runs()?.map(ALWAYS_FALSE_HANDLER) || []
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.runs = []
|
this.runs.set([])
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
|
||||||
this.loading$.next(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,12 +205,12 @@ export class BackupsHistoryModal {
|
|||||||
const loader = this.loader.open('Deleting...').subscribe()
|
const loader = this.loader.open('Deleting...').subscribe()
|
||||||
const ids = this.selected
|
const ids = this.selected
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((_, i) => this.runs?.[i].id || '')
|
.map((_, i) => this.runs()?.[i].id || '')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.deleteBackupRuns({ ids })
|
await this.api.deleteBackupRuns({ ids })
|
||||||
this.runs = this.runs?.filter(r => !ids.includes(r.id)) || []
|
this.runs.set(this.runs()?.filter(r => !ids.includes(r.id)) || [])
|
||||||
this.selected = this.runs.map(ALWAYS_FALSE_HANDLER)
|
this.selected = this.runs()?.map(ALWAYS_FALSE_HANDLER) || []
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { Component, inject, OnInit } from '@angular/core'
|
import { Component, inject, OnInit } from '@angular/core'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { TuiForModule } from '@taiga-ui/cdk'
|
|
||||||
import {
|
import {
|
||||||
TuiDialogOptions,
|
TuiDialogOptions,
|
||||||
TuiDialogService,
|
TuiDialogService,
|
||||||
TuiNotificationModule,
|
TuiNotificationModule,
|
||||||
TuiSvgModule,
|
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
|
||||||
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
||||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||||
import { BehaviorSubject, filter } from 'rxjs'
|
import { BehaviorSubject, filter } from 'rxjs'
|
||||||
import { BackupJob } from 'src/app/services/api/api.types'
|
import { BackupJob } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { BackupJobBuilder } from '../utils/job-builder'
|
|
||||||
import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe'
|
|
||||||
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
||||||
|
import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe'
|
||||||
|
import { BackupJobBuilder } from '../utils/job-builder'
|
||||||
import { EDIT } from './edit.component'
|
import { EDIT } from './edit.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -39,27 +36,27 @@ import { EDIT } from './edit.component'
|
|||||||
Create New Job
|
Create New Job
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="g-hidden-scrollbar" tuiFade>
|
<table class="g-table">
|
||||||
<table class="g-table">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Target</th>
|
||||||
|
<th>Packages</th>
|
||||||
|
<th>Schedule</th>
|
||||||
|
<th [style.width.rem]="3.5"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (job of jobs; track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<td class="title">{{ job.name }}</td>
|
||||||
<th>Target</th>
|
<td class="target">
|
||||||
<th>Packages</th>
|
<tui-icon [icon]="job.target.type | getBackupIcon" />
|
||||||
<th>Schedule</th>
|
|
||||||
<th [style.width.rem]="3.5"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let job of jobs || null; else: loading; empty: blank">
|
|
||||||
<td>{{ job.name }}</td>
|
|
||||||
<td>
|
|
||||||
<tui-svg [src]="job.target.type | getBackupIcon"></tui-svg>
|
|
||||||
{{ job.target.name }}
|
{{ job.target.name }}
|
||||||
</td>
|
</td>
|
||||||
<td>Packages: {{ job.packageIds.length }}</td>
|
<td class="packages">Packages: {{ job.packageIds.length }}</td>
|
||||||
<td>{{ (job.cron | toHumanCron).message }}</td>
|
<td class="schedule">{{ (job.cron | toHumanCron).message }}</td>
|
||||||
<td>
|
<td class="actions">
|
||||||
<button
|
<button
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
appearance="icon"
|
appearance="icon"
|
||||||
@@ -76,26 +73,68 @@ import { EDIT } from './edit.component'
|
|||||||
></button>
|
></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template #loading>
|
} @empty {
|
||||||
<tr *ngFor="let i of ['', '']">
|
@if (jobs) {
|
||||||
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template #blank>
|
|
||||||
<tr><td colspan="5">No jobs found.</td></tr>
|
<tr><td colspan="5">No jobs found.</td></tr>
|
||||||
</ng-template>
|
} @else {
|
||||||
</tbody>
|
@for (i of ['', '']; track $index) {
|
||||||
</table>
|
<tr>
|
||||||
</div>
|
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
tui-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: sub;
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
tr {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:only-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
order: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
order: 2;
|
||||||
|
padding: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target {
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packages {
|
||||||
|
order: 4;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule {
|
||||||
|
order: 5;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
TuiForModule,
|
|
||||||
TuiNotificationModule,
|
TuiNotificationModule,
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
TuiSvgModule,
|
TuiIconModule,
|
||||||
TuiFadeModule,
|
|
||||||
ToHumanCronPipe,
|
ToHumanCronPipe,
|
||||||
GetBackupIconPipe,
|
GetBackupIconPipe,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, inject, OnInit } from '@angular/core'
|
import { Component, inject, OnInit, signal } from '@angular/core'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { CT } from '@start9labs/start-sdk'
|
import { CT } from '@start9labs/start-sdk'
|
||||||
import { TuiNotificationModule } from '@taiga-ui/core'
|
import { TuiNotificationModule } from '@taiga-ui/core'
|
||||||
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||||
import { BehaviorSubject } from 'rxjs'
|
|
||||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
|
||||||
import {
|
|
||||||
cifsSpec,
|
|
||||||
diskBackupTargetSpec,
|
|
||||||
dropboxSpec,
|
|
||||||
googleDriveSpec,
|
|
||||||
remoteBackupTargetSpec,
|
|
||||||
} from '../types/target'
|
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
|
||||||
import {
|
import {
|
||||||
BackupTarget,
|
BackupTarget,
|
||||||
BackupTargetType,
|
BackupTargetType,
|
||||||
@@ -23,13 +13,21 @@ import {
|
|||||||
UnknownDisk,
|
UnknownDisk,
|
||||||
} from 'src/app/services/api/api.types'
|
} from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { BackupConfig } from '../types/backup-config'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
import { BackupsPhysicalComponent } from '../components/physical.component'
|
import { BackupsPhysicalComponent } from '../components/physical.component'
|
||||||
import { BackupsTargetsComponent } from '../components/targets.component'
|
import { BackupsTargetsComponent } from '../components/targets.component'
|
||||||
|
import { BackupConfig } from '../types/backup-config'
|
||||||
|
import {
|
||||||
|
cifsSpec,
|
||||||
|
diskBackupTargetSpec,
|
||||||
|
dropboxSpec,
|
||||||
|
googleDriveSpec,
|
||||||
|
remoteBackupTargetSpec,
|
||||||
|
} from '../types/target'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<ng-container *ngIf="loading$ | async"></ng-container>
|
|
||||||
<tui-notification>
|
<tui-notification>
|
||||||
Backup targets are physical or virtual locations for storing encrypted
|
Backup targets are physical or virtual locations for storing encrypted
|
||||||
backups. They can be physical drives plugged into your server, shared
|
backups. They can be physical drives plugged into your server, shared
|
||||||
@@ -45,31 +43,32 @@ import { BackupsTargetsComponent } from '../components/targets.component'
|
|||||||
</tui-notification>
|
</tui-notification>
|
||||||
<h3 class="g-title">
|
<h3 class="g-title">
|
||||||
Unknown Physical Drives
|
Unknown Physical Drives
|
||||||
<button tuiButton size="s" icon="tuiIconRefreshCw" (click)="refresh()">
|
<button
|
||||||
|
tuiButton
|
||||||
|
size="s"
|
||||||
|
iconLeft="tuiIconRefreshCw"
|
||||||
|
(click)="refresh()"
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="g-hidden-scrollbar" tuiFade>
|
<table
|
||||||
<table
|
class="g-table"
|
||||||
class="g-table"
|
[backupsPhysical]="targets()?.unknownDisks || null"
|
||||||
[backupsPhysical]="targets?.unknownDisks || null"
|
(add)="addPhysical($event)"
|
||||||
(add)="addPhysical($event)"
|
></table>
|
||||||
></table>
|
|
||||||
</div>
|
|
||||||
<h3 class="g-title">
|
<h3 class="g-title">
|
||||||
Saved Targets
|
Saved Targets
|
||||||
<button tuiButton size="s" icon="tuiIconPlus" (click)="addRemote()">
|
<button tuiButton size="s" iconLeft="tuiIconPlus" (click)="addRemote()">
|
||||||
Add Target
|
Add Target
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="g-hidden-scrollbar" tuiFade>
|
<table
|
||||||
<table
|
class="g-table"
|
||||||
class="g-table"
|
[backupsTargets]="targets()?.saved || null"
|
||||||
[backupsTargets]="targets?.saved || null"
|
(delete)="onDelete($event)"
|
||||||
(delete)="onDelete($event)"
|
(update)="onUpdate($event)"
|
||||||
(update)="onUpdate($event)"
|
></table>
|
||||||
></table>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
@@ -78,7 +77,6 @@ import { BackupsTargetsComponent } from '../components/targets.component'
|
|||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
BackupsPhysicalComponent,
|
BackupsPhysicalComponent,
|
||||||
BackupsTargetsComponent,
|
BackupsTargetsComponent,
|
||||||
TuiFadeModule,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class BackupsTargetsModal implements OnInit {
|
export class BackupsTargetsModal implements OnInit {
|
||||||
@@ -87,25 +85,20 @@ export class BackupsTargetsModal implements OnInit {
|
|||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
|
|
||||||
readonly loading$ = new BehaviorSubject(true)
|
targets = signal<RR.GetBackupTargetsRes | null>(null)
|
||||||
|
|
||||||
targets?: RR.GetBackupTargetsRes
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.refresh()
|
this.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
this.loading$.next(true)
|
this.targets.set(null)
|
||||||
this.targets = undefined
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.targets = await this.api.getBackupTargets({})
|
this.targets.set(await this.api.getBackupTargets({}))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
this.targets = { unknownDisks: [], saved: [] }
|
this.targets.set({ unknownDisks: [], saved: [] })
|
||||||
} finally {
|
|
||||||
this.loading$.next(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +107,7 @@ export class BackupsTargetsModal implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.removeBackupTarget({ id })
|
await this.api.removeBackupTarget({ id })
|
||||||
this.setTargets(this.targets?.saved.filter(a => a.id !== id))
|
this.setTargets(this.targets()?.saved.filter(a => a.id !== id))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -158,8 +151,8 @@ export class BackupsTargetsModal implements OnInit {
|
|||||||
...value,
|
...value,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
this.setTargets(
|
this.setTargets(
|
||||||
this.targets?.saved.concat(response),
|
this.targets()?.saved.concat(response),
|
||||||
this.targets?.unknownDisks.filter(a => a !== disk),
|
this.targets()?.unknownDisks.filter(a => a !== disk),
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}),
|
}),
|
||||||
@@ -221,10 +214,10 @@ export class BackupsTargetsModal implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setTargets(
|
private setTargets(
|
||||||
saved: BackupTarget[] = this.targets?.saved || [],
|
saved: BackupTarget[] = this.targets()?.saved || [],
|
||||||
unknownDisks: UnknownDisk[] = this.targets?.unknownDisks || [],
|
unknownDisks: UnknownDisk[] = this.targets()?.unknownDisks || [],
|
||||||
) {
|
) {
|
||||||
this.targets = { unknownDisks, saved }
|
this.targets.set({ unknownDisks, saved })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSpec(target: BackupTarget) {
|
private async getSpec(target: BackupTarget) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -0.5rem;
|
inset: -0.5rem;
|
||||||
@@ -53,7 +53,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
|||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 0.25rem;
|
width: 0.25rem;
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ import { TimeService } from 'src/app/services/time.service'
|
|||||||
color: var(--tui-text-01);
|
color: var(--tui-text-01);
|
||||||
padding-top: 0.4rem;
|
padding-top: 0.4rem;
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: attr(data-unit);
|
content: attr(data-unit);
|
||||||
font-size: 0.5rem;
|
font-size: 0.5rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { tuiPure } from '@taiga-ui/cdk'
|
import { tuiPure } from '@taiga-ui/cdk'
|
||||||
import { TuiLinkModule, TuiSvgModule } from '@taiga-ui/core'
|
import { TuiLinkModule } from '@taiga-ui/core'
|
||||||
|
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||||
import { TuiLineClampModule } from '@taiga-ui/kit'
|
import { TuiLineClampModule } from '@taiga-ui/kit'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { first, Observable } from 'rxjs'
|
import { first, Observable } from 'rxjs'
|
||||||
@@ -20,22 +21,24 @@ import { toRouterLink } from 'src/app/utils/to-router-link'
|
|||||||
@Component({
|
@Component({
|
||||||
selector: '[notificationItem]',
|
selector: '[notificationItem]',
|
||||||
template: `
|
template: `
|
||||||
<td [style.padding-top.rem]="0.4"><ng-content /></td>
|
<td class="checkbox"><ng-content /></td>
|
||||||
<td>{{ notificationItem.createdAt | date: 'MMM d, y, h:mm a' }}</td>
|
<td class="date">
|
||||||
<td [style.color]="color">
|
{{ notificationItem.createdAt | date: 'medium' }}
|
||||||
<tui-svg [src]="icon" />
|
</td>
|
||||||
|
<td class="title" [style.color]="color">
|
||||||
|
<tui-icon [icon]="icon" [style.font-size.rem]="1" />
|
||||||
{{ notificationItem.title }}
|
{{ notificationItem.title }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="service">
|
||||||
<a
|
@if (manifest$ | async; as manifest) {
|
||||||
*ngIf="manifest$ | async as manifest; else na"
|
<a tuiLink [routerLink]="getLink(manifest.id)">
|
||||||
[routerLink]="getLink(manifest.id)"
|
{{ manifest.title }}
|
||||||
>
|
</a>
|
||||||
{{ manifest.title }}
|
} @else {
|
||||||
</a>
|
N/A
|
||||||
<ng-template #na>N/A</ng-template>
|
}
|
||||||
</td>
|
</td>
|
||||||
<td [style.padding-bottom.rem]="0.5">
|
<td class="content">
|
||||||
<tui-line-clamp
|
<tui-line-clamp
|
||||||
style="pointer-events: none"
|
style="pointer-events: none"
|
||||||
[linesLimit]="4"
|
[linesLimit]="4"
|
||||||
@@ -43,20 +46,16 @@ import { toRouterLink } from 'src/app/utils/to-router-link'
|
|||||||
[content]="notificationItem.message"
|
[content]="notificationItem.message"
|
||||||
(overflownChange)="overflow = $event"
|
(overflownChange)="overflow = $event"
|
||||||
/>
|
/>
|
||||||
<button
|
@if (overflow) {
|
||||||
*ngIf="overflow"
|
<button tuiLink (click)="service.viewFull(notificationItem)">
|
||||||
tuiLink
|
View Full
|
||||||
(click)="service.viewFull(notificationItem)"
|
</button>
|
||||||
>
|
}
|
||||||
View Full
|
@if (notificationItem.code === 1) {
|
||||||
</button>
|
<button tuiLink (click)="service.viewReport(notificationItem)">
|
||||||
<button
|
View Report
|
||||||
*ngIf="notificationItem.code === 1"
|
</button>
|
||||||
tuiLink
|
}
|
||||||
(click)="service.viewReport(notificationItem)"
|
|
||||||
>
|
|
||||||
View Report
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -65,21 +64,58 @@ import { toRouterLink } from 'src/app/utils/to-router-link'
|
|||||||
'[class._new]': '!notificationItem.read',
|
'[class._new]': '!notificationItem.read',
|
||||||
},
|
},
|
||||||
styles: `
|
styles: `
|
||||||
:host._new {
|
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||||
background: var(--tui-clear);
|
|
||||||
|
:host {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
|
&._new {
|
||||||
|
background: var(--tui-clear) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
.checkbox {
|
||||||
|
@include fullsize();
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
order: 1;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service:not(:has(a)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
TuiLineClampModule,
|
TuiLineClampModule,
|
||||||
TuiSvgModule,
|
|
||||||
TuiLinkModule,
|
TuiLinkModule,
|
||||||
|
TuiIconModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class NotificationItemComponent {
|
export class NotificationItemComponent {
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ import { NotificationItemComponent } from './item.component'
|
|||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
`,
|
`,
|
||||||
|
styles: `
|
||||||
|
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) input {
|
||||||
|
@include fullsize();
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { Domain } from 'src/app/services/patch-db/data-model'
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Domain</th>
|
<th>Domain</th>
|
||||||
<th>Added</th>
|
|
||||||
<th>DDNS Provider</th>
|
<th>DDNS Provider</th>
|
||||||
<th>Network Strategy</th>
|
<th>Network Strategy</th>
|
||||||
<th>Used By</th>
|
<th>Used By</th>
|
||||||
@@ -25,36 +24,87 @@ import { Domain } from 'src/app/services/patch-db/data-model'
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let domain of domains">
|
@for (domain of domains; track $index) {
|
||||||
<td>{{ domain.value }}</td>
|
<tr *ngFor="let domain of domains">
|
||||||
<td>{{ domain.createdAt | date: 'short' }}</td>
|
<td class="title">{{ domain.value }}</td>
|
||||||
<td>{{ domain.provider }}</td>
|
<td class="provider">{{ domain.provider }}</td>
|
||||||
<td>{{ getStrategy(domain) }}</td>
|
<td class="strategy">{{ getStrategy(domain) }}</td>
|
||||||
<td>
|
<td class="used">
|
||||||
<button
|
@if (domain.usedBy.length; as qty) {
|
||||||
*ngIf="domain.usedBy.length as qty; else unused"
|
<button tuiLink (click)="onUsedBy(domain)">
|
||||||
tuiLink
|
Used by: {{ qty }}
|
||||||
(click)="onUsedBy(domain)"
|
</button>
|
||||||
>
|
} @else {
|
||||||
Interfaces: {{ qty }}
|
N/A
|
||||||
</button>
|
}
|
||||||
<ng-template #unused>N/A</ng-template>
|
</td>
|
||||||
</td>
|
<td class="actions">
|
||||||
<td>
|
<button
|
||||||
<button
|
tuiIconButton
|
||||||
tuiIconButton
|
size="xs"
|
||||||
size="xs"
|
appearance="icon"
|
||||||
appearance="icon"
|
iconLeft="tuiIconTrash2"
|
||||||
iconLeft="tuiIconTrash2"
|
(click)="delete.emit(domain)"
|
||||||
[style.display]="'flex'"
|
>
|
||||||
(click)="delete.emit(domain)"
|
Delete
|
||||||
>
|
</button>
|
||||||
Delete
|
</td>
|
||||||
</button>
|
</tr>
|
||||||
</td>
|
} @empty {
|
||||||
</tr>
|
<tr><td colspan="6">No domains</td></tr>
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
`,
|
`,
|
||||||
|
styles: `
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
tr {
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:only-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
order: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
order: 2;
|
||||||
|
padding: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy {
|
||||||
|
order: 3;
|
||||||
|
grid-column: span 2;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: 'Strategy: ';
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider {
|
||||||
|
order: 4;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: 'DDNS: ';
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.used {
|
||||||
|
order: 5;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&:not(:has(button)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule, TuiButtonModule, TuiLinkModule],
|
imports: [CommonModule, TuiButtonModule, TuiLinkModule],
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
|
||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
|
||||||
import {
|
|
||||||
TuiDataListModule,
|
|
||||||
TuiDialogOptions,
|
|
||||||
TuiDialogService,
|
|
||||||
TuiDropdownModule,
|
|
||||||
TuiHostedDropdownModule,
|
|
||||||
} from '@taiga-ui/core'
|
|
||||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
|
||||||
import { filter } from 'rxjs'
|
|
||||||
import {
|
|
||||||
FormComponent,
|
|
||||||
FormContext,
|
|
||||||
} from 'src/app/routes/portal/components/form.component'
|
|
||||||
import { Proxy } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
|
||||||
import { DELETE_OPTIONS, ProxyUpdate } from './constants'
|
|
||||||
import { CB } from '@start9labs/start-sdk'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'proxies-menu',
|
|
||||||
template: `
|
|
||||||
<tui-hosted-dropdown
|
|
||||||
style="float: right"
|
|
||||||
tuiDropdownAlign="left"
|
|
||||||
[sided]="true"
|
|
||||||
[content]="dropdown"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
tuiIconButton
|
|
||||||
type="button"
|
|
||||||
appearance="icon"
|
|
||||||
size="s"
|
|
||||||
iconLeft="tuiIconMoreHorizontal"
|
|
||||||
></button>
|
|
||||||
</tui-hosted-dropdown>
|
|
||||||
<ng-template #dropdown>
|
|
||||||
<tui-data-list>
|
|
||||||
<button tuiOption (click)="rename()">Rename</button>
|
|
||||||
<tui-opt-group>
|
|
||||||
<button tuiOption (click)="delete()">Delete</button>
|
|
||||||
</tui-opt-group>
|
|
||||||
</tui-data-list>
|
|
||||||
</ng-template>
|
|
||||||
`,
|
|
||||||
standalone: true,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
TuiButtonModule,
|
|
||||||
TuiDataListModule,
|
|
||||||
TuiDropdownModule,
|
|
||||||
TuiHostedDropdownModule,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class ProxiesMenuComponent {
|
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
|
||||||
private readonly loader = inject(LoadingService)
|
|
||||||
private readonly errorService = inject(ErrorService)
|
|
||||||
private readonly api = inject(ApiService)
|
|
||||||
private readonly formDialog = inject(FormDialogService)
|
|
||||||
|
|
||||||
@Input({ required: true }) proxy!: Proxy
|
|
||||||
|
|
||||||
delete() {
|
|
||||||
this.dialogs
|
|
||||||
.open(TUI_PROMPT, DELETE_OPTIONS)
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(async () => {
|
|
||||||
const loader = this.loader.open('Deleting...').subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.api.deleteProxy({ id: this.proxy.id })
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async rename() {
|
|
||||||
const spec = { name: 'Name', required: { default: this.proxy.name } }
|
|
||||||
const name = await CB.Value.text(spec).build({} as any)
|
|
||||||
const options: Partial<TuiDialogOptions<FormContext<{ name: string }>>> = {
|
|
||||||
label: `Rename ${this.proxy.name}`,
|
|
||||||
data: {
|
|
||||||
spec: { name },
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'Save',
|
|
||||||
handler: value => this.update(value),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async update(value: ProxyUpdate): Promise<boolean> {
|
|
||||||
const loader = this.loader.open('Saving...').subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.api.updateProxy(value)
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,7 @@ import { wireguardSpec, WireguardSpec } from './constants'
|
|||||||
Add Proxy
|
Add Proxy
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
<table class="g-table" [proxies]="(proxies$ | async) || []"></table>
|
<table class="g-table" [proxies]="proxies$ | async"></table>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
|||||||
@@ -2,15 +2,31 @@ import { CommonModule } from '@angular/common'
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
|
||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { TuiDialogService, TuiLinkModule } from '@taiga-ui/core'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { TuiBadgeModule, TuiButtonModule } from '@taiga-ui/experimental'
|
import { CB } from '@start9labs/start-sdk'
|
||||||
|
import {
|
||||||
|
TuiDataListModule,
|
||||||
|
TuiDialogOptions,
|
||||||
|
TuiDialogService,
|
||||||
|
TuiLinkModule,
|
||||||
|
} from '@taiga-ui/core'
|
||||||
|
import { TuiButtonModule, TuiIconsModule } from '@taiga-ui/experimental'
|
||||||
|
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||||
|
import { filter } from 'rxjs'
|
||||||
|
import {
|
||||||
|
FormComponent,
|
||||||
|
FormContext,
|
||||||
|
} from 'src/app/routes/portal/components/form.component'
|
||||||
|
import {
|
||||||
|
DELETE_OPTIONS,
|
||||||
|
ProxyUpdate,
|
||||||
|
} from 'src/app/routes/portal/routes/system/settings/routes/proxies/constants'
|
||||||
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { Proxy } from 'src/app/services/patch-db/data-model'
|
import { Proxy } from 'src/app/services/patch-db/data-model'
|
||||||
import { ProxiesMenuComponent } from './menu.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'table[proxies]',
|
selector: 'table[proxies]',
|
||||||
@@ -18,49 +34,106 @@ import { ProxiesMenuComponent } from './menu.component'
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Created</th>
|
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Used By</th>
|
<th>Used By</th>
|
||||||
<th></th>
|
<th [style.width.rem]="3.5"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let proxy of proxies">
|
@for (proxy of proxies; track $index) {
|
||||||
<td>{{ proxy.name }}</td>
|
<tr>
|
||||||
<td>{{ proxy.createdAt | date: 'short' }}</td>
|
<td class="title">{{ proxy.name }}</td>
|
||||||
<td>{{ proxy.type }}</td>
|
<td class="type">{{ proxy.type }}</td>
|
||||||
<td>
|
<td class="used">
|
||||||
<button
|
@if (getLength(proxy); as length) {
|
||||||
*ngIf="getLength(proxy); else unused"
|
<button tuiLink (click)="onUsedBy(proxy)">
|
||||||
tuiLink
|
Used by: {{ length }}
|
||||||
(click)="onUsedBy(proxy)"
|
</button>
|
||||||
>
|
} @else {
|
||||||
Connections: {{ getLength(proxy) }}
|
N/A
|
||||||
</button>
|
}
|
||||||
<ng-template #unused>N/A</ng-template>
|
</td>
|
||||||
</td>
|
<td class="actions">
|
||||||
<td><proxies-menu [proxy]="proxy" /></td>
|
<button
|
||||||
</tr>
|
tuiIconButton
|
||||||
|
appearance="icon"
|
||||||
|
size="xs"
|
||||||
|
iconLeft="tuiIconEdit2"
|
||||||
|
(click)="rename(proxy)"
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
appearance="icon"
|
||||||
|
size="xs"
|
||||||
|
iconLeft="tuiIconTrash2"
|
||||||
|
(click)="delete(proxy)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
@if (proxies) {
|
||||||
|
<tr><td colspan="5">No proxies added</td></tr>
|
||||||
|
} @else {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
`,
|
`,
|
||||||
|
styles: `
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
tr {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:only-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
order: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
order: 2;
|
||||||
|
padding: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.used {
|
||||||
|
order: 4;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&:not(:has(button)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
imports: [CommonModule, TuiLinkModule, TuiIconsModule, TuiButtonModule],
|
||||||
CommonModule,
|
|
||||||
TuiButtonModule,
|
|
||||||
TuiBadgeModule,
|
|
||||||
TuiLinkModule,
|
|
||||||
ProxiesMenuComponent,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class ProxiesTableComponent {
|
export class ProxiesTableComponent {
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
|
private readonly loader = inject(LoadingService)
|
||||||
|
private readonly errorService = inject(ErrorService)
|
||||||
|
private readonly api = inject(ApiService)
|
||||||
|
private readonly formDialog = inject(FormDialogService)
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
proxies: readonly Proxy[] = []
|
proxies: readonly Proxy[] | null = null
|
||||||
|
|
||||||
@Output()
|
|
||||||
readonly delete = new EventEmitter<Proxy>()
|
|
||||||
|
|
||||||
getLength({ usedBy }: Proxy) {
|
getLength({ usedBy }: Proxy) {
|
||||||
return usedBy.domains.length + usedBy.services.length
|
return usedBy.domains.length + usedBy.services.length
|
||||||
@@ -81,4 +154,54 @@ export class ProxiesTableComponent {
|
|||||||
|
|
||||||
this.dialogs.open(message, { label: 'Used by', size: 's' }).subscribe()
|
this.dialogs.open(message, { label: 'Used by', size: 's' }).subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete({ id }: Proxy) {
|
||||||
|
this.dialogs
|
||||||
|
.open(TUI_PROMPT, DELETE_OPTIONS)
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
.subscribe(async () => {
|
||||||
|
const loader = this.loader.open('Deleting...').subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.deleteProxy({ id })
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async rename(proxy: Proxy) {
|
||||||
|
const spec = { name: 'Name', required: { default: proxy.name } }
|
||||||
|
const name = await CB.Value.text(spec).build({} as any)
|
||||||
|
const options: Partial<TuiDialogOptions<FormContext<{ name: string }>>> = {
|
||||||
|
label: `Rename ${proxy.name}`,
|
||||||
|
data: {
|
||||||
|
spec: { name },
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Save',
|
||||||
|
handler: value => this.update(value),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formDialog.open(FormComponent, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async update(value: ProxyUpdate): Promise<boolean> {
|
||||||
|
const loader = this.loader.open('Saving...').subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.updateProxy(value)
|
||||||
|
return true
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
import { TuiLinkModule } from '@taiga-ui/core'
|
import { TuiLinkModule } from '@taiga-ui/core'
|
||||||
import {
|
import {
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
TuiCheckboxModule,
|
TuiCheckboxModule,
|
||||||
|
TuiFadeModule,
|
||||||
TuiIconModule,
|
TuiIconModule,
|
||||||
} from '@taiga-ui/experimental'
|
} from '@taiga-ui/experimental'
|
||||||
import { BehaviorSubject } from 'rxjs'
|
import { BehaviorSubject } from 'rxjs'
|
||||||
import { TuiForModule } from '@taiga-ui/cdk'
|
|
||||||
import { Session } from 'src/app/services/api/api.types'
|
import { Session } from 'src/app/services/api/api.types'
|
||||||
import { PlatformInfoPipe } from './platform-info.pipe'
|
import { PlatformInfoPipe } from './platform-info.pipe'
|
||||||
import { FormsModule } from '@angular/forms'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'table[sessions]',
|
selector: 'table[sessions]',
|
||||||
@@ -23,15 +23,16 @@ import { FormsModule } from '@angular/forms'
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th [style.width.%]="50" [style.padding-left.rem]="single ? null : 2">
|
<th [style.width.%]="50" [style.padding-left.rem]="single ? null : 2">
|
||||||
<input
|
@if (!single) {
|
||||||
*ngIf="!single"
|
<input
|
||||||
tuiCheckbox
|
tuiCheckbox
|
||||||
size="s"
|
size="s"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
[disabled]="!sessions?.length"
|
[disabled]="!sessions?.length"
|
||||||
[ngModel]="all"
|
[ngModel]="all"
|
||||||
(ngModelChange)="onAll($event)"
|
(ngModelChange)="onAll($event)"
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
User Agent
|
User Agent
|
||||||
</th>
|
</th>
|
||||||
<th [style.width.%]="25">Platform</th>
|
<th [style.width.%]="25">Platform</th>
|
||||||
@@ -39,54 +40,94 @@ import { FormsModule } from '@angular/forms'
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let session of sessions; else: loading">
|
@for (session of sessions; track $index) {
|
||||||
<td [style.padding-left.rem]="single ? null : 2">
|
<tr>
|
||||||
<input
|
<td [style.padding-left.rem]="single ? null : 2">
|
||||||
*ngIf="!single"
|
@if (!single) {
|
||||||
tuiCheckbox
|
<input
|
||||||
size="s"
|
tuiCheckbox
|
||||||
type="checkbox"
|
size="s"
|
||||||
[ngModel]="selected$.value.includes(session)"
|
type="checkbox"
|
||||||
(ngModelChange)="onToggle(session)"
|
[ngModel]="selected$.value.includes(session)"
|
||||||
/>
|
(ngModelChange)="onToggle(session)"
|
||||||
{{ session.userAgent }}
|
/>
|
||||||
</td>
|
}
|
||||||
<td *ngIf="session.metadata.platforms | platformInfo as info">
|
<span tuiFade class="agent">{{ session.userAgent }}</span>
|
||||||
<tui-icon [icon]="info.icon"></tui-icon>
|
|
||||||
{{ info.name }}
|
|
||||||
</td>
|
|
||||||
<td>{{ session.lastActive }}</td>
|
|
||||||
</tr>
|
|
||||||
<ng-template #loading>
|
|
||||||
<tr *ngFor="let _ of single ? [''] : ['', '']">
|
|
||||||
<td colspan="5">
|
|
||||||
<div class="tui-skeleton">Loading</div>
|
|
||||||
</td>
|
</td>
|
||||||
|
@if (session.metadata.platforms | platformInfo; as info) {
|
||||||
|
<td class="platform">
|
||||||
|
<tui-icon [icon]="info.icon" />
|
||||||
|
{{ info.name }}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
<td class="date">{{ session.lastActive | date: 'medium' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
} @empty {
|
||||||
|
@if (sessions) {
|
||||||
|
<tr><td colspan="5">No sessions</td></tr>
|
||||||
|
} @else {
|
||||||
|
@for (item of single ? [''] : ['', '']; track $index) {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
|
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||||
|
|
||||||
input {
|
input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 0.5rem;
|
left: 0.25rem;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
input {
|
||||||
|
@include fullsize();
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:first-child {
|
||||||
|
padding: 0 0.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent {
|
||||||
|
white-space: nowrap;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform {
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
TuiForModule,
|
FormsModule,
|
||||||
|
PlatformInfoPipe,
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
TuiLinkModule,
|
TuiLinkModule,
|
||||||
PlatformInfoPipe,
|
|
||||||
TuiIconModule,
|
TuiIconModule,
|
||||||
TuiCheckboxModule,
|
TuiCheckboxModule,
|
||||||
FormsModule,
|
TuiFadeModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SSHTableComponent<T extends Session> implements OnChanges {
|
export class SSHTableComponent<T extends Session> implements OnChanges {
|
||||||
|
|||||||
@@ -6,19 +6,14 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
TuiDialogOptions,
|
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
|
||||||
TuiDialogService,
|
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
|
||||||
TuiLinkModule,
|
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
||||||
} from '@taiga-ui/core'
|
import { filter, take } from 'rxjs'
|
||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
|
||||||
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
|
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
|
||||||
import { SSHKey } from 'src/app/services/api/api.types'
|
import { SSHKey } from 'src/app/services/api/api.types'
|
||||||
import { filter, take } from 'rxjs'
|
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
|
||||||
import { TuiForModule } from '@taiga-ui/cdk'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'table[keys]',
|
selector: 'table[keys]',
|
||||||
@@ -33,34 +28,83 @@ import { TuiForModule } from '@taiga-ui/cdk'
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let key of keys; else: loading">
|
@for (key of keys; track $index) {
|
||||||
<td>{{ key.hostname }}</td>
|
<tr>
|
||||||
<td>{{ key.createdAt | date: 'medium' }}</td>
|
<td class="title">{{ key.hostname }}</td>
|
||||||
<td>{{ key.alg }}</td>
|
<td class="date">{{ key.createdAt | date: 'medium' }}</td>
|
||||||
<td>{{ key.fingerprint }}</td>
|
<td class="algorithm">{{ key.alg }}</td>
|
||||||
<td>
|
<td class="fingerprint" tuiFade>{{ key.fingerprint }}</td>
|
||||||
<button
|
<td class="actions">
|
||||||
tuiIconButton
|
<button
|
||||||
size="xs"
|
tuiIconButton
|
||||||
appearance="icon"
|
size="xs"
|
||||||
iconLeft="tuiIconTrash2"
|
appearance="icon"
|
||||||
[style.display]="'flex'"
|
iconLeft="tuiIconTrash2"
|
||||||
(click)="delete(key)"
|
(click)="delete(key)"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<ng-template #loading>
|
|
||||||
<tr *ngFor="let _ of ['', '']">
|
|
||||||
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
} @empty {
|
||||||
|
@if (keys) {
|
||||||
|
<tr><td colspan="5">No keys added</td></tr>
|
||||||
|
} @else {
|
||||||
|
@for (i of ['', '']; track $index) {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
`,
|
`,
|
||||||
|
styles: `
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
tr {
|
||||||
|
grid-template-columns: 3fr 2fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:only-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
order: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
order: 2;
|
||||||
|
padding: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fingerprint {
|
||||||
|
order: 3;
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
order: 4;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm {
|
||||||
|
order: 5;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: 'Algorithm: ';
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule, TuiForModule, TuiButtonModule, TuiLinkModule],
|
imports: [CommonModule, TuiButtonModule, TuiFadeModule],
|
||||||
})
|
})
|
||||||
export class SSHTableComponent {
|
export class SSHTableComponent {
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@@ -21,10 +21,40 @@
|
|||||||
/>
|
/>
|
||||||
<link rel="manifest" href="manifest.webmanifest" />
|
<link rel="manifest" href="manifest.webmanifest" />
|
||||||
<meta name="theme-color" content="#ff5b71" />
|
<meta name="theme-color" content="#ff5b71" />
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
background: #222428;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-root {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root>
|
||||||
|
<img
|
||||||
|
src="assets/img/icon.png"
|
||||||
|
style="width: 8rem; height: 8rem"
|
||||||
|
alt="Start OS"
|
||||||
|
/>
|
||||||
|
<h1>Loading</h1>
|
||||||
|
<progress></progress>
|
||||||
|
</app-root>
|
||||||
<noscript>
|
<noscript>
|
||||||
Please enable JavaScript to continue using this application.
|
Please enable JavaScript to continue using this application.
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ hr {
|
|||||||
tui-root._mobile & {
|
tui-root._mobile & {
|
||||||
// For tui-tab-bar
|
// For tui-tab-bar
|
||||||
height: calc(100vh - 3.875rem - var(--tui-height-l));
|
height: calc(100vh - 3.875rem - var(--tui-height-l));
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ hr {
|
|||||||
height: 2rem;
|
height: 2rem;
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
box-shadow: inset 0 -1px var(--tui-clear);
|
box-shadow: inset 0 -1px var(--tui-clear);
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@@ -89,6 +91,46 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tui-root._mobile .g-table {
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
border-radius: var(--tui-radius-l);
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
// TODO: Theme
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:has(:checked) {
|
||||||
|
box-shadow: inset 0 0 0 0.125rem var(--tui-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
position: static;
|
||||||
|
height: auto;
|
||||||
|
min-height: 1.5rem;
|
||||||
|
align-content: center;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:not([tuiFade]) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.g-title {
|
.g-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -127,6 +169,8 @@ hr {
|
|||||||
|
|
||||||
a.g-action,
|
a.g-action,
|
||||||
button.g-action {
|
button.g-action {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: var(--tui-disabled-opacity);
|
opacity: var(--tui-disabled-opacity);
|
||||||
|
|||||||
Reference in New Issue
Block a user