-
-
+
+
+
+ | Name |
+ Target |
+ Packages |
+ Schedule |
+ |
+
+
+
+ @for (job of jobs; track $index) {
- | Name |
- Target |
- Packages |
- Schedule |
- |
-
-
-
-
- | {{ job.name }} |
-
-
+ | {{ job.name }} |
+
+
{{ job.target.name }}
|
- Packages: {{ job.packageIds.length }} |
- {{ (job.cron | toHumanCron).message }} |
-
+ | Packages: {{ job.packageIds.length }} |
+ {{ (job.cron | toHumanCron).message }} |
+
|
-
-
- Loading |
-
-
-
+ } @empty {
+ @if (jobs) {
| No jobs found. |
-
-
-
-
+ } @else {
+ @for (i of ['', '']; track $index) {
+
+ Loading |
+
+ }
+ }
+ }
+
+
+ `,
+ 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,
imports: [
- CommonModule,
- TuiForModule,
TuiNotificationModule,
TuiButtonModule,
- TuiSvgModule,
- TuiFadeModule,
+ TuiIconModule,
ToHumanCronPipe,
GetBackupIconPipe,
],
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/backups/modals/targets.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/backups/modals/targets.component.ts
index 0c7aa954d..f06b315a7 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/backups/modals/targets.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/backups/modals/targets.component.ts
@@ -1,21 +1,11 @@
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 { CT } from '@start9labs/start-sdk'
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 { BehaviorSubject } from 'rxjs'
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 {
BackupTarget,
BackupTargetType,
@@ -23,13 +13,21 @@ import {
UnknownDisk,
} from 'src/app/services/api/api.types'
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 { BackupsTargetsComponent } from '../components/targets.component'
+import { BackupConfig } from '../types/backup-config'
+import {
+ cifsSpec,
+ diskBackupTargetSpec,
+ dropboxSpec,
+ googleDriveSpec,
+ remoteBackupTargetSpec,
+} from '../types/target'
@Component({
template: `
-
Backup targets are physical or virtual locations for storing encrypted
backups. They can be physical drives plugged into your server, shared
@@ -45,31 +43,32 @@ import { BackupsTargetsComponent } from '../components/targets.component'
Unknown Physical Drives
-
-
+
Saved Targets
-
+
Add Target
-
+
`,
standalone: true,
imports: [
@@ -78,7 +77,6 @@ import { BackupsTargetsComponent } from '../components/targets.component'
TuiButtonModule,
BackupsPhysicalComponent,
BackupsTargetsComponent,
- TuiFadeModule,
],
})
export class BackupsTargetsModal implements OnInit {
@@ -87,25 +85,20 @@ export class BackupsTargetsModal implements OnInit {
private readonly formDialog = inject(FormDialogService)
private readonly loader = inject(LoadingService)
- readonly loading$ = new BehaviorSubject(true)
-
- targets?: RR.GetBackupTargetsRes
+ targets = signal
(null)
ngOnInit() {
this.refresh()
}
async refresh() {
- this.loading$.next(true)
- this.targets = undefined
+ this.targets.set(null)
try {
- this.targets = await this.api.getBackupTargets({})
+ this.targets.set(await this.api.getBackupTargets({}))
} catch (e: any) {
this.errorService.handleError(e)
- this.targets = { unknownDisks: [], saved: [] }
- } finally {
- this.loading$.next(false)
+ this.targets.set({ unknownDisks: [], saved: [] })
}
}
@@ -114,7 +107,7 @@ export class BackupsTargetsModal implements OnInit {
try {
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) {
this.errorService.handleError(e)
} finally {
@@ -158,8 +151,8 @@ export class BackupsTargetsModal implements OnInit {
...value,
}).then(response => {
this.setTargets(
- this.targets?.saved.concat(response),
- this.targets?.unknownDisks.filter(a => a !== disk),
+ this.targets()?.saved.concat(response),
+ this.targets()?.unknownDisks.filter(a => a !== disk),
)
return true
}),
@@ -221,10 +214,10 @@ export class BackupsTargetsModal implements OnInit {
}
private setTargets(
- saved: BackupTarget[] = this.targets?.saved || [],
- unknownDisks: UnknownDisk[] = this.targets?.unknownDisks || [],
+ saved: BackupTarget[] = this.targets()?.saved || [],
+ unknownDisks: UnknownDisk[] = this.targets()?.unknownDisks || [],
) {
- this.targets = { unknownDisks, saved }
+ this.targets.set({ unknownDisks, saved })
}
private async getSpec(target: BackupTarget) {
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/metrics/cpu.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/metrics/cpu.component.ts
index 0b3ef27ae..6ed092ace 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/metrics/cpu.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/metrics/cpu.component.ts
@@ -45,7 +45,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
top: 50%;
left: 50%;
- &:before {
+ &::before {
content: '';
position: absolute;
inset: -0.5rem;
@@ -53,7 +53,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
border-radius: 100%;
}
- &:after {
+ &::after {
content: '';
position: absolute;
width: 0.25rem;
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.component.ts
index 95c4d8de0..7fd01f420 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.component.ts
@@ -131,7 +131,7 @@ import { TimeService } from 'src/app/services/time.service'
color: var(--tui-text-01);
padding-top: 0.4rem;
- &:after {
+ &::after {
content: attr(data-unit);
font-size: 0.5rem;
font-weight: normal;
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/notifications/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/notifications/item.component.ts
index 11e7dea59..3d03b0e57 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/notifications/item.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/notifications/item.component.ts
@@ -8,7 +8,8 @@ import {
import { RouterLink } from '@angular/router'
import { T } from '@start9labs/start-sdk'
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 { PatchDB } from 'patch-db-client'
import { first, Observable } from 'rxjs'
@@ -20,22 +21,24 @@ import { toRouterLink } from 'src/app/utils/to-router-link'
@Component({
selector: '[notificationItem]',
template: `
- |
- {{ notificationItem.createdAt | date: 'MMM d, y, h:mm a' }} |
-
-
+ | |
+
+ {{ notificationItem.createdAt | date: 'medium' }}
+ |
+
+
{{ notificationItem.title }}
|
-
-
- {{ manifest.title }}
-
- N/A
+ |
+ @if (manifest$ | async; as manifest) {
+
+ {{ manifest.title }}
+
+ } @else {
+ N/A
+ }
|
-
+ |
-
- View Full
-
-
- View Report
-
+ @if (overflow) {
+
+ View Full
+
+ }
+ @if (notificationItem.code === 1) {
+
+ View Report
+
+ }
|
`,
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -65,21 +64,58 @@ import { toRouterLink } from 'src/app/utils/to-router-link'
'[class._new]': '!notificationItem.read',
},
styles: `
- :host._new {
- background: var(--tui-clear);
+ @import '@taiga-ui/core/styles/taiga-ui-local';
+
+ :host {
+ grid-template-columns: 1fr;
+
+ &._new {
+ background: var(--tui-clear) !important;
+ }
+ }
+
+ button {
+ position: relative;
}
td {
padding: 0.25rem;
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: [
CommonModule,
RouterLink,
TuiLineClampModule,
- TuiSvgModule,
TuiLinkModule,
+ TuiIconModule,
],
})
export class NotificationItemComponent {
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/notifications/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/notifications/table.component.ts
index e4c298f4e..c6b56b197 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/notifications/table.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/notifications/table.component.ts
@@ -66,6 +66,14 @@ import { NotificationItemComponent } from './item.component'
}
`,
+ styles: `
+ @import '@taiga-ui/core/styles/taiga-ui-local';
+
+ :host-context(tui-root._mobile) input {
+ @include fullsize();
+ opacity: 0;
+ }
+ `,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/domains/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/domains/table.component.ts
index 8e77f3a77..c6a0a70d9 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/domains/table.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/domains/table.component.ts
@@ -17,7 +17,6 @@ import { Domain } from 'src/app/services/patch-db/data-model'
| Domain |
- Added |
DDNS Provider |
Network Strategy |
Used By |
@@ -25,36 +24,87 @@ import { Domain } from 'src/app/services/patch-db/data-model'
-
- | {{ domain.value }} |
- {{ domain.createdAt | date: 'short' }} |
- {{ domain.provider }} |
- {{ getStrategy(domain) }} |
-
-
- Interfaces: {{ qty }}
-
- N/A
- |
-
-
- Delete
-
- |
-
+ @for (domain of domains; track $index) {
+
+ | {{ domain.value }} |
+ {{ domain.provider }} |
+ {{ getStrategy(domain) }} |
+
+ @if (domain.usedBy.length; as qty) {
+
+ Used by: {{ qty }}
+
+ } @else {
+ N/A
+ }
+ |
+
+
+ Delete
+
+ |
+
+ } @empty {
+ | No domains |
+ }
`,
+ 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,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, TuiButtonModule, TuiLinkModule],
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/menu.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/menu.component.ts
deleted file mode 100644
index 40b20e606..000000000
--- a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/menu.component.ts
+++ /dev/null
@@ -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: `
-
-
-
-
-
- Rename
-
- Delete
-
-
-
- `,
- 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>> = {
- 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 {
- 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()
- }
- }
-}
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/proxies.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/proxies.component.ts
index 6d660de2a..fc8912bff 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/proxies.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/proxies.component.ts
@@ -24,7 +24,7 @@ import { wireguardSpec, WireguardSpec } from './constants'
Add Proxy
-
+
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/table.component.ts
index 418e8e71a..9d67be0fc 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/table.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/proxies/table.component.ts
@@ -2,15 +2,31 @@ import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
- EventEmitter,
inject,
Input,
- Output,
} from '@angular/core'
-import { TuiDialogService, TuiLinkModule } from '@taiga-ui/core'
-import { TuiBadgeModule, TuiButtonModule } from '@taiga-ui/experimental'
+import { ErrorService, LoadingService } from '@start9labs/shared'
+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 { ProxiesMenuComponent } from './menu.component'
@Component({
selector: 'table[proxies]',
@@ -18,49 +34,106 @@ import { ProxiesMenuComponent } from './menu.component'
| Name |
- Created |
Type |
Used By |
- |
+ |
-
- | {{ proxy.name }} |
- {{ proxy.createdAt | date: 'short' }} |
- {{ proxy.type }} |
-
-
- Connections: {{ getLength(proxy) }}
-
- N/A
- |
- |
-
+ @for (proxy of proxies; track $index) {
+
+ | {{ proxy.name }} |
+ {{ proxy.type }} |
+
+ @if (getLength(proxy); as length) {
+
+ Used by: {{ length }}
+
+ } @else {
+ N/A
+ }
+ |
+
+
+ Rename
+
+
+ Delete
+
+ |
+
+ } @empty {
+ @if (proxies) {
+ | No proxies added |
+ } @else {
+
+ Loading |
+
+ }
+ }
`,
+ 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,
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [
- CommonModule,
- TuiButtonModule,
- TuiBadgeModule,
- TuiLinkModule,
- ProxiesMenuComponent,
- ],
+ imports: [CommonModule, TuiLinkModule, TuiIconsModule, TuiButtonModule],
})
export class ProxiesTableComponent {
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()
- proxies: readonly Proxy[] = []
-
- @Output()
- readonly delete = new EventEmitter()
+ proxies: readonly Proxy[] | null = null
getLength({ usedBy }: Proxy) {
return usedBy.domains.length + usedBy.services.length
@@ -81,4 +154,54 @@ export class ProxiesTableComponent {
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>> = {
+ 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 {
+ 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()
+ }
+ }
}
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/sessions/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/sessions/table.component.ts
index 418a88dfe..e6b085877 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/sessions/table.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/sessions/table.component.ts
@@ -5,17 +5,17 @@ import {
Input,
OnChanges,
} from '@angular/core'
+import { FormsModule } from '@angular/forms'
import { TuiLinkModule } from '@taiga-ui/core'
import {
TuiButtonModule,
TuiCheckboxModule,
+ TuiFadeModule,
TuiIconModule,
} from '@taiga-ui/experimental'
import { BehaviorSubject } from 'rxjs'
-import { TuiForModule } from '@taiga-ui/cdk'
import { Session } from 'src/app/services/api/api.types'
import { PlatformInfoPipe } from './platform-info.pipe'
-import { FormsModule } from '@angular/forms'
@Component({
selector: 'table[sessions]',
@@ -23,15 +23,16 @@ import { FormsModule } from '@angular/forms'
|
-
+ @if (!single) {
+
+ }
User Agent
|
Platform |
@@ -39,54 +40,94 @@ import { FormsModule } from '@angular/forms'
-
- |
-
- {{ session.userAgent }}
- |
-
-
- {{ info.name }}
- |
- {{ session.lastActive }} |
-
-
-
- |
- Loading
+ @for (session of sessions; track $index) {
+ |
+ |
+ @if (!single) {
+
+ }
+ {{ session.userAgent }}
|
+ @if (session.metadata.platforms | platformInfo; as info) {
+
+
+ {{ info.name }}
+ |
+ }
+ {{ session.lastActive | date: 'medium' }} |
-
+ } @empty {
+ @if (sessions) {
+ | No sessions |
+ } @else {
+ @for (item of single ? [''] : ['', '']; track $index) {
+
+ Loading |
+
+ }
+ }
+ }
`,
styles: [
`
+ @import '@taiga-ui/core/styles/taiga-ui-local';
+
input {
position: absolute;
top: 50%;
- left: 0.5rem;
+ left: 0.25rem;
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,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
- TuiForModule,
+ FormsModule,
+ PlatformInfoPipe,
TuiButtonModule,
TuiLinkModule,
- PlatformInfoPipe,
TuiIconModule,
TuiCheckboxModule,
- FormsModule,
+ TuiFadeModule,
],
})
export class SSHTableComponent implements OnChanges {
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/ssh/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/ssh/table.component.ts
index 57faafa82..45475981d 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/ssh/table.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/ssh/table.component.ts
@@ -6,19 +6,14 @@ import {
inject,
Input,
} from '@angular/core'
-import {
- TuiDialogOptions,
- TuiDialogService,
- TuiLinkModule,
-} from '@taiga-ui/core'
-import { TuiButtonModule } from '@taiga-ui/experimental'
+import { ErrorService, LoadingService } from '@start9labs/shared'
+import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
+import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
+import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
+import { filter, take } from 'rxjs'
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
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 { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
-import { TuiForModule } from '@taiga-ui/cdk'
@Component({
selector: 'table[keys]',
@@ -33,34 +28,83 @@ import { TuiForModule } from '@taiga-ui/cdk'
-
- | {{ key.hostname }} |
- {{ key.createdAt | date: 'medium' }} |
- {{ key.alg }} |
- {{ key.fingerprint }} |
-
-
- Delete
-
- |
-
-
-
- Loading |
+ @for (key of keys; track $index) {
+
+ | {{ key.hostname }} |
+ {{ key.createdAt | date: 'medium' }} |
+ {{ key.alg }} |
+ {{ key.fingerprint }} |
+
+
+ Delete
+
+ |
-
+ } @empty {
+ @if (keys) {
+ | No keys added |
+ } @else {
+ @for (i of ['', '']; track $index) {
+
+ Loading |
+
+ }
+ }
+ }
`,
+ 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,
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [CommonModule, TuiForModule, TuiButtonModule, TuiLinkModule],
+ imports: [CommonModule, TuiButtonModule, TuiFadeModule],
})
export class SSHTableComponent {
private readonly loader = inject(LoadingService)
diff --git a/web/projects/ui/src/index.html b/web/projects/ui/src/index.html
index 0248b7462..dd037db64 100644
--- a/web/projects/ui/src/index.html
+++ b/web/projects/ui/src/index.html
@@ -1,4 +1,4 @@
-
+
@@ -21,10 +21,40 @@
/>
+
-
+
+
+ Loading
+
+
diff --git a/web/projects/ui/src/styles.scss b/web/projects/ui/src/styles.scss
index 98e9d0eef..a121c8d45 100644
--- a/web/projects/ui/src/styles.scss
+++ b/web/projects/ui/src/styles.scss
@@ -42,6 +42,7 @@ hr {
tui-root._mobile & {
// For tui-tab-bar
height: calc(100vh - 3.875rem - var(--tui-height-l));
+ padding: 1rem;
}
}
@@ -77,6 +78,7 @@ hr {
height: 2rem;
padding: 0 0.25rem;
box-shadow: inset 0 -1px var(--tui-clear);
+ text-overflow: ellipsis;
}
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 {
display: flex;
align-items: center;
@@ -127,6 +169,8 @@ hr {
a.g-action,
button.g-action {
+ cursor: pointer;
+
&:disabled {
pointer-events: none;
opacity: var(--tui-disabled-opacity);