mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
feat: basis for portal (#2352)
This commit is contained in:
BIN
frontend/projects/shared/assets/img/background_dark.jpeg
Normal file
BIN
frontend/projects/shared/assets/img/background_dark.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
BIN
frontend/projects/shared/assets/img/background_light.jpeg
Normal file
BIN
frontend/projects/shared/assets/img/background_light.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
@@ -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 {
|
tui-dialog {
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,11 @@
|
|||||||
<footer appFooter></footer>
|
<footer appFooter></footer>
|
||||||
</ion-footer>
|
</ion-footer>
|
||||||
<ion-footer
|
<ion-footer
|
||||||
*ngIf="(authService.isVerified$ | async) && !(sidebarOpen$ | async)"
|
*ngIf="
|
||||||
|
(navigation$ | async) &&
|
||||||
|
(authService.isVerified$ | async) &&
|
||||||
|
!(sidebarOpen$ | async)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<connection-bar></connection-bar>
|
<connection-bar></connection-bar>
|
||||||
</ion-footer>
|
</ion-footer>
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import { PatchDB } from 'patch-db-client'
|
|||||||
import { DataModel } from './services/patch-db/data-model'
|
import { DataModel } from './services/patch-db/data-model'
|
||||||
|
|
||||||
function hasNavigation(url: string): boolean {
|
function hasNavigation(url: string): boolean {
|
||||||
return !url.startsWith('/loading') && !url.startsWith('/diagnostic')
|
return (
|
||||||
|
!url.startsWith('/loading') &&
|
||||||
|
!url.startsWith('/diagnostic') &&
|
||||||
|
!url.startsWith('/portal')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<header appHeader>My server</header>
|
||||||
|
<nav appNavigation></nav>
|
||||||
|
<main>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</main>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
:host {
|
||||||
|
background: url(/assets/img/background_dark.jpeg);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
37
frontend/projects/ui/src/app/apps/portal/portal.module.ts
Normal file
37
frontend/projects/ui/src/app/apps/portal/portal.module.ts
Normal 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 {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{{ (service$ | async)?.manifest?.title }}
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<a
|
||||||
|
*ngFor="let service of services$ | async"
|
||||||
|
[routerLink]="service.manifest.id"
|
||||||
|
(click)="onClick(service)"
|
||||||
|
>
|
||||||
|
{{ service.manifest.title }}
|
||||||
|
</a>
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
<ng-template
|
<!--<ng-template-->
|
||||||
[tuiDialog]="show$ | async"
|
<!-- [tuiDialog]="show$ | async"-->
|
||||||
[tuiDialogOptions]="{ label: 'Refresh Needed', size: 's' }"
|
<!-- [tuiDialogOptions]="{ label: 'Refresh Needed', size: 's' }"-->
|
||||||
(tuiDialogChange)="onDismiss()"
|
<!-- (tuiDialogChange)="onDismiss()"-->
|
||||||
>
|
<!-->-->
|
||||||
Your user interface is cached and out of date. Hard refresh the page to get
|
<!-- Your user interface is cached and out of date. Hard refresh the page to get-->
|
||||||
the latest UI.
|
<!-- the latest UI.-->
|
||||||
<ul>
|
<!-- <ul>-->
|
||||||
<li>
|
<!-- <li>-->
|
||||||
<b>On Mac</b>
|
<!-- <b>On Mac</b>-->
|
||||||
: cmd + shift + R
|
<!-- : cmd + shift + R-->
|
||||||
</li>
|
<!-- </li>-->
|
||||||
<li>
|
<!-- <li>-->
|
||||||
<b>On Linux/Windows</b>
|
<!-- <b>On Linux/Windows</b>-->
|
||||||
: ctrl + shift + R
|
<!-- : ctrl + shift + R-->
|
||||||
</li>
|
<!-- </li>-->
|
||||||
</ul>
|
<!-- </ul>-->
|
||||||
<button
|
<!-- <button-->
|
||||||
tuiButton
|
<!-- tuiButton-->
|
||||||
tuiAutoFocus
|
<!-- tuiAutoFocus-->
|
||||||
appearance="secondary"
|
<!-- appearance="secondary"-->
|
||||||
style="float: right"
|
<!-- style="float: right"-->
|
||||||
(click)="onDismiss()"
|
<!-- (click)="onDismiss()"-->
|
||||||
>
|
<!-- >-->
|
||||||
Ok
|
<!-- Ok-->
|
||||||
</button>
|
<!-- </button>-->
|
||||||
</ng-template>
|
<!--</ng-template>-->
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./apps/login/login.module').then(m => m.LoginPageModule),
|
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: '',
|
path: '',
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
|
|||||||
Reference in New Issue
Block a user