feat: basis for portal (#2352)

This commit is contained in:
Alex Inkin
2023-07-16 23:50:56 +08:00
committed by GitHub
parent bd0ddafcd0
commit 9c0c6c1bd6
27 changed files with 406 additions and 29 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -27,6 +27,18 @@
}
}
[tuiWrapper][data-appearance='success'] {
color: var(--tui-success-fill);
}
[tuiWrapper][data-appearance='warning'] {
color: var(--tui-warning-fill);
}
[tuiWrapper][data-appearance='error'] {
color: var(--tui-error-fill);
}
tui-dialog {
transform: translate3d(0, 0, 0);
}

View File

@@ -67,7 +67,11 @@
<footer appFooter></footer>
</ion-footer>
<ion-footer
*ngIf="(authService.isVerified$ | async) && !(sidebarOpen$ | async)"
*ngIf="
(navigation$ | async) &&
(authService.isVerified$ | async) &&
!(sidebarOpen$ | async)
"
>
<connection-bar></connection-bar>
</ion-footer>

View File

@@ -17,7 +17,11 @@ import { PatchDB } from 'patch-db-client'
import { DataModel } from './services/patch-db/data-model'
function hasNavigation(url: string): boolean {
return !url.startsWith('/loading') && !url.startsWith('/diagnostic')
return (
!url.startsWith('/loading') &&
!url.startsWith('/diagnostic') &&
!url.startsWith('/portal')
)
}
@Component({

View File

@@ -0,0 +1,11 @@
<ng-content></ng-content>
<div class="toolbar">
<button tuiIconButton icon="tuiIconCloudLarge" appearance="success">
Connection
</button>
<tui-badged-content size="m" [contentBottom]="4">
<button tuiIconButton icon="tuiIconBellLarge" appearance="warning">
Notifications
</button>
</tui-badged-content>
</div>

View File

@@ -0,0 +1,12 @@
:host {
display: flex;
align-items: center;
height: 4.5rem;
background: rgb(51 51 51 / 74%);
padding: 0 1rem 0 2rem;
font-size: 1.5rem;
}
.toolbar {
margin-left: auto;
}

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { TuiBadgedContentModule } from '@taiga-ui/kit'
import { TuiButtonModule } from '@taiga-ui/core'
@Component({
selector: 'header[appHeader]',
templateUrl: 'header.component.html',
styleUrls: ['header.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiBadgedContentModule, TuiButtonModule],
})
export class HeaderComponent {}

View File

@@ -0,0 +1,28 @@
<a
class="tab"
routerLink="services"
routerLinkActive="tab_active"
[routerLinkActiveOptions]="{ exact: true }"
>
<img class="icon" src="assets/img/icon_transparent.png" alt="Home" />
</a>
<a
*ngFor="let tab of tabs$ | async"
#rla="routerLinkActive"
class="tab"
routerLinkActive="tab_active"
[routerLinkActiveOptions]="{ exact: true }"
[routerLink]="tab.routerLink"
>
<img class="icon" [src]="tab.icon" [alt]="tab.title" />
<button
tuiIconButton
size="xs"
icon="tuiIconClose"
appearance="icon"
class="close"
(click.stop.prevent)="removeTab(tab, rla.isActive)"
>
Close
</button>
</a>

View File

@@ -0,0 +1,29 @@
:host {
height: 3rem;
display: flex;
background: rgb(97 95 95 / 75%);
}
.tab {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 7.5rem;
&_active {
background: #373a3f;
}
}
.icon {
width: 2rem;
height: 2rem;
border-radius: 100%;
}
.close {
position: absolute;
top: 0;
right: 0;
}

View File

@@ -0,0 +1,26 @@
import { CommonModule, Location } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { RouterModule } from '@angular/router'
import { TuiButtonModule } from '@taiga-ui/core'
import { NavigationItem, NavigationService } from './navigation.service'
@Component({
selector: 'nav[appNavigation]',
templateUrl: 'navigation.component.html',
styleUrls: ['navigation.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, RouterModule, TuiButtonModule],
})
export class NavigationComponent {
private readonly location = inject(Location)
private readonly navigation = inject(NavigationService)
readonly tabs$ = this.navigation.getTabs()
removeTab(tab: NavigationItem, active: boolean) {
this.navigation.removeTab(tab)
if (active) this.location.back()
}
}

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core'
import { BehaviorSubject, Observable } from 'rxjs'
export interface NavigationItem {
readonly routerLink: string
readonly icon: string
readonly title: string
}
@Injectable({
providedIn: 'root',
})
export class NavigationService {
readonly tabs = new BehaviorSubject<readonly NavigationItem[]>([])
getTabs(): Observable<readonly NavigationItem[]> {
return this.tabs
}
removeTab(tab: NavigationItem) {
this.tabs.next(this.tabs.value.filter(t => t !== tab))
}
addTab(tab: NavigationItem) {
if (this.tabs.value.every(t => t.routerLink !== tab.routerLink)) {
this.tabs.next([...this.tabs.value, tab])
}
}
}

View File

@@ -0,0 +1,5 @@
<header appHeader>My server</header>
<nav appNavigation></nav>
<main>
<router-outlet></router-outlet>
</main>

View File

@@ -0,0 +1,8 @@
:host {
background: url(/assets/img/background_dark.jpeg);
background-size: cover;
}
main {
flex: 1;
}

View File

@@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
templateUrl: 'portal.component.html',
styleUrls: ['portal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PortalComponent {}

View File

@@ -0,0 +1,37 @@
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'
const ROUTES: Routes = [
{
path: '',
component: PortalComponent,
children: [
{
redirectTo: 'services',
pathMatch: 'full',
path: '',
},
{
path: 'services',
loadChildren: () =>
import('./routes/services/services.module').then(
m => m.ServicesModule,
),
},
],
},
]
@NgModule({
imports: [
RouterModule.forChild(ROUTES),
HeaderComponent,
NavigationComponent,
],
declarations: [PortalComponent],
exports: [PortalComponent],
})
export class PortalModule {}

View File

@@ -0,0 +1 @@
{{ (service$ | async)?.manifest?.title }}

View File

@@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { tap } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { NavigationService } from '../../../components/navigation/navigation.service'
@Component({
templateUrl: 'service.component.html',
styleUrls: ['service.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ServiceComponent {
private readonly route = inject(ActivatedRoute)
private readonly router = inject(Router)
private readonly navigation = inject(NavigationService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly service$ = this.patch
.watch$('package-data', getPkgId(this.route))
.pipe(
tap(pkg => {
// if package disappears, navigate to list page
if (!pkg) {
this.router.navigate(['..'], { relativeTo: this.route })
} else {
this.navigation.addTab({
title: pkg.manifest.title,
routerLink: ['services', pkg.manifest.id].join('/'),
icon: pkg.icon,
})
}
}),
)
}

View File

@@ -0,0 +1,18 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ServiceComponent } from './service.component'
const ROUTES: Routes = [
{
path: '',
component: ServiceComponent,
},
]
@NgModule({
imports: [CommonModule, RouterModule.forChild(ROUTES)],
declarations: [ServiceComponent],
exports: [ServiceComponent],
})
export class ServiceModule {}

View File

@@ -0,0 +1,7 @@
<a
*ngFor="let service of services$ | async"
[routerLink]="service.manifest.id"
(click)="onClick(service)"
>
{{ service.manifest.title }}
</a>

View File

@@ -0,0 +1,23 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { NavigationService } from '../../components/navigation/navigation.service'
import { ServicesService } from './services.service'
@Component({
templateUrl: 'services.component.html',
styleUrls: ['services.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ServicesComponent {
private readonly navigation = inject(NavigationService)
readonly services$ = inject(ServicesService)
onClick({ manifest, icon }: PackageDataEntry) {
this.navigation.addTab({
title: manifest.title,
routerLink: ['services', manifest.id].join('/'),
icon,
})
}
}

View File

@@ -0,0 +1,23 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ServicesComponent } from './services.component'
const ROUTES: Routes = [
{
path: '',
component: ServicesComponent,
},
{
path: ':pkgId',
loadChildren: () =>
import('./service/service.module').then(m => m.ServiceModule),
},
]
@NgModule({
imports: [CommonModule, RouterModule.forChild(ROUTES)],
declarations: [ServicesComponent],
exports: [ServicesComponent],
})
export class ServicesModule {}

View File

@@ -0,0 +1,36 @@
import { inject, Injectable } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { filter, map, Observable, pairwise, shareReplay, startWith } from 'rxjs'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
@Injectable({
providedIn: 'root',
})
export class ServicesService extends Observable<readonly PackageDataEntry[]> {
private readonly services$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$('package-data')
.pipe(
map(pkgs => Object.values(pkgs)),
startWith([]),
pairwise(),
filter(([prev, next]) => {
const length = next.length
return !length || prev.length !== length
}),
map(([_, pkgs]) =>
pkgs.sort((a, b) =>
b.manifest.title.toLowerCase() > a.manifest.title.toLowerCase()
? -1
: 1,
),
),
shareReplay(1),
)
constructor() {
super(subscriber => this.services$.subscribe(subscriber))
}
}

View File

@@ -1,27 +1,27 @@
<ng-template
[tuiDialog]="show$ | async"
[tuiDialogOptions]="{ label: 'Refresh Needed', size: 's' }"
(tuiDialogChange)="onDismiss()"
>
Your user interface is cached and out of date. Hard refresh the page to get
the latest UI.
<ul>
<li>
<b>On Mac</b>
: cmd + shift + R
</li>
<li>
<b>On Linux/Windows</b>
: ctrl + shift + R
</li>
</ul>
<button
tuiButton
tuiAutoFocus
appearance="secondary"
style="float: right"
(click)="onDismiss()"
>
Ok
</button>
</ng-template>
<!--<ng-template-->
<!-- [tuiDialog]="show$ | async"-->
<!-- [tuiDialogOptions]="{ label: 'Refresh Needed', size: 's' }"-->
<!-- (tuiDialogChange)="onDismiss()"-->
<!--&gt;-->
<!-- Your user interface is cached and out of date. Hard refresh the page to get-->
<!-- the latest UI.-->
<!-- <ul>-->
<!-- <li>-->
<!-- <b>On Mac</b>-->
<!-- : cmd + shift + R-->
<!-- </li>-->
<!-- <li>-->
<!-- <b>On Linux/Windows</b>-->
<!-- : ctrl + shift + R-->
<!-- </li>-->
<!-- </ul>-->
<!-- <button-->
<!-- tuiButton-->
<!-- tuiAutoFocus-->
<!-- appearance="secondary"-->
<!-- style="float: right"-->
<!-- (click)="onDismiss()"-->
<!-- >-->
<!-- Ok-->
<!-- </button>-->
<!--</ng-template>-->

View File

@@ -22,6 +22,13 @@ const routes: Routes = [
loadChildren: () =>
import('./apps/login/login.module').then(m => m.LoginPageModule),
},
{
path: 'portal',
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
loadChildren: () =>
import('./apps/portal/portal.module').then(m => m.PortalModule),
},
{
path: '',
canActivate: [AuthGuard],