mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
refactor: finalize new portal (#2543)
This commit is contained in:
@@ -178,8 +178,9 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
1rem 0 var(--tui-clear),
|
1rem 0 var(--tui-clear),
|
||||||
-1rem 0 var(--tui-clear);
|
-1rem 0 var(--tui-clear);
|
||||||
padding-top: 0.375rem !important;
|
padding-top: 0.25rem !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
|
margin: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
|
import { TuiDataListModule } from '@taiga-ui/core'
|
||||||
|
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
icon: string
|
||||||
|
label: string
|
||||||
|
action: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-actions',
|
||||||
|
template: `
|
||||||
|
<tui-data-list>
|
||||||
|
<h3 class="title"><ng-content /></h3>
|
||||||
|
<tui-opt-group
|
||||||
|
*ngFor="let group of actions | keyvalue: asIsOrder"
|
||||||
|
[label]="group.key.toUpperCase()"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
*ngFor="let action of group.value"
|
||||||
|
tuiOption
|
||||||
|
class="item"
|
||||||
|
(click)="action.action()"
|
||||||
|
>
|
||||||
|
<tui-icon class="icon" [icon]="action.icon" />
|
||||||
|
{{ action.label }}
|
||||||
|
</button>
|
||||||
|
</tui-opt-group>
|
||||||
|
</tui-data-list>
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0.5rem 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
font: var(--tui-font-text-l);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
opacity: var(--tui-disabled-opacity);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [TuiDataListModule, CommonModule, TuiIconModule],
|
||||||
|
})
|
||||||
|
export class ActionsComponent {
|
||||||
|
@Input()
|
||||||
|
actions: Record<string, readonly Action[]> = {}
|
||||||
|
|
||||||
|
asIsOrder(a: any, b: any) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<tui-data-list>
|
|
||||||
<h3 class="title"><ng-content></ng-content></h3>
|
|
||||||
<tui-opt-group
|
|
||||||
*ngFor="let group of actions | keyvalue : asIsOrder"
|
|
||||||
[label]="group.key.toUpperCase()"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
*ngFor="let action of group.value"
|
|
||||||
tuiOption
|
|
||||||
class="item"
|
|
||||||
(click)="action.action()"
|
|
||||||
>
|
|
||||||
<tui-svg class="icon" [src]="action.icon"></tui-svg>
|
|
||||||
{{ action.label }}
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
|
||||||
</tui-data-list>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
.title {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0.5rem 0.25rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
font: var(--tui-font-text-l);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
opacity: var(--tui-disabled-opacity);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
|
||||||
import { TuiDataListModule, TuiSvgModule } from '@taiga-ui/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
|
|
||||||
export interface Action {
|
|
||||||
icon: string
|
|
||||||
label: string
|
|
||||||
action: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-actions',
|
|
||||||
templateUrl: './actions.component.html',
|
|
||||||
styleUrls: ['./actions.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [TuiDataListModule, TuiSvgModule, CommonModule],
|
|
||||||
})
|
|
||||||
export class ActionsComponent {
|
|
||||||
@Input()
|
|
||||||
actions: Record<string, readonly Action[]> = {}
|
|
||||||
|
|
||||||
asIsOrder(a: any, b: any) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
158
web/projects/ui/src/app/apps/portal/components/card.component.ts
Normal file
158
web/projects/ui/src/app/apps/portal/components/card.component.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostListener,
|
||||||
|
inject,
|
||||||
|
Input,
|
||||||
|
} from '@angular/core'
|
||||||
|
import {
|
||||||
|
TuiBadgedContentModule,
|
||||||
|
TuiBadgeNotificationModule,
|
||||||
|
TuiButtonModule,
|
||||||
|
TuiIconModule,
|
||||||
|
} from '@taiga-ui/experimental'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { TickerModule } from '@start9labs/shared'
|
||||||
|
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
|
||||||
|
import { NavigationService } from '../services/navigation.service'
|
||||||
|
import { Action, ActionsComponent } from './actions.component'
|
||||||
|
import { toRouterLink } from '../utils/to-router-link'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: '[appCard]',
|
||||||
|
template: `
|
||||||
|
<span class="link">
|
||||||
|
<tui-badged-content [style.--tui-radius.rem]="1.5">
|
||||||
|
@if (badge) {
|
||||||
|
<tui-badge-notification size="m" tuiSlot="top">
|
||||||
|
{{ badge }}
|
||||||
|
</tui-badge-notification>
|
||||||
|
}
|
||||||
|
@if (icon?.startsWith('tuiIcon')) {
|
||||||
|
<tui-icon class="icon" [icon]="icon" />
|
||||||
|
} @else {
|
||||||
|
<img alt="" class="icon" [src]="icon" />
|
||||||
|
}
|
||||||
|
</tui-badged-content>
|
||||||
|
<label ticker class="title">{{ title }}</label>
|
||||||
|
</span>
|
||||||
|
@if (isService) {
|
||||||
|
<span class="side">
|
||||||
|
<tui-hosted-dropdown
|
||||||
|
[content]="content"
|
||||||
|
(click.stop.prevent)="(0)"
|
||||||
|
(pointerdown.stop)="(0)"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
appearance="outline"
|
||||||
|
size="xs"
|
||||||
|
iconLeft="tuiIconMoreHorizontal"
|
||||||
|
[style.border-radius.%]="100"
|
||||||
|
>
|
||||||
|
Actions
|
||||||
|
</button>
|
||||||
|
<ng-template #content let-close="close">
|
||||||
|
<app-actions [actions]="actions" (click)="close()">
|
||||||
|
{{ title }}
|
||||||
|
</app-actions>
|
||||||
|
</ng-template>
|
||||||
|
</tui-hosted-dropdown>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
height: 5.5rem;
|
||||||
|
width: 12.5rem;
|
||||||
|
border-radius: var(--tui-radius-l);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
||||||
|
// TODO: Theme
|
||||||
|
background: rgb(111 109 109);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
font: var(--tui-font-text-m);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 100%;
|
||||||
|
color: var(--tui-text-01-night);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side {
|
||||||
|
width: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
||||||
|
// TODO: Theme
|
||||||
|
background: #4b4a4a;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterLink,
|
||||||
|
TuiButtonModule,
|
||||||
|
TuiHostedDropdownModule,
|
||||||
|
TuiDataListModule,
|
||||||
|
TuiIconModule,
|
||||||
|
TickerModule,
|
||||||
|
TuiBadgedContentModule,
|
||||||
|
TuiBadgeNotificationModule,
|
||||||
|
ActionsComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CardComponent {
|
||||||
|
private readonly navigation = inject(NavigationService)
|
||||||
|
|
||||||
|
@Input({ required: true })
|
||||||
|
id!: string
|
||||||
|
|
||||||
|
@Input({ required: true })
|
||||||
|
icon!: string
|
||||||
|
|
||||||
|
@Input({ required: true })
|
||||||
|
title!: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
actions: Record<string, readonly Action[]> = {}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
badge: number | null = null
|
||||||
|
|
||||||
|
get isService(): boolean {
|
||||||
|
return !this.id.includes('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('click')
|
||||||
|
onClick() {
|
||||||
|
const { id, icon, title } = this
|
||||||
|
const routerLink = toRouterLink(id)
|
||||||
|
|
||||||
|
this.navigation.addTab({ icon, title, routerLink })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevents Firefox from starting a native drag
|
||||||
|
@HostListener('pointerdown.prevent')
|
||||||
|
onDown() {}
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<span class="link">
|
|
||||||
<tui-badged-content [style.--tui-radius.rem]="1.5">
|
|
||||||
<tui-badge-notification *ngIf="badge" size="m" tuiSlot="top">
|
|
||||||
{{ badge }}
|
|
||||||
</tui-badge-notification>
|
|
||||||
<tui-icon
|
|
||||||
*ngIf="icon?.startsWith('tuiIcon'); else url"
|
|
||||||
class="icon"
|
|
||||||
[icon]="icon"
|
|
||||||
/>
|
|
||||||
<ng-template #url>
|
|
||||||
<img alt="" class="icon" [src]="icon" />
|
|
||||||
</ng-template>
|
|
||||||
</tui-badged-content>
|
|
||||||
<label ticker class="title">{{ title }}</label>
|
|
||||||
</span>
|
|
||||||
<span *ngIf="isService" class="side">
|
|
||||||
<tui-hosted-dropdown
|
|
||||||
#dropdown
|
|
||||||
[content]="content"
|
|
||||||
(click.stop.prevent)="(0)"
|
|
||||||
(pointerdown.stop)="(0)"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
tuiIconButton
|
|
||||||
appearance="outline"
|
|
||||||
size="xs"
|
|
||||||
iconLeft="tuiIconMoreHorizontal"
|
|
||||||
[style.border-radius.%]="100"
|
|
||||||
>
|
|
||||||
Actions
|
|
||||||
</button>
|
|
||||||
<ng-template #content>
|
|
||||||
<app-actions
|
|
||||||
[actions]="actions"
|
|
||||||
(click)="dropdown.openChange.next(false)"
|
|
||||||
>
|
|
||||||
{{ title }}
|
|
||||||
</app-actions>
|
|
||||||
</ng-template>
|
|
||||||
</tui-hosted-dropdown>
|
|
||||||
</span>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
height: 5.5rem;
|
|
||||||
width: 12.5rem;
|
|
||||||
border-radius: var(--tui-radius-l);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
|
||||||
// TODO: Theme
|
|
||||||
background: rgb(111 109 109);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
gap: 0.25rem;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
font: var(--tui-font-text-m);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
border-radius: 100%;
|
|
||||||
color: var(--tui-text-01-night);
|
|
||||||
}
|
|
||||||
|
|
||||||
tui-svg.icon {
|
|
||||||
transform: scale(1.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side {
|
|
||||||
width: 3rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
|
||||||
// TODO: Theme
|
|
||||||
background: #4b4a4a;
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
HostListener,
|
|
||||||
inject,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import {
|
|
||||||
TuiBadgedContentModule,
|
|
||||||
TuiBadgeNotificationModule,
|
|
||||||
TuiButtonModule,
|
|
||||||
TuiIconModule,
|
|
||||||
} from '@taiga-ui/experimental'
|
|
||||||
import { RouterLink } from '@angular/router'
|
|
||||||
import { TickerModule } from '@start9labs/shared'
|
|
||||||
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
|
|
||||||
import { NavigationService } from '../../services/navigation.service'
|
|
||||||
import { Action, ActionsComponent } from '../actions/actions.component'
|
|
||||||
import { toRouterLink } from '../../utils/to-router-link'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: '[appCard]',
|
|
||||||
templateUrl: 'card.component.html',
|
|
||||||
styleUrls: ['card.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
RouterLink,
|
|
||||||
TuiButtonModule,
|
|
||||||
TuiHostedDropdownModule,
|
|
||||||
TuiDataListModule,
|
|
||||||
TuiIconModule,
|
|
||||||
TickerModule,
|
|
||||||
TuiBadgedContentModule,
|
|
||||||
TuiBadgeNotificationModule,
|
|
||||||
ActionsComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class CardComponent {
|
|
||||||
private readonly navigation = inject(NavigationService)
|
|
||||||
|
|
||||||
@Input({ required: true })
|
|
||||||
id!: string
|
|
||||||
|
|
||||||
@Input({ required: true })
|
|
||||||
icon!: string
|
|
||||||
|
|
||||||
@Input({ required: true })
|
|
||||||
title!: string
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
actions: Record<string, readonly Action[]> = {}
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
badge: number | null = null
|
|
||||||
|
|
||||||
get isService(): boolean {
|
|
||||||
return !this.id.includes('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('click')
|
|
||||||
onClick() {
|
|
||||||
const { id, icon, title } = this
|
|
||||||
const routerLink = toRouterLink(id)
|
|
||||||
|
|
||||||
this.navigation.addTab({ icon, title, routerLink })
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('pointerdown.prevent')
|
|
||||||
onDown() {
|
|
||||||
// Prevents Firefox from starting a native drag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="content" (tuiActiveZoneChange)="open = $event">
|
<div class="content" (tuiActiveZoneChange)="open = $event">
|
||||||
<button class="toggle" (click)="open = !open" (mousedown.prevent)="(0)">
|
<button class="toggle" (click)="open = !open" (mousedown.prevent)="(0)">
|
||||||
<tui-svg src="tuiIconArrowUpCircleLarge" class="icon"></tui-svg>
|
<tui-icon icon="tuiIconArrowUpCircle" class="icon" />
|
||||||
Toggle drawer
|
Toggle drawer
|
||||||
</button>
|
</button>
|
||||||
<tui-input
|
<tui-input
|
||||||
@@ -16,37 +16,42 @@
|
|||||||
<tui-scrollbar class="scrollbar">
|
<tui-scrollbar class="scrollbar">
|
||||||
<h2 class="title">System Utilities</h2>
|
<h2 class="title">System Utilities</h2>
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<a
|
@for (
|
||||||
*ngFor="
|
item of system | keyvalue | tuiFilter: bySearch : search;
|
||||||
let item of system | keyvalue | tuiFilter : bySearch : search;
|
track $index
|
||||||
empty: empty
|
) {
|
||||||
"
|
<a
|
||||||
appCard
|
appCard
|
||||||
[badge]="item.key | toBadge | async"
|
[badge]="item.key | toBadge | async"
|
||||||
[drawerItem]="item.key"
|
[drawerItem]="item.key"
|
||||||
[id]="item.key"
|
[id]="item.key"
|
||||||
[title]="item.value.title"
|
[title]="item.value.title"
|
||||||
[icon]="item.value.icon"
|
[icon]="item.value.icon"
|
||||||
[routerLink]="item.key"
|
[routerLink]="item.key"
|
||||||
(click)="open = false"
|
(click)="open = false"
|
||||||
></a>
|
></a>
|
||||||
|
} @empty {
|
||||||
|
Nothing found
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<h2 class="title">Installed services</h2>
|
<h2 class="title">Installed services</h2>
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<a
|
@for (
|
||||||
*ngFor="
|
item of (services$ | async) || [] | tuiFilter: bySearch : search;
|
||||||
let item of (services$ | async) || [] | tuiFilter : bySearch : search;
|
track $index
|
||||||
empty: empty
|
) {
|
||||||
"
|
<a
|
||||||
appCard
|
appCard
|
||||||
[drawerItem]="item.manifest.id"
|
[drawerItem]="item.manifest.id"
|
||||||
[id]="item.manifest.id"
|
[id]="item.manifest.id"
|
||||||
[icon]="item.icon"
|
[icon]="item.icon"
|
||||||
[title]="item.manifest.title"
|
[title]="item.manifest.title"
|
||||||
[routerLink]="getLink(item.manifest.id)"
|
[routerLink]="getLink(item.manifest.id)"
|
||||||
(click)="open = false"
|
(click)="open = false"
|
||||||
></a>
|
></a>
|
||||||
|
} @empty {
|
||||||
|
Nothing found
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<ng-template #empty>Nothing found</ng-template>
|
|
||||||
</tui-scrollbar>
|
</tui-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import {
|
|||||||
} from '@taiga-ui/cdk'
|
} from '@taiga-ui/cdk'
|
||||||
import {
|
import {
|
||||||
TuiScrollbarModule,
|
TuiScrollbarModule,
|
||||||
TuiSvgModule,
|
|
||||||
TuiTextfieldControllerModule,
|
TuiTextfieldControllerModule,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
|
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||||
import { TuiInputModule } from '@taiga-ui/kit'
|
import { TuiInputModule } from '@taiga-ui/kit'
|
||||||
import { CardComponent } from '../card/card.component'
|
import { CardComponent } from '../card.component'
|
||||||
import { ServicesService } from '../../services/services.service'
|
import { ServicesService } from '../../services/services.service'
|
||||||
import { toRouterLink } from '../../utils/to-router-link'
|
import { toRouterLink } from '../../utils/to-router-link'
|
||||||
import { DrawerItemDirective } from './drawer-item.directive'
|
import { DrawerItemDirective } from './drawer-item.directive'
|
||||||
@@ -36,7 +36,6 @@ import { ToBadgePipe } from '../../pipes/to-badge'
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
TuiSvgModule,
|
|
||||||
TuiScrollbarModule,
|
TuiScrollbarModule,
|
||||||
TuiActiveZoneModule,
|
TuiActiveZoneModule,
|
||||||
TuiInputModule,
|
TuiInputModule,
|
||||||
@@ -46,6 +45,7 @@ import { ToBadgePipe } from '../../pipes/to-badge'
|
|||||||
CardComponent,
|
CardComponent,
|
||||||
DrawerItemDirective,
|
DrawerItemDirective,
|
||||||
ToBadgePipe,
|
ToBadgePipe,
|
||||||
|
TuiIconModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DrawerComponent {
|
export class DrawerComponent {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { combineLatest, map, Observable, startWith } from 'rxjs'
|
||||||
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { AsyncPipe } from '@angular/common'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'header-connection',
|
||||||
|
template: `
|
||||||
|
@if (connection$ | async; as connection) {
|
||||||
|
<tui-icon
|
||||||
|
[title]="connection.message"
|
||||||
|
[icon]="connection.icon"
|
||||||
|
[style.color]="connection.color"
|
||||||
|
[style.margin.rem]="0.5"
|
||||||
|
></tui-icon>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [TuiIconModule, AsyncPipe],
|
||||||
|
})
|
||||||
|
export class HeaderConnectionComponent {
|
||||||
|
readonly connection$: Observable<{
|
||||||
|
message: string
|
||||||
|
color: string
|
||||||
|
icon: string
|
||||||
|
}> = combineLatest([
|
||||||
|
inject(ConnectionService).networkConnected$,
|
||||||
|
inject(ConnectionService).websocketConnected$.pipe(startWith(false)),
|
||||||
|
inject(PatchDB<DataModel>)
|
||||||
|
.watch$('server-info', 'status-info')
|
||||||
|
.pipe(startWith({ restarting: false, 'shutting-down': false })),
|
||||||
|
]).pipe(
|
||||||
|
map(([network, websocket, status]) => {
|
||||||
|
if (!network)
|
||||||
|
return {
|
||||||
|
message: 'No Internet',
|
||||||
|
color: 'var(--tui-error-fill)',
|
||||||
|
icon: 'tuiIconCloudOff',
|
||||||
|
}
|
||||||
|
if (!websocket)
|
||||||
|
return {
|
||||||
|
message: 'Connecting',
|
||||||
|
color: 'var(--tui-warning-fill)',
|
||||||
|
icon: 'tuiIconCloudOff',
|
||||||
|
}
|
||||||
|
if (status['shutting-down'])
|
||||||
|
return {
|
||||||
|
message: 'Shutting Down',
|
||||||
|
color: 'var(--tui-neutral-fill)',
|
||||||
|
icon: 'tuiIconPower',
|
||||||
|
}
|
||||||
|
if (status.restarting)
|
||||||
|
return {
|
||||||
|
message: 'Restarting',
|
||||||
|
color: 'var(--tui-neutral-fill)',
|
||||||
|
icon: 'tuiIconPower',
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Connected',
|
||||||
|
color: 'var(--tui-success-fill)',
|
||||||
|
icon: 'tuiIconCloud',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
TuiDataListModule,
|
TuiDataListModule,
|
||||||
|
TuiDialogOptions,
|
||||||
TuiDialogService,
|
TuiDialogService,
|
||||||
TuiHostedDropdownModule,
|
TuiHostedDropdownModule,
|
||||||
TuiSvgModule,
|
TuiSvgModule,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
|
||||||
|
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { filter } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
import { AuthService } from 'src/app/services/auth.service'
|
||||||
import { ABOUT } from './about.component'
|
import { ABOUT } from './about.component'
|
||||||
|
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'header-menu',
|
selector: 'header-menu',
|
||||||
template: `
|
template: `
|
||||||
<tui-hosted-dropdown
|
<tui-hosted-dropdown [content]="content" [tuiDropdownMaxHeight]="9999">
|
||||||
[content]="content"
|
|
||||||
[tuiDropdownMaxHeight]="9999"
|
|
||||||
(click.stop.prevent)="(0)"
|
|
||||||
(pointerdown.stop)="(0)"
|
|
||||||
>
|
|
||||||
<button tuiIconButton appearance="">
|
<button tuiIconButton appearance="">
|
||||||
<img style="max-width: 62%" src="assets/img/icon.png" alt="StartOS" />
|
<img style="max-width: 62%" src="assets/img/icon.png" alt="StartOS" />
|
||||||
</button>
|
</button>
|
||||||
@@ -26,43 +28,35 @@ import { ABOUT } from './about.component'
|
|||||||
<tui-data-list>
|
<tui-data-list>
|
||||||
<h3 class="title">StartOS</h3>
|
<h3 class="title">StartOS</h3>
|
||||||
<button tuiOption class="item" (click)="about()">
|
<button tuiOption class="item" (click)="about()">
|
||||||
<tui-svg src="tuiIconInfo"></tui-svg>
|
<tui-icon icon="tuiIconInfo" />
|
||||||
About this server
|
About this server
|
||||||
</button>
|
</button>
|
||||||
<tui-opt-group>
|
<tui-opt-group>
|
||||||
<button tuiOption class="item" (click)="({})">
|
@for (link of links; track $index) {
|
||||||
<tui-svg src="tuiIconBookOpen"></tui-svg>
|
<a
|
||||||
User Manual
|
tuiOption
|
||||||
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
class="item"
|
||||||
</button>
|
target="_blank"
|
||||||
<button tuiOption class="item" (click)="({})">
|
rel="noreferrer"
|
||||||
<tui-svg src="tuiIconHeadphones"></tui-svg>
|
[href]="link.href"
|
||||||
Contact Support
|
>
|
||||||
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
<tui-icon [icon]="link.icon" />
|
||||||
</button>
|
{{ link.name }}
|
||||||
<button tuiOption class="item" (click)="({})">
|
<tui-icon class="external" icon="tuiIconArrowUpRight" />
|
||||||
<tui-svg src="tuiIconDollarSign"></tui-svg>
|
</a>
|
||||||
Donate to Start9
|
}
|
||||||
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
</tui-opt-group>
|
||||||
<tui-opt-group>
|
<tui-opt-group>
|
||||||
<button tuiOption class="item" (click)="({})">
|
@for (item of system; track $index) {
|
||||||
<tui-svg src="tuiIconTool"></tui-svg>
|
<button tuiOption class="item" (click)="prompt(item.action)">
|
||||||
System Rebuild
|
<tui-icon [icon]="item.icon" />
|
||||||
</button>
|
{{ item.action }}
|
||||||
<button tuiOption class="item" (click)="({})">
|
</button>
|
||||||
<tui-svg src="tuiIconRefreshCw"></tui-svg>
|
}
|
||||||
Restart
|
|
||||||
</button>
|
|
||||||
<button tuiOption class="item" (click)="({})">
|
|
||||||
<tui-svg src="tuiIconPower"></tui-svg>
|
|
||||||
Shutdown
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
</tui-opt-group>
|
||||||
<tui-opt-group>
|
<tui-opt-group>
|
||||||
<button tuiOption class="item" (click)="logout()">
|
<button tuiOption class="item" (click)="logout()">
|
||||||
<tui-svg src="tuiIconLogOut"></tui-svg>
|
<tui-icon icon="tuiIconLogOut" />
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</tui-opt-group>
|
</tui-opt-group>
|
||||||
@@ -72,6 +66,10 @@ import { ABOUT } from './about.component'
|
|||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
|
tui-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -80,7 +78,6 @@ import { ABOUT } from './about.component'
|
|||||||
.title {
|
.title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0.5rem 0.25rem;
|
padding: 0 0.5rem 0.25rem;
|
||||||
white-space: nowrap;
|
|
||||||
font: var(--tui-font-text-l);
|
font: var(--tui-font-text-l);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -98,13 +95,50 @@ import { ABOUT } from './about.component'
|
|||||||
TuiDataListModule,
|
TuiDataListModule,
|
||||||
TuiSvgModule,
|
TuiSvgModule,
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
|
TuiIconModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class HeaderMenuComponent {
|
export class HeaderMenuComponent {
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
|
private readonly errorService = inject(ErrorService)
|
||||||
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly auth = inject(AuthService)
|
private readonly auth = inject(AuthService)
|
||||||
|
private readonly patch = inject(PatchDB<DataModel>)
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
|
|
||||||
|
readonly links = [
|
||||||
|
{
|
||||||
|
name: 'User Manual',
|
||||||
|
icon: 'tuiIconBookOpen',
|
||||||
|
href: 'https://docs.start9.com/0.3.5.x/user-manual',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Contact Support',
|
||||||
|
icon: 'tuiIconHeadphones',
|
||||||
|
href: 'https://start9.com/contact',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Donate to Start9',
|
||||||
|
icon: 'tuiIconDollarSign',
|
||||||
|
href: 'https://donate.start9.com',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
readonly system = [
|
||||||
|
{
|
||||||
|
icon: 'tuiIconTool',
|
||||||
|
action: 'System Rebuild',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'tuiIconRefreshCw',
|
||||||
|
action: 'Restart',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'tuiIconPower',
|
||||||
|
action: 'Shutdown',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
about() {
|
about() {
|
||||||
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
|
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
|
||||||
}
|
}
|
||||||
@@ -113,4 +147,72 @@ export class HeaderMenuComponent {
|
|||||||
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
||||||
this.auth.setUnverified()
|
this.auth.setUnverified()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async prompt(action: keyof typeof METHODS) {
|
||||||
|
const minutes =
|
||||||
|
action === 'System Rebuild'
|
||||||
|
? Object.keys(await getAllPackages(this.patch)).length * 2
|
||||||
|
: ''
|
||||||
|
|
||||||
|
this.dialogs
|
||||||
|
.open(TUI_PROMPT, getOptions(action, minutes))
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
.subscribe(async () => {
|
||||||
|
const loader = this.loader.open(`Beginning ${action}...`).subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api[METHODS[action]]({})
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const METHODS = {
|
||||||
|
Restart: 'restartServer',
|
||||||
|
Shutdown: 'shutdownServer',
|
||||||
|
'System Rebuild': 'systemRebuild',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
function getOptions(
|
||||||
|
key: keyof typeof METHODS,
|
||||||
|
minutes: unknown,
|
||||||
|
): Partial<TuiDialogOptions<TuiPromptData>> {
|
||||||
|
switch (key) {
|
||||||
|
case 'Restart':
|
||||||
|
return {
|
||||||
|
label: 'Restart',
|
||||||
|
size: 's',
|
||||||
|
data: {
|
||||||
|
content:
|
||||||
|
'Are you sure you want to restart your server? It can take several minutes to come back online.',
|
||||||
|
yes: 'Restart',
|
||||||
|
no: 'Cancel',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case 'Shutdown':
|
||||||
|
return {
|
||||||
|
label: 'Warning',
|
||||||
|
size: 's',
|
||||||
|
data: {
|
||||||
|
content:
|
||||||
|
'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, You will need to physically unplug your server and plug it back in',
|
||||||
|
yes: 'Shutdown',
|
||||||
|
no: 'Cancel',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
label: 'Warning',
|
||||||
|
size: 's',
|
||||||
|
data: {
|
||||||
|
content: `This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your server.`,
|
||||||
|
yes: 'Rebuild',
|
||||||
|
no: 'Cancel',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,19 +25,13 @@ import { SidebarDirective } from '../../../../app/sidebar-host.component'
|
|||||||
import { HeaderMenuComponent } from './header-menu.component'
|
import { HeaderMenuComponent } from './header-menu.component'
|
||||||
import { HeaderNotificationsComponent } from './header-notifications.component'
|
import { HeaderNotificationsComponent } from './header-notifications.component'
|
||||||
import { NotificationService } from '../../services/notification.service'
|
import { NotificationService } from '../../services/notification.service'
|
||||||
|
import { HeaderConnectionComponent } from './header-connection.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'header[appHeader]',
|
selector: 'header[appHeader]',
|
||||||
template: `
|
template: `
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
<button
|
<header-connection [style.margin-left]="'auto'" />
|
||||||
tuiIconButton
|
|
||||||
iconLeft="tuiIconCloudLarge"
|
|
||||||
appearance="icon-success"
|
|
||||||
[style.margin-left]="'auto'"
|
|
||||||
>
|
|
||||||
Connection
|
|
||||||
</button>
|
|
||||||
<tui-badged-content
|
<tui-badged-content
|
||||||
*tuiLet="notificationService.unreadCount$ | async as unread"
|
*tuiLet="notificationService.unreadCount$ | async as unread"
|
||||||
[style.--tui-radius.%]="50"
|
[style.--tui-radius.%]="50"
|
||||||
@@ -87,6 +81,7 @@ import { NotificationService } from '../../services/notification.service'
|
|||||||
SidebarDirective,
|
SidebarDirective,
|
||||||
HeaderMenuComponent,
|
HeaderMenuComponent,
|
||||||
HeaderNotificationsComponent,
|
HeaderNotificationsComponent,
|
||||||
|
HeaderConnectionComponent,
|
||||||
TuiLetModule,
|
TuiLetModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { Router, RouterModule } from '@angular/router'
|
||||||
|
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
|
||||||
|
import { NavigationService } from '../services/navigation.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'nav[appNavigation]',
|
||||||
|
template: `
|
||||||
|
<a
|
||||||
|
class="tab"
|
||||||
|
routerLink="desktop"
|
||||||
|
routerLinkActive="tab_active"
|
||||||
|
[routerLinkActiveOptions]="{ exact: true }"
|
||||||
|
>
|
||||||
|
<tui-icon icon="tuiIconHome" class="icon" />
|
||||||
|
</a>
|
||||||
|
@for (tab of tabs$ | async; track tab) {
|
||||||
|
<a
|
||||||
|
#rla="routerLinkActive"
|
||||||
|
class="tab"
|
||||||
|
routerLinkActive="tab_active"
|
||||||
|
[routerLink]="tab.routerLink"
|
||||||
|
>
|
||||||
|
@if (tab.icon.startsWith('tuiIcon')) {
|
||||||
|
<tui-icon class="icon" [icon]="tab.icon" />
|
||||||
|
} @else {
|
||||||
|
<img class="icon" [src]="tab.icon" [alt]="tab.title" />
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
size="xs"
|
||||||
|
iconLeft="tuiIconClose"
|
||||||
|
appearance="icon"
|
||||||
|
class="close"
|
||||||
|
(click.stop.prevent)="removeTab(tab.routerLink, rla.isActive)"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
@include scrollbar-hidden;
|
||||||
|
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
overflow: auto;
|
||||||
|
// TODO: Theme
|
||||||
|
background: rgb(97 95 95 / 84%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 7.5rem;
|
||||||
|
|
||||||
|
&_active {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
// TODO: Theme
|
||||||
|
background: #373a3f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 100%;
|
||||||
|
color: var(--tui-base-08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [CommonModule, RouterModule, TuiButtonModule, TuiIconModule],
|
||||||
|
})
|
||||||
|
export class NavigationComponent {
|
||||||
|
private readonly router = inject(Router)
|
||||||
|
private readonly navigation = inject(NavigationService)
|
||||||
|
|
||||||
|
readonly tabs$ = this.navigation.getTabs()
|
||||||
|
|
||||||
|
removeTab(routerLink: string, active: boolean) {
|
||||||
|
this.navigation.removeTab(routerLink)
|
||||||
|
|
||||||
|
if (active) this.router.navigate(['/portal/desktop'])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<a
|
|
||||||
class="tab"
|
|
||||||
routerLink="desktop"
|
|
||||||
routerLinkActive="tab_active"
|
|
||||||
[routerLinkActiveOptions]="{ exact: true }"
|
|
||||||
>
|
|
||||||
<tui-icon icon="tuiIconHome" class="icon" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
*ngFor="let tab of tabs$ | async"
|
|
||||||
#rla="routerLinkActive"
|
|
||||||
class="tab"
|
|
||||||
routerLinkActive="tab_active"
|
|
||||||
[routerLink]="tab.routerLink"
|
|
||||||
>
|
|
||||||
<tui-icon
|
|
||||||
*ngIf="tab.icon.startsWith('tuiIcon'); else url"
|
|
||||||
class="icon"
|
|
||||||
[icon]="tab.icon"
|
|
||||||
/>
|
|
||||||
<ng-template #url>
|
|
||||||
<img class="icon" [src]="tab.icon" [alt]="tab.title" />
|
|
||||||
</ng-template>
|
|
||||||
<button
|
|
||||||
tuiIconButton
|
|
||||||
size="xs"
|
|
||||||
iconLeft="tuiIconClose"
|
|
||||||
appearance="icon"
|
|
||||||
class="close"
|
|
||||||
(click.stop.prevent)="removeTab(tab.routerLink, rla.isActive)"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
|
||||||
|
|
||||||
:host {
|
|
||||||
@include scrollbar-hidden;
|
|
||||||
|
|
||||||
height: 3rem;
|
|
||||||
display: flex;
|
|
||||||
// TODO: Theme
|
|
||||||
background: rgb(97 95 95 / 84%);
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 7.5rem;
|
|
||||||
|
|
||||||
&_active {
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1;
|
|
||||||
// TODO: Theme
|
|
||||||
background: #373a3f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border-radius: 100%;
|
|
||||||
color: var(--tui-base-08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|
||||||
import { Router, RouterModule } from '@angular/router'
|
|
||||||
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
|
|
||||||
import { NavigationService } from '../../services/navigation.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'nav[appNavigation]',
|
|
||||||
templateUrl: 'navigation.component.html',
|
|
||||||
styleUrls: ['navigation.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [CommonModule, RouterModule, TuiButtonModule, TuiIconModule],
|
|
||||||
})
|
|
||||||
export class NavigationComponent {
|
|
||||||
private readonly router = inject(Router)
|
|
||||||
private readonly navigation = inject(NavigationService)
|
|
||||||
|
|
||||||
readonly tabs$ = this.navigation.getTabs()
|
|
||||||
|
|
||||||
removeTab(routerLink: string, active: boolean) {
|
|
||||||
this.navigation.removeTab(routerLink)
|
|
||||||
|
|
||||||
if (active) this.router.navigate(['/portal/desktop'])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<header appHeader>My server</header>
|
|
||||||
<nav appNavigation></nav>
|
|
||||||
<main>
|
|
||||||
<router-outlet />
|
|
||||||
</main>
|
|
||||||
<app-drawer />
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
:host {
|
|
||||||
// TODO: Theme
|
|
||||||
background: url(/assets/img/background_dark.jpeg);
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,43 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { RouterOutlet } from '@angular/router'
|
||||||
import { tuiDropdownOptionsProvider } from '@taiga-ui/core'
|
import { tuiDropdownOptionsProvider } from '@taiga-ui/core'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { HeaderComponent } from './components/header/header.component'
|
||||||
|
import { NavigationComponent } from './components/navigation.component'
|
||||||
|
import { DrawerComponent } from './components/drawer/drawer.component'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: 'portal.component.html',
|
standalone: true,
|
||||||
styleUrls: ['portal.component.scss'],
|
template: `
|
||||||
|
<header appHeader>{{ name$ | async }}</header>
|
||||||
|
<nav appNavigation></nav>
|
||||||
|
<main><router-outlet /></main>
|
||||||
|
<app-drawer />
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
:host {
|
||||||
|
// TODO: Theme
|
||||||
|
background: url(/assets/img/background_dark.jpeg);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterOutlet,
|
||||||
|
HeaderComponent,
|
||||||
|
NavigationComponent,
|
||||||
|
DrawerComponent,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// TODO: Move to global
|
// TODO: Move to global
|
||||||
tuiDropdownOptionsProvider({
|
tuiDropdownOptionsProvider({
|
||||||
@@ -12,4 +45,6 @@ import { tuiDropdownOptionsProvider } from '@taiga-ui/core'
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PortalComponent {}
|
export class PortalComponent {
|
||||||
|
readonly name$ = inject(PatchDB<DataModel>).watch$('ui', 'name')
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
|
||||||
import { HeaderComponent } from './components/header/header.component'
|
|
||||||
import { PortalComponent } from './portal.component'
|
|
||||||
import { NavigationComponent } from './components/navigation/navigation.component'
|
|
||||||
import { DrawerComponent } from './components/drawer/drawer.component'
|
|
||||||
|
|
||||||
const ROUTES: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: PortalComponent,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
redirectTo: 'desktop',
|
|
||||||
pathMatch: 'full',
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'desktop',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./routes/desktop/desktop.module').then(m => m.DesktopModule),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'service',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./routes/service/service.module').then(m => m.ServiceModule),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'system',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./routes/system/system.module').then(m => m.SystemModule),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
RouterModule.forChild(ROUTES),
|
|
||||||
HeaderComponent,
|
|
||||||
NavigationComponent,
|
|
||||||
DrawerComponent,
|
|
||||||
],
|
|
||||||
declarations: [PortalComponent],
|
|
||||||
exports: [PortalComponent],
|
|
||||||
})
|
|
||||||
export class PortalModule {}
|
|
||||||
35
web/projects/ui/src/app/apps/portal/portal.routes.ts
Normal file
35
web/projects/ui/src/app/apps/portal/portal.routes.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Routes } from '@angular/router'
|
||||||
|
import { PortalComponent } from './portal.component'
|
||||||
|
|
||||||
|
const ROUTES: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: PortalComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
redirectTo: 'desktop',
|
||||||
|
pathMatch: 'full',
|
||||||
|
path: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'desktop',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./routes/desktop/desktop.component').then(
|
||||||
|
m => m.DesktopComponent,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'service',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./routes/service/service.module').then(m => m.ServiceModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'system',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./routes/system/system.module').then(m => m.SystemModule),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default ROUTES
|
||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
HostBinding,
|
HostBinding,
|
||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { TuiTilesComponent } from '@taiga-ui/kit'
|
import { TuiTilesComponent } from '@taiga-ui/kit'
|
||||||
@@ -17,7 +16,7 @@ import { TuiTilesComponent } from '@taiga-ui/kit'
|
|||||||
selector: '[desktopItem]',
|
selector: '[desktopItem]',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class DesktopItemDirective implements OnInit, OnDestroy {
|
export class DesktopItemDirective implements OnInit {
|
||||||
private readonly element: Element = inject(ElementRef).nativeElement
|
private readonly element: Element = inject(ElementRef).nativeElement
|
||||||
private readonly tiles = inject(TuiTilesComponent)
|
private readonly tiles = inject(TuiTilesComponent)
|
||||||
|
|
||||||
@@ -32,9 +31,4 @@ export class DesktopItemDirective implements OnInit, OnDestroy {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.empty) this.tiles.element = this.element
|
if (this.empty) this.tiles.element = this.element
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove after Taiga UI updated to 3.40.0
|
|
||||||
ngOnDestroy() {
|
|
||||||
if (this.tiles.element === this.element) this.tiles.element = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
@@ -6,18 +7,48 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
|
import { RouterModule } from '@angular/router'
|
||||||
|
import { DragScrollerDirective } from '@start9labs/shared'
|
||||||
import { EMPTY_QUERY, TUI_PARENT_STOP } from '@taiga-ui/cdk'
|
import { EMPTY_QUERY, TUI_PARENT_STOP } from '@taiga-ui/cdk'
|
||||||
import { tuiFadeIn, tuiScaleIn } from '@taiga-ui/core'
|
import {
|
||||||
import { TuiTileComponent, TuiTilesComponent } from '@taiga-ui/kit'
|
tuiFadeIn,
|
||||||
|
TuiLoaderModule,
|
||||||
|
tuiScaleIn,
|
||||||
|
TuiSvgModule,
|
||||||
|
} from '@taiga-ui/core'
|
||||||
|
import { TuiFadeModule } from '@taiga-ui/experimental'
|
||||||
|
import {
|
||||||
|
TuiTileComponent,
|
||||||
|
TuiTilesComponent,
|
||||||
|
TuiTilesModule,
|
||||||
|
} from '@taiga-ui/kit'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { DesktopService } from '../../services/desktop.service'
|
import { DesktopService } from '../../services/desktop.service'
|
||||||
import { DektopLoadingService } from './dektop-loading.service'
|
import { DektopLoadingService } from './dektop-loading.service'
|
||||||
|
import { CardComponent } from '../../components/card.component'
|
||||||
|
import { DesktopItemDirective } from './desktop-item.directive'
|
||||||
|
import { ToNavigationItemPipe } from '../../pipes/to-navigation-item'
|
||||||
|
import { ToBadgePipe } from '../../pipes/to-badge'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
standalone: true,
|
||||||
templateUrl: 'desktop.component.html',
|
templateUrl: 'desktop.component.html',
|
||||||
styleUrls: ['desktop.component.scss'],
|
styleUrls: ['desktop.component.scss'],
|
||||||
animations: [TUI_PARENT_STOP, tuiScaleIn, tuiFadeIn],
|
animations: [TUI_PARENT_STOP, tuiScaleIn, tuiFadeIn],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterModule,
|
||||||
|
CardComponent,
|
||||||
|
DesktopItemDirective,
|
||||||
|
TuiSvgModule,
|
||||||
|
TuiLoaderModule,
|
||||||
|
TuiTilesModule,
|
||||||
|
ToNavigationItemPipe,
|
||||||
|
TuiFadeModule,
|
||||||
|
DragScrollerDirective,
|
||||||
|
ToBadgePipe,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class DesktopComponent {
|
export class DesktopComponent {
|
||||||
@ViewChildren(TuiTileComponent, { read: ElementRef })
|
@ViewChildren(TuiTileComponent, { read: ElementRef })
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
|
||||||
import { DragScrollerDirective } from '@start9labs/shared'
|
|
||||||
import { TuiLoaderModule, TuiSvgModule } from '@taiga-ui/core'
|
|
||||||
import { TuiFadeModule } from '@taiga-ui/experimental'
|
|
||||||
import { TuiTilesModule } from '@taiga-ui/kit'
|
|
||||||
import { DesktopComponent } from './desktop.component'
|
|
||||||
import { CardComponent } from '../../components/card/card.component'
|
|
||||||
import { ToNavigationItemPipe } from '../../pipes/to-navigation-item'
|
|
||||||
import { ToBadgePipe } from '../../pipes/to-badge'
|
|
||||||
import { DesktopItemDirective } from './desktop-item.directive'
|
|
||||||
|
|
||||||
const ROUTES: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: DesktopComponent,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
CardComponent,
|
|
||||||
DesktopItemDirective,
|
|
||||||
TuiSvgModule,
|
|
||||||
TuiLoaderModule,
|
|
||||||
TuiTilesModule,
|
|
||||||
ToNavigationItemPipe,
|
|
||||||
RouterModule.forChild(ROUTES),
|
|
||||||
TuiFadeModule,
|
|
||||||
DragScrollerDirective,
|
|
||||||
ToBadgePipe,
|
|
||||||
],
|
|
||||||
declarations: [DesktopComponent],
|
|
||||||
exports: [DesktopComponent],
|
|
||||||
})
|
|
||||||
export class DesktopModule {}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { inject, Pipe, PipeTransform, Type } from '@angular/core'
|
import { inject, Pipe, PipeTransform, Type } from '@angular/core'
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
import { Params } from '@angular/router'
|
||||||
import { Manifest } from '@start9labs/marketplace'
|
import { Manifest } from '@start9labs/marketplace'
|
||||||
import { MarkdownComponent } from '@start9labs/shared'
|
import { MarkdownComponent } from '@start9labs/shared'
|
||||||
import { TuiDialogService } from '@taiga-ui/core'
|
import { TuiDialogService } from '@taiga-ui/core'
|
||||||
@@ -33,8 +33,6 @@ export class ToMenuPipe implements PipeTransform {
|
|||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
private readonly route = inject(ActivatedRoute)
|
|
||||||
private readonly router = inject(Router)
|
|
||||||
private readonly proxyService = inject(ProxyService)
|
private readonly proxyService = inject(ProxyService)
|
||||||
|
|
||||||
transform({ manifest, installed }: PackageDataEntry): ServiceMenu[] {
|
transform({ manifest, installed }: PackageDataEntry): ServiceMenu[] {
|
||||||
|
|||||||
@@ -10,15 +10,6 @@ import { SettingBtn } from '../settings.types'
|
|||||||
<button *ngIf="button.action" class="g-action" (click)="button.action()">
|
<button *ngIf="button.action" class="g-action" (click)="button.action()">
|
||||||
<ng-container *ngTemplateOutlet="template" />
|
<ng-container *ngTemplateOutlet="template" />
|
||||||
</button>
|
</button>
|
||||||
<a
|
|
||||||
*ngIf="button.href"
|
|
||||||
class="g-action"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
[href]="button.href"
|
|
||||||
>
|
|
||||||
<ng-container *ngTemplateOutlet="template" />
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
*ngIf="button.routerLink"
|
*ngIf="button.routerLink"
|
||||||
class="g-action"
|
class="g-action"
|
||||||
@@ -34,7 +25,6 @@ import { SettingBtn } from '../settings.types'
|
|||||||
<ng-content />
|
<ng-content />
|
||||||
</div>
|
</div>
|
||||||
<tui-icon *ngIf="button.routerLink" icon="tuiIconChevronRight" />
|
<tui-icon *ngIf="button.routerLink" icon="tuiIconChevronRight" />
|
||||||
<tui-icon *ngIf="button.href" icon="tuiIconExternalLink" />
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
|
|||||||
@@ -111,26 +111,6 @@ export class SettingsService {
|
|||||||
routerLink: 'sessions',
|
routerLink: 'sessions',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Support: [
|
|
||||||
{
|
|
||||||
title: 'User Manual',
|
|
||||||
description: 'Discover what StartOS can do',
|
|
||||||
icon: 'tuiIconMap',
|
|
||||||
href: 'https://docs.start9.com/0.3.5.x/user-manual',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Contact Support',
|
|
||||||
description: 'Get help from the Start9 team and community',
|
|
||||||
icon: 'tuiIconMessageSquare',
|
|
||||||
href: 'https://start9.com/contact',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Donate to Start9',
|
|
||||||
description: `Support StartOS development`,
|
|
||||||
icon: 'tuiIconDollarSign',
|
|
||||||
href: 'https://donate.start9.com',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setBrowserTab(): Promise<void> {
|
private async setBrowserTab(): Promise<void> {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export interface SettingBtn {
|
|||||||
description: string
|
description: string
|
||||||
icon: string
|
icon: string
|
||||||
action?: Function
|
action?: Function
|
||||||
href?: string
|
|
||||||
routerLink?: string
|
routerLink?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ const routes: Routes = [
|
|||||||
path: 'portal',
|
path: 'portal',
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
canActivateChild: [AuthGuard],
|
canActivateChild: [AuthGuard],
|
||||||
loadChildren: () =>
|
loadChildren: () => import('./apps/portal/portal.routes').then(m => m),
|
||||||
import('./apps/portal/portal.module').then(m => m.PortalModule),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user