diff --git a/frontend/projects/shared/assets/img/background_dark.jpeg b/frontend/projects/shared/assets/img/background_dark.jpeg new file mode 100644 index 000000000..59ed7d7d7 Binary files /dev/null and b/frontend/projects/shared/assets/img/background_dark.jpeg differ diff --git a/frontend/projects/shared/assets/img/background_light.jpeg b/frontend/projects/shared/assets/img/background_light.jpeg new file mode 100644 index 000000000..0795145f9 Binary files /dev/null and b/frontend/projects/shared/assets/img/background_light.jpeg differ diff --git a/frontend/projects/shared/styles/taiga.scss b/frontend/projects/shared/styles/taiga.scss index 8bd35d622..3fbc0b9bc 100644 --- a/frontend/projects/shared/styles/taiga.scss +++ b/frontend/projects/shared/styles/taiga.scss @@ -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); } diff --git a/frontend/projects/ui/src/app/app.component.html b/frontend/projects/ui/src/app/app.component.html index dcc65b8c6..78ac210ca 100644 --- a/frontend/projects/ui/src/app/app.component.html +++ b/frontend/projects/ui/src/app/app.component.html @@ -67,7 +67,11 @@ diff --git a/frontend/projects/ui/src/app/app.component.ts b/frontend/projects/ui/src/app/app.component.ts index 68e77ffee..c8b2fcb5f 100644 --- a/frontend/projects/ui/src/app/app.component.ts +++ b/frontend/projects/ui/src/app/app.component.ts @@ -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({ diff --git a/frontend/projects/ui/src/app/apps/portal/components/header/header.component.html b/frontend/projects/ui/src/app/apps/portal/components/header/header.component.html new file mode 100644 index 000000000..54abed9a7 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/components/header/header.component.html @@ -0,0 +1,11 @@ + +
+ + + + +
diff --git a/frontend/projects/ui/src/app/apps/portal/components/header/header.component.scss b/frontend/projects/ui/src/app/apps/portal/components/header/header.component.scss new file mode 100644 index 000000000..85ded7f11 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/components/header/header.component.scss @@ -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; +} diff --git a/frontend/projects/ui/src/app/apps/portal/components/header/header.component.ts b/frontend/projects/ui/src/app/apps/portal/components/header/header.component.ts new file mode 100644 index 000000000..8e84d8f47 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/components/header/header.component.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html new file mode 100644 index 000000000..baa36ccd1 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html @@ -0,0 +1,28 @@ + + Home + + + + + diff --git a/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.scss b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.scss new file mode 100644 index 000000000..2e2834f3a --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.scss @@ -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; +} diff --git a/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts new file mode 100644 index 000000000..7c9655020 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts @@ -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() + } +} diff --git a/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.service.ts b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.service.ts new file mode 100644 index 000000000..232634697 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.service.ts @@ -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([]) + + getTabs(): Observable { + 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]) + } + } +} diff --git a/frontend/projects/ui/src/app/apps/portal/portal.component.html b/frontend/projects/ui/src/app/apps/portal/portal.component.html new file mode 100644 index 000000000..5eb48eb41 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/portal.component.html @@ -0,0 +1,5 @@ +
My server
+ +
+ +
diff --git a/frontend/projects/ui/src/app/apps/portal/portal.component.scss b/frontend/projects/ui/src/app/apps/portal/portal.component.scss new file mode 100644 index 000000000..128c72f8a --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/portal.component.scss @@ -0,0 +1,8 @@ +:host { + background: url(/assets/img/background_dark.jpeg); + background-size: cover; +} + +main { + flex: 1; +} diff --git a/frontend/projects/ui/src/app/apps/portal/portal.component.ts b/frontend/projects/ui/src/app/apps/portal/portal.component.ts new file mode 100644 index 000000000..57d53a6fe --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/portal.component.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/apps/portal/portal.module.ts b/frontend/projects/ui/src/app/apps/portal/portal.module.ts new file mode 100644 index 000000000..5512c0854 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/portal.module.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.component.html b/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.component.html new file mode 100644 index 000000000..044b180cf --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.component.html @@ -0,0 +1 @@ +{{ (service$ | async)?.manifest?.title }} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.component.scss b/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.component.ts new file mode 100644 index 000000000..380573d1a --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.component.ts @@ -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) + + 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, + }) + } + }), + ) +} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.module.ts b/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.module.ts new file mode 100644 index 000000000..4d187310b --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/services/service/service.module.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/services/services.component.html b/frontend/projects/ui/src/app/apps/portal/routes/services/services.component.html new file mode 100644 index 000000000..51dcfae2a --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/services/services.component.html @@ -0,0 +1,7 @@ + + {{ service.manifest.title }} + diff --git a/frontend/projects/ui/src/app/apps/portal/routes/services/services.component.scss b/frontend/projects/ui/src/app/apps/portal/routes/services/services.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/projects/ui/src/app/apps/portal/routes/services/services.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/services/services.component.ts new file mode 100644 index 000000000..bd9e8749b --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/services/services.component.ts @@ -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, + }) + } +} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/services/services.module.ts b/frontend/projects/ui/src/app/apps/portal/routes/services/services.module.ts new file mode 100644 index 000000000..82488da4a --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/services/services.module.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/services/services.service.ts b/frontend/projects/ui/src/app/apps/portal/routes/services/services.service.ts new file mode 100644 index 000000000..0a0683fbc --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/services/services.service.ts @@ -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 { + private readonly services$ = inject>(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)) + } +} diff --git a/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.html b/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.html index db2480687..cb412aae9 100644 --- a/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.html +++ b/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.html @@ -1,27 +1,27 @@ - - Your user interface is cached and out of date. Hard refresh the page to get - the latest UI. -
    -
  • - On Mac - : cmd + shift + R -
  • -
  • - On Linux/Windows - : ctrl + shift + R -
  • -
- -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/projects/ui/src/app/routing.module.ts b/frontend/projects/ui/src/app/routing.module.ts index f8b67c9f9..f713d6fa8 100644 --- a/frontend/projects/ui/src/app/routing.module.ts +++ b/frontend/projects/ui/src/app/routing.module.ts @@ -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],