Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@
// NOTE - could be refactored to separate code-workspace
// https://supabase.com/docs/guides/functions/local-development#setting-up-your-environment
"deno.enable": true,
"deno.enablePaths": [
"apps/picsa-server/supabase/functions",
"apps/picsa-server/_deprecated/docker",
"apps/picsa-server/_deprecated/scripts"
],
"deno.enablePaths": ["apps/picsa-server/supabase/functions"],
"deno.unstable": true,
"deno.config": "apps/picsa-server/supabase/functions/deno.jsonc"
}
113 changes: 18 additions & 95 deletions apps/picsa-apps/dashboard/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,101 +1,24 @@
<div class="page bg-[#fafafa]">
@if (initComplete() && authService.isAuthChecked()) {
@if (authService.authUser()) {
<mat-toolbar color="primary" class="mat-elevation-z2" style="display: flex; align-items: center">
<button mat-icon-button (click)="sidenav.opened = !sidenav.opened">
<mat-icon>menu</mat-icon>
</button>
<dashboard-deployment-select />
<span style="flex: 1; text-align: center">PICSA Dashboard</span>
<dashboard-profile-menu />
</mat-toolbar>

<mat-sidenav-container style="flex: 1">
<mat-sidenav #sidenav mode="side" opened [fixedInViewport]="true" fixedTopGap="64">
<mat-nav-list style="width: 256px">
@for (link of navLinks; track link.href) {
@if (link.children) {
<!-- Nested nav items -->
<mat-expansion-panel
class="mat-elevation-z0"
[expanded]="rla.isActive"
*roleRequired="link.roleRequired"
>
<mat-expansion-panel-header
style="padding: 0 16px"
[routerLink]="link.href"
routerLinkActive
#rla="routerLinkActive"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-panel-title>
<ng-container *ngTemplateOutlet="linkTemplate; context: { $implicit: link }"></ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
@for (child of link.children || []; track $index) {
<a
mat-list-item
[routerLink]="link.href + child.href"
routerLinkActive="mdc-list-item--activated active-link"
*roleRequired="link.roleRequired"
>
{{ child.label }}</a
>
}
</mat-expansion-panel>
} @else {
<!-- Single nav item -->
<a mat-list-item [routerLink]="link.href" routerLinkActive="mdc-list-item--activated active-link">
<ng-container *ngTemplateOutlet="linkTemplate; context: { $implicit: link }"></ng-container>
</a>
}
}
<mat-divider style="margin-top: auto"></mat-divider>
<div mat-subheader *roleRequired="'deployments.admin'">Admin</div>
<mat-divider></mat-divider>
@for (link of adminLinks; track link.href) {
<a
mat-list-item
[routerLink]="link.href"
routerLinkActive="mdc-list-item--activated active-link"
*roleRequired="link.roleRequired"
>
<ng-container *ngTemplateOutlet="linkTemplate; context: { $implicit: link }"></ng-container>
</a>
}
</mat-nav-list>
<mat-divider></mat-divider>
<div class="text-sm px-2 opacity-50 text-center">v{{ appVersion }}</div>
</mat-sidenav>
<mat-sidenav-content>
<!-- Only show router content if active deployment selected and init complete -->
@if (deployment()) {
<router-outlet></router-outlet>
} @else {
<div class="deployment-select-banner">Select a deployment to view content</div>
}
</mat-sidenav-content>
</mat-sidenav-container>
} @else {
<!-- Landing page or Public Route -->
@if (isPublicPage()) {
<router-outlet></router-outlet>
} @else {
<div class="flex-1 flex flex-col">
@switch (viewState()) {
@case ('loading') {
<mat-spinner diameter="50" class="m-auto" />
}
@case ('public') {
<router-outlet />
}
@case ('deployment-select') {
<dashboard-deployment-select-layout />
}
@case ('authenticated') {
<dashboard-authenticated-layout />
}
@default {
<dashboard-landing-page class="flex-1" />
}
}
} @else {
<div class="loading-overlay">
<mat-spinner diameter="50"></mat-spinner>
</div>
</div>
@if (shouldShowFooter()) {
<dashboard-footer class="z-10" />
}
</div>

<ng-template #linkTemplate let-link>
<div class="nav-item">
@if (link.matIcon) {
<mat-icon style="margin-right: 8px">{{ link.matIcon }}</mat-icon>
}
<span>{{ link.label }}</span>
</div>
</ng-template>
5 changes: 0 additions & 5 deletions apps/picsa-apps/dashboard/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,3 @@ mat-sidenav-content {
.active-link {
text-decoration: underline;
}
.deployment-select-banner {
display: block;
margin-top: 33vh;
text-align: center;
}
62 changes: 31 additions & 31 deletions apps/picsa-apps/dashboard/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,45 @@
import { NgTemplateOutlet } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { APP_VERSION } from '@picsa/environments/src/version';
import { PicsaDialogService } from '@picsa/shared/features';
import { SupabaseService } from '@picsa/shared/services/core/supabase';
import { filter, map } from 'rxjs/operators';

import { ADMIN_NAV_LINKS, DASHBOARD_NAV_LINKS, PUBLIC_PAGES } from './data';
import { PUBLIC_PAGES } from './data';
import { AuthenticatedLayoutComponent } from './layout/authenticated-layout/authenticated-layout';
import { DeploymentSelectLayoutComponent } from './layout/deployment-select/deployment-select.component';
import { DashboardFooterComponent } from './layout/footer/footer.component';
import { LandingPageComponent } from './layout/landing/landing.component';
import { DashboardMaterialModule } from './material.module';
import { AuthRoleRequiredDirective } from './modules/auth';
import { DashboardAuthService } from './modules/auth/services/auth.service';
import { DeploymentSelectComponent } from './modules/deployment/components';
import { DeploymentDashboardService } from './modules/deployment/deployment.service';
import { LandingPageComponent } from './modules/landing/landing.component';
import { ProfileMenuComponent } from './modules/profile/components/profile-menu/profile-menu.component';

type ViewState = 'public' | 'authenticated' | 'landing' | 'loading' | 'deployment-select';

@Component({
imports: [
NgTemplateOutlet,
RouterModule,
MatProgressSpinnerModule,
DashboardMaterialModule,
DeploymentSelectComponent,
ProfileMenuComponent,
AuthRoleRequiredDirective,
DeploymentSelectLayoutComponent,
DashboardFooterComponent,
LandingPageComponent,
AuthenticatedLayoutComponent,
],
selector: 'dashboard-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements AfterViewInit {
supabaseService = inject(SupabaseService);
private supabaseService = inject(SupabaseService);
private deploymentService = inject(DeploymentDashboardService);
public authService = inject(DashboardAuthService);
private authService = inject(DashboardAuthService);
private router = inject(Router);

title = 'picsa-apps-dashboard';
navLinks = DASHBOARD_NAV_LINKS;
adminLinks = ADMIN_NAV_LINKS;
appVersion = APP_VERSION;

/** Track public pages for unauthenticated user access */
public isPublicPage = toSignal(
private isPublicPage = toSignal(
this.router.events.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
map((e) => {
Expand All @@ -55,13 +49,27 @@ export class AppComponent implements AfterViewInit {
{ initialValue: false },
);

public deployment = this.deploymentService.activeDeployment;
public viewState = computed<ViewState>(() => {
// Show public pages immediately
if (this.isPublicPage()) return 'public';
// Landing page bypass (skip loading if no data stored at all)
if (!this.authService.authUser() && localStorage.length === 0) {
return 'landing';
}

public initComplete = signal(false);
if (this.authService.authUser()) {
if (!this.deploymentService.isDeploymentChecked()) return 'loading';
if (!this.deploymentService.activeDeployment()) return 'deployment-select';
return 'authenticated';
} else {
if (!this.authService.isAuthChecked()) return 'loading';
return 'landing';
}
});
public shouldShowFooter = computed(() => !['authenticated', 'loading'].includes(this.viewState()));

constructor() {
const dialogService = inject(PicsaDialogService);

// HACK - disable translation in dialog to prevent loading extension app config service theme
dialogService.useTranslation = false;
}
Expand All @@ -70,13 +78,5 @@ export class AppComponent implements AfterViewInit {
// eagerly initialise supabase and deployment services to ensure available
// NOTE - do not include any services here that depend on an active deployment (could be undefined)
await this.supabaseService.ready();
// Also wait for auth service to be ready (which waits for supabase client)
// Though initComplete is purely a local signal. Auth service has its own wait.
// Wait, the plan says "Update initialization logic to wait for auth check".
// `authService.init()` awaits `supabaseAuthService.ready()` which waits for `register$`.
// It does NOT wait for `isAuthChecked` to be true. `isAuthChecked` becomes true asynchronously on auth state change.
// So we just need to ensure services are ready. The template handles `isAuthChecked`.
await this.authService.ready();
this.initComplete.set(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<mat-toolbar color="primary" class="mat-elevation-z2 flex items-center">
<button mat-icon-button (click)="sidenav.opened = !sidenav.opened">
<mat-icon>menu</mat-icon>
</button>
<dashboard-deployment-select />
<dashboard-profile-menu class="ml-auto" />
</mat-toolbar>

<mat-sidenav-container style="flex: 1">
<mat-sidenav #sidenav mode="side" opened [fixedInViewport]="true" fixedTopGap="64">
<mat-nav-list class="w-64 flex-1">
@for (link of navLinks; track link.href) { @if (link.children) {
<!-- Nested nav items -->
<mat-expansion-panel class="mat-elevation-z0" [expanded]="rla.isActive" *roleRequired="link.roleRequired">
<mat-expansion-panel-header
style="padding: 0 16px"
[routerLink]="link.href"
routerLinkActive
#rla="routerLinkActive"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-panel-title>
<ng-container *ngTemplateOutlet="linkTemplate; context: { $implicit: link }"></ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
@for (child of link.children || []; track $index) {
<a
mat-list-item
[routerLink]="link.href + child.href"
routerLinkActive="mdc-list-item--activated active-link"
*roleRequired="link.roleRequired"
>
{{ child.label }}</a
>
}
</mat-expansion-panel>
} @else {
<!-- Single nav item -->
<a mat-list-item [routerLink]="link.href" routerLinkActive="mdc-list-item--activated active-link">
<ng-container *ngTemplateOutlet="linkTemplate; context: { $implicit: link }"></ng-container>
</a>
} }
</mat-nav-list>
<mat-nav-list class="w-64 mt-auto" *roleRequired="'deployments.admin'">
<mat-divider></mat-divider>
<div mat-subheader>Admin</div>
<mat-divider></mat-divider>
@for (link of adminLinks; track link.href) {
<a
mat-list-item
[routerLink]="link.href"
routerLinkActive="mdc-list-item--activated active-link"
*roleRequired="link.roleRequired"
>
<ng-container *ngTemplateOutlet="linkTemplate; context: { $implicit: link }"></ng-container>
</a>
}
</mat-nav-list>
<mat-divider></mat-divider>
<dashboard-footer [isSidebar]="true" class="w-64" />
</mat-sidenav>
<mat-sidenav-content>
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>

<ng-template #linkTemplate let-link>
<div class="nav-item">
@if (link.matIcon) {
<mat-icon style="margin-right: 8px">{{ link.matIcon }}</mat-icon>
}
<span>{{ link.label }}</span>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { APP_VERSION } from '@picsa/environments/src/version';

import { ADMIN_NAV_LINKS, DASHBOARD_NAV_LINKS } from '../../data';
import { DashboardMaterialModule } from '../../material.module';
import { AuthRoleRequiredDirective } from '../../modules/auth';
import { DeploymentSelectComponent } from '../../modules/deployment/components';
import { ProfileMenuComponent } from '../../modules/profile/components/profile-menu/profile-menu.component';
import { DashboardFooterComponent } from '../footer/footer.component';

@Component({
imports: [
NgTemplateOutlet,
RouterModule,
DashboardFooterComponent,
MatProgressSpinnerModule,
DashboardMaterialModule,
DeploymentSelectComponent,
ProfileMenuComponent,
AuthRoleRequiredDirective,
],
selector: 'dashboard-authenticated-layout',
templateUrl: './authenticated-layout.html',
styleUrls: ['./authenticated-layout.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AuthenticatedLayoutComponent {
navLinks = DASHBOARD_NAV_LINKS;
adminLinks = ADMIN_NAV_LINKS;
appVersion = APP_VERSION;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<mat-toolbar color="primary" class="mat-elevation-z2 flex items-center">
<dashboard-profile-menu class="ml-auto" />
</mat-toolbar>

<div class="flex flex-1 flex-col items-center mt-[12vh] px-4">
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-slate-800 tracking-tight mb-2">Welcome to PICSA Dashboard</h1>
<p class="text-lg text-slate-500">Select your deployment to begin</p>
</div>

<div class="grid grid-cols-2 md:grid-cols-3 gap-8 w-full max-w-screen-sm">
@for (deployment of service.userDeployments(); track deployment.id) {
<button matButton="outlined" (click)="service.setActiveDeployment(deployment.id)" class="deployment-button group">
<div class="container" [attr.data-deployment]="deployment.id">
@if (deployment.icon_path) {
<img [src]="deployment.icon_path | storagePath" class="h-12 w-12 object-contain" />
}
<div class="text-lg font-semibold">{{ deployment.label }}</div>
</div>
</button>
}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.deployment-button {
@apply h-32;
@apply flex flex-col items-center p-4 bg-white border rounded-lg shadow-sm;
@apply transition-all duration-200 hover:shadow-xl hover:-translate-y-1 focus:outline-none focus:ring-2;
@apply border-primary;
}
// border-slate-200 hover:border-blue-500 focus:ring-blue-500
Loading