Add icon component, update logo handling, and enhance user profile display
This commit is contained in:
parent
d0fd155ea3
commit
6db373573c
@ -18,7 +18,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist/frontend",
|
||||
"index": "src/index.html",
|
||||
@ -64,7 +64,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "frontend:build:production"
|
||||
@ -79,10 +79,10 @@
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
@ -104,5 +104,31 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,24 +10,24 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.1.0",
|
||||
"@angular/common": "^19.1.0",
|
||||
"@angular/compiler": "^19.1.0",
|
||||
"@angular/core": "^19.1.0",
|
||||
"@angular/forms": "^19.1.0",
|
||||
"@angular/platform-browser": "^19.1.0",
|
||||
"@angular/platform-browser-dynamic": "^19.1.0",
|
||||
"@angular/platform-server": "^19.1.0",
|
||||
"@angular/router": "^19.1.0",
|
||||
"@angular/ssr": "^19.2.14",
|
||||
"@angular/animations": "^20.3.6",
|
||||
"@angular/common": "^20.3.6",
|
||||
"@angular/compiler": "^20.3.6",
|
||||
"@angular/core": "^20.3.6",
|
||||
"@angular/forms": "^20.3.6",
|
||||
"@angular/platform-browser": "^20.3.6",
|
||||
"@angular/platform-browser-dynamic": "^20.3.6",
|
||||
"@angular/platform-server": "^20.3.6",
|
||||
"@angular/router": "^20.3.6",
|
||||
"@angular/ssr": "^20.3.6",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.1.7",
|
||||
"@angular/cli": "^19.1.7",
|
||||
"@angular/compiler-cli": "^19.1.0",
|
||||
"@angular/build": "^20.3.6",
|
||||
"@angular/cli": "^20.3.6",
|
||||
"@angular/compiler-cli": "^20.3.6",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^18.18.0",
|
||||
"jasmine-core": "~5.5.0",
|
||||
@ -36,6 +36,6 @@
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
"typescript": "~5.9.3"
|
||||
}
|
||||
}
|
||||
5616
src/angular/frontend/pnpm-lock.yaml
generated
5616
src/angular/frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
src/angular/frontend/public/full_logo.svg
(Stored with Git LFS)
BIN
src/angular/frontend/public/full_logo.svg
(Stored with Git LFS)
Binary file not shown.
@ -3,6 +3,7 @@ import {LoginComponent} from './components/login/login.component';
|
||||
import {HomeComponent} from './components/home/home.component';
|
||||
import {authGuard} from './guards/auth.guard';
|
||||
import {ShellComponent} from './components/shell/shell.component';
|
||||
import {subjectResolver} from './resolvers/subject.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@ -21,6 +22,9 @@ export const routes: Routes = [
|
||||
},
|
||||
],
|
||||
canActivate: [authGuard],
|
||||
resolve: {
|
||||
user: subjectResolver,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {GandalfClient} from '../gandalf-client';
|
||||
import {lastValueFrom, Observable, ObservableInput} from 'rxjs';
|
||||
import {lastValueFrom, Observable} from 'rxjs';
|
||||
|
||||
export interface ValidateCredentialsCommand {
|
||||
usernameOrEmail: string;
|
||||
@ -31,4 +31,13 @@ export class AuthService extends GandalfClient {
|
||||
return lastValueFrom(this.login$(command, onError));
|
||||
}
|
||||
|
||||
logout$(onError?: (error: any) => void): Observable<void> {
|
||||
return this.handleRequest(this.http.get<void>(this.base + '/logout'), onError);
|
||||
}
|
||||
|
||||
logoutAsync(onError?: (error: any) => void): Promise<void> {
|
||||
return lastValueFrom(this.logout$(onError));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import {GandalfClient} from "../gandalf-client";
|
||||
import {lastValueFrom, Observable} from "rxjs";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProxyService extends GandalfClient {
|
||||
|
||||
private base = '/api/appproxy/q5lBLamL6rKy';
|
||||
|
||||
me$(onError?: (error: any) => void): Observable<{ "name": string }> {
|
||||
return this.handleRequest(this.http.get<{ "name": string }>(this.base + '/api/subject/me'), onError);
|
||||
}
|
||||
|
||||
meAsync(onError?: (error: any) => void): Promise<{ "name": string }> {
|
||||
return lastValueFrom(this.me$(onError));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
@if (path()) {
|
||||
<div class="icon masked" style='--mask-path: url("{{path()}}")'></div>
|
||||
} @else {
|
||||
<div class="icon"></div>
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
&.masked {
|
||||
mask: var(--mask-path) no-repeat;
|
||||
mask-size: contain;
|
||||
mask-position: center;
|
||||
background-color: var(--text-color);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IconComponent } from './icon.component';
|
||||
|
||||
describe('IconComponent', () => {
|
||||
let component: IconComponent;
|
||||
let fixture: ComponentFixture<IconComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IconComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(IconComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import {Component, input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-icon',
|
||||
imports: [],
|
||||
templateUrl: './icon.component.html',
|
||||
styleUrl: './icon.component.scss'
|
||||
})
|
||||
export class IconComponent {
|
||||
public path = input<string>('');
|
||||
}
|
||||
@ -1 +1,5 @@
|
||||
<p>nav-profile works!</p>
|
||||
<div class="user-info">
|
||||
<span>{{user()}}</span>
|
||||
<a href="" (click)="logout($event)">Logout</a>
|
||||
</div>
|
||||
<div class="profile-picture neutral-30"></div>
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
a {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
margin-left: 1rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {Component, computed, inject} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {toSignal} from '@angular/core/rxjs-interop';
|
||||
import {AuthService} from '../../../clients/gandalf/mithrandir/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-profile',
|
||||
@ -8,5 +11,17 @@ import { Component } from '@angular/core';
|
||||
standalone: true,
|
||||
})
|
||||
export class NavProfileComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private authService = inject(AuthService); // Replace 'any' with the actual type of AuthService
|
||||
private router = inject(Router); // Replace 'any' with the actual type of AuthService
|
||||
private data = toSignal(this.route.data);
|
||||
|
||||
|
||||
user = computed(() => this.data()?.["user"] as string | undefined ?? 'Unknown');
|
||||
|
||||
async logout($event: PointerEvent) {
|
||||
$event.preventDefault();
|
||||
await this.authService.logoutAsync();
|
||||
await this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<app-panel class="shell-nav-bar primary-50">
|
||||
<img class="logo" src="/full_logo.svg" alt="logo">
|
||||
<app-panel class="shell-nav-bar neutral-20">
|
||||
<app-icon path="/full_logo.svg" class="logo primary-icon-50"></app-icon>
|
||||
<app-nav-profile></app-nav-profile>
|
||||
</app-panel>
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
.shell-nav-bar {
|
||||
.logo {
|
||||
height: 3rem;
|
||||
$height: 3rem;
|
||||
height: $height;
|
||||
width: #{$height * 3.356876171875};
|
||||
}
|
||||
app-nav-profile {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {Component, computed, inject} from '@angular/core';
|
||||
import {NavProfileComponent} from './nav-profile/nav-profile.component';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {ActivatedRoute, RouterOutlet} from '@angular/router';
|
||||
import {PanelComponent} from '../panel/panel.component';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
import {toSignal} from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shell',
|
||||
imports: [
|
||||
NavProfileComponent,
|
||||
RouterOutlet,
|
||||
PanelComponent
|
||||
PanelComponent,
|
||||
IconComponent
|
||||
],
|
||||
templateUrl: './shell.component.html',
|
||||
styleUrl: './shell.component.scss',
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { subjectResolver } from './subject.resolver';
|
||||
|
||||
describe('subjectResolver', () => {
|
||||
const executeResolver: ResolveFn<boolean> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => subjectResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
||||
15
src/angular/frontend/src/app/resolvers/subject.resolver.ts
Normal file
15
src/angular/frontend/src/app/resolvers/subject.resolver.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {ResolveFn} from '@angular/router';
|
||||
import {inject} from '@angular/core';
|
||||
import {ProxyService} from '../clients/gandalf/mithrandir/proxy.service';
|
||||
import {map} from 'rxjs';
|
||||
|
||||
export const subjectResolver: ResolveFn<string> = (route, state) => {
|
||||
const proxyService = inject(ProxyService);
|
||||
return proxyService.me$().pipe(
|
||||
map(x => {
|
||||
console.log(x);
|
||||
return x.name;
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -14,12 +14,6 @@ $danger-color: hsl(10, 80%, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin set-neutral-colors($step: 10) {
|
||||
@for $i from 0 through 100/$step {
|
||||
--neutral-#{$i * $step}: hsl(0, 0%, #{$i * $step}%);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin set-common-colors() {
|
||||
--primary-color: #{$primary-color};
|
||||
--secondary-color: #{$secondary-color};
|
||||
@ -29,21 +23,39 @@ $danger-color: hsl(10, 80%, 50%);
|
||||
--danger-color: #{$danger-color};
|
||||
}
|
||||
|
||||
@mixin set-color-classes($name, $color, $step: 10, $padded: false) {
|
||||
$from: if($padded, 0, 1);
|
||||
$to: if($padded, 100/$step, (100/$step)-1);
|
||||
@mixin set-color-classes($name, $color, $step: 10, $padded: false, $text-color-swap-step: 30, $text-color-swap-light: var(--neutral-10), $text-color-swap-dark: var(--neutral-90)) {
|
||||
$from: 1;
|
||||
$to: 100/$step;
|
||||
|
||||
@if $padded {
|
||||
$from: 0;
|
||||
}
|
||||
|
||||
@if not $padded {
|
||||
$to: $to - 1;
|
||||
}
|
||||
|
||||
@for $i from $from through $to {
|
||||
.#{$name}-#{$i * $step} {
|
||||
--bg-color: hsl(#{color.hue($color)}, #{color.saturation($color)}, #{$i * $step}%);
|
||||
--text-color: var(--neutral-10);
|
||||
|
||||
@if $i * $step <= $text-color-swap-step {
|
||||
--text-color: #{$text-color-swap-dark};
|
||||
} @else {
|
||||
--text-color: #{$text-color-swap-light};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@for $i from $from through $to {
|
||||
.#{$name}-icon-#{$i * $step} {
|
||||
--text-color: hsl(#{color.hue($color)}, #{color.saturation($color)}, #{$i * $step}%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
@include set-neutral-colors();
|
||||
@include set-color-shades("neutral", hsl(0, 0%, 0%), 10);
|
||||
@include set-common-colors();
|
||||
@include set-color-shades("primary", $primary-color, 10);
|
||||
@include set-color-shades("secondary", $secondary-color, 10);
|
||||
@ -59,8 +71,18 @@ $danger-color: hsl(10, 80%, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@include set-color-classes("primary", $primary-color, 10, true);
|
||||
@include set-color-classes("secondary", $secondary-color, 10);
|
||||
a {
|
||||
color: var(--secondary-70);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@include set-color-classes("neutral", hsl(0, 0%, 0%), 10, true);
|
||||
@include set-color-classes("primary", $primary-color, 10, false);
|
||||
@include set-color-classes("secondary", $secondary-color, 10, false, 60);
|
||||
|
||||
|
||||
|
||||
|
||||
@ -15,8 +15,3 @@ body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#path1 { fill: black; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#path1 { fill: white; }
|
||||
}
|
||||
|
||||
@ -110,6 +110,21 @@ public class AuthController : ControllerBase
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
[HttpGet("[action]")]
|
||||
public async Task<IActionResult> Me()
|
||||
{
|
||||
var sessionExists = Request.Cookies.ContainsKey("MithrandirSession");
|
||||
|
||||
if (!sessionExists)
|
||||
{
|
||||
return Unauthorized("Session expired.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
return Ok(sessionExists);
|
||||
}
|
||||
|
||||
private string GetCacheKey(string subjectId, string appId, string tokenType)
|
||||
{
|
||||
return $"{subjectId}:{appId}:{tokenType}";
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Suspectus.Gandalf.Palantir.Abstractions;
|
||||
using Suspectus.Gandalf.Palantir.Data.Database;
|
||||
|
||||
namespace Suspectus.Gandalf.Palantir.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SubjectController(InvokerContext invokerContext, ApplicationContext context) : ControllerBase
|
||||
{
|
||||
[HttpGet("me")]
|
||||
public async Task<IActionResult> GetSubject()
|
||||
{
|
||||
var subject = await context.Subjects.Where(x => x.Id == invokerContext.Invoker!.SubjectId).Select(x => x.Name).SingleAsync();
|
||||
return Ok(new { Name = subject });
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user