diff --git a/src/angular/frontend/src/app/clients/gandalf/mithrandir/subject/dtos/subject-list-dto.ts b/src/angular/frontend/src/app/clients/gandalf/mithrandir/subject/dtos/subject-list-dto.ts new file mode 100644 index 0000000..ba09617 --- /dev/null +++ b/src/angular/frontend/src/app/clients/gandalf/mithrandir/subject/dtos/subject-list-dto.ts @@ -0,0 +1,6 @@ +export interface SubjectListDto { + id: string; + name: string; + isOwner: boolean; + visibility: string; +} diff --git a/src/angular/frontend/src/app/clients/gandalf/mithrandir/subject.service.ts b/src/angular/frontend/src/app/clients/gandalf/mithrandir/subject/subject.service.ts similarity index 52% rename from src/angular/frontend/src/app/clients/gandalf/mithrandir/subject.service.ts rename to src/angular/frontend/src/app/clients/gandalf/mithrandir/subject/subject.service.ts index 2646ba2..2d45df3 100644 --- a/src/angular/frontend/src/app/clients/gandalf/mithrandir/subject.service.ts +++ b/src/angular/frontend/src/app/clients/gandalf/mithrandir/subject/subject.service.ts @@ -1,6 +1,7 @@ import {Injectable} from "@angular/core"; import {lastValueFrom, Observable} from "rxjs"; -import {PalantirService} from "./palantir.service"; +import {PalantirService} from '../palantir.service'; +import {SubjectListDto} from './dtos/subject-list-dto'; @Injectable({ providedIn: 'root' @@ -16,4 +17,12 @@ export class SubjectService extends PalantirService { meAsync(onError?: (error: any) => void): Promise<{ "name": string }> { return lastValueFrom(this.me$(onError)); } + + forTenant$(tenantId: string, onError?: (error: any) => void): Observable { + return this.handleRequest(this.http.get(this.baseUrl + '/for-tenant/' + tenantId), onError); + } + + forTenantAsync(tenantId: string, onError?: (error: any) => void): Promise { + return lastValueFrom(this.forTenant$(tenantId, onError)); + } } diff --git a/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.html b/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.html new file mode 100644 index 0000000..7bbf7a2 --- /dev/null +++ b/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.html @@ -0,0 +1,12 @@ +@if (loading()) { + Loading... +} @else { + @for (subject of subjects(); track subject.id) { + + {{subject.name}} + + + + + } +} diff --git a/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.scss b/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.scss new file mode 100644 index 0000000..006f55c --- /dev/null +++ b/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.scss @@ -0,0 +1,34 @@ +:host { + display: flex; + flex-direction: column; + gap: .5rem; + + app-panel { + display: flex; + align-items: center; + } + + .title { + + display: flex; + align-items: center; + + &.owner { + &::before { + background-color: var(--primary-30); + color: var(--neutral-90); + font-size: 0.7rem; + line-height: 0.7rem; + padding: 0.15rem 0.3rem; + font-weight: bold; + border-radius: 0.5rem; + margin-right: 1ch; + content: 'Owner'; + } + } + } + + .actions { + margin-left: auto; + } +} diff --git a/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.spec.ts b/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.spec.ts new file mode 100644 index 0000000..36b420f --- /dev/null +++ b/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubjectListComponent } from './subject-list.component'; + +describe('SubjectListComponent', () => { + let component: SubjectListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SubjectListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SubjectListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.ts b/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.ts new file mode 100644 index 0000000..d5dcbed --- /dev/null +++ b/src/angular/frontend/src/app/components/subject/subject-list/subject-list.component.ts @@ -0,0 +1,38 @@ +import {Component, inject, input, OnInit, signal} from '@angular/core'; +import {PanelComponent} from '../../panel/panel.component'; +import {RouterLink} from '@angular/router'; +import {NgClass} from '@angular/common'; +import {SubjectService} from '../../../clients/gandalf/mithrandir/subject/subject.service'; +import {SubjectListDto} from '../../../clients/gandalf/mithrandir/subject/dtos/subject-list-dto'; + +@Component({ + selector: 'app-subject-list', + imports: [ + PanelComponent, + RouterLink, + NgClass + ], + templateUrl: './subject-list.component.html', + styleUrl: './subject-list.component.scss', +}) +export class SubjectListComponent implements OnInit { + + public tenantId = input() + + protected loading = signal(true); + protected subjects = signal([]); + + private subjectService = inject(SubjectService); + + async ngOnInit(): Promise { + + const tenantId = this.tenantId(); + + if (tenantId) { + const subjects = await this.subjectService.forTenantAsync(tenantId); + this.subjects.set(subjects); + this.loading.set(false); + } + } + +} diff --git a/src/angular/frontend/src/app/components/tenant/tenant-detail/tenant-detail.component.html b/src/angular/frontend/src/app/components/tenant/tenant-detail/tenant-detail.component.html index 641a1f2..3e22eff 100644 --- a/src/angular/frontend/src/app/components/tenant/tenant-detail/tenant-detail.component.html +++ b/src/angular/frontend/src/app/components/tenant/tenant-detail/tenant-detail.component.html @@ -1,7 +1,7 @@ - {{tab.name}} - content + diff --git a/src/angular/frontend/src/app/components/tenant/tenant-detail/tenant-detail.component.ts b/src/angular/frontend/src/app/components/tenant/tenant-detail/tenant-detail.component.ts index b1eb85d..f98d67a 100644 --- a/src/angular/frontend/src/app/components/tenant/tenant-detail/tenant-detail.component.ts +++ b/src/angular/frontend/src/app/components/tenant/tenant-detail/tenant-detail.component.ts @@ -2,12 +2,14 @@ import {Component, inject, signal} from '@angular/core'; import {TabGroup, TabGroupComponent} from '../../tab-group/tab-group.component'; import {ActiveTabDirective} from '../../tab-group/active-tab.directive'; import {ActivatedRoute, Router} from '@angular/router'; +import {SubjectListComponent} from '../../subject/subject-list/subject-list.component'; @Component({ selector: 'app-tenant-detail', imports: [ TabGroupComponent, - ActiveTabDirective + ActiveTabDirective, + SubjectListComponent ], templateUrl: './tenant-detail.component.html', styleUrl: './tenant-detail.component.scss', @@ -38,13 +40,17 @@ export class TenantDetailComponent { ] protected activeTabId = signal(null) + protected tenantId = signal(null) private route = inject(ActivatedRoute); private router = inject(Router) constructor() { - const tabId = this.route.snapshot.queryParamMap.get('tab'); + const routeSnapshot = this.route.snapshot; + const tenantId = routeSnapshot.paramMap.get('id'); + const tabId = routeSnapshot.queryParamMap.get('tab'); this.activeTabId.set(tabId) + this.tenantId.set(tenantId); } async tabChanged(tab: TabGroup) { diff --git a/src/angular/frontend/src/app/resolvers/subject.resolver.spec.ts b/src/angular/frontend/src/app/resolvers/subject.resolver.spec.ts index a8ef3db..189ef40 100644 --- a/src/angular/frontend/src/app/resolvers/subject.resolver.spec.ts +++ b/src/angular/frontend/src/app/resolvers/subject.resolver.spec.ts @@ -4,7 +4,7 @@ import { ResolveFn } from '@angular/router'; import { subjectResolver } from './subject.resolver'; describe('subjectResolver', () => { - const executeResolver: ResolveFn = (...resolverParameters) => + const executeResolver: ResolveFn = (...resolverParameters) => TestBed.runInInjectionContext(() => subjectResolver(...resolverParameters)); beforeEach(() => { diff --git a/src/angular/frontend/src/app/resolvers/subject.resolver.ts b/src/angular/frontend/src/app/resolvers/subject.resolver.ts index 9d05963..f461998 100644 --- a/src/angular/frontend/src/app/resolvers/subject.resolver.ts +++ b/src/angular/frontend/src/app/resolvers/subject.resolver.ts @@ -1,7 +1,7 @@ import {ResolveFn} from '@angular/router'; import {inject} from '@angular/core'; import {map} from 'rxjs'; -import {SubjectService} from '../clients/gandalf/mithrandir/subject.service'; +import {SubjectService} from '../clients/gandalf/mithrandir/subject/subject.service'; export const subjectResolver: ResolveFn = (route, state) => { const subjectService = inject(SubjectService); diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/SubjectController.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/SubjectController.cs index 4d2ea8a..5949050 100644 --- a/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/SubjectController.cs +++ b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/SubjectController.cs @@ -1,13 +1,16 @@ +using HashidsNet; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Suspectus.Gandalf.Palantir.Abstractions; using Suspectus.Gandalf.Palantir.Data.Database; +using Suspectus.Gandalf.Palantir.Data.Dto.Subject; +using Suspectus.Gandalf.Palantir.Data.Entities.Base; namespace Suspectus.Gandalf.Palantir.Api.Controllers; [ApiController] [Route("api/[controller]")] -public class SubjectController(InvokerContext invokerContext, ApplicationContext context) : ControllerBase +public class SubjectController(InvokerContext invokerContext, ApplicationContext context, IHashids hashids) : ControllerBase { [HttpGet("me")] public async Task GetSubject() @@ -15,4 +18,33 @@ public class SubjectController(InvokerContext invokerContext, ApplicationContext var subject = await context.Subjects.Where(x => x.Id == invokerContext.Invoker!.SubjectId).Select(x => x.Name).SingleAsync(); return Ok(new { Name = subject }); } + + [HttpGet("for-tenant/{tenantIdHash}")] + public async Task GetSubjectsByTenantId(string tenantIdHash, InvokerContext invokerContext) + { + if (!hashids.TryDecodeSingleLong(tenantIdHash, out var tenantId)) + { + return BadRequest(); + } + + var tenantOwnerId = await context.Tenants.Where(x => x.Id == tenantId).Select(x => (long?)x.OwnerId).SingleOrDefaultAsync(); + + if (tenantOwnerId is null) + return NotFound(); + + var subjects = await context.TenantSubjectRelations + .Where(x => x.TenantId == tenantId) + .Select(x => x.Subject!) + .ToListAsync(); + + var dtos = subjects.Select(x => new SubjectListDto + { + Id = hashids.EncodeLong(x.Id!.Value), + Name = x.Name, + IsOwner = x.Id == tenantOwnerId, + Visibility = x.Visibility + }); + + return Ok(dtos); + } } \ No newline at end of file diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Data/Dto/Subject/SubjectListDto.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Data/Dto/Subject/SubjectListDto.cs new file mode 100644 index 0000000..c5401c4 --- /dev/null +++ b/src/dotnet/Suspectus.Gandalf.Palantir.Data/Dto/Subject/SubjectListDto.cs @@ -0,0 +1,11 @@ +using Suspectus.Gandalf.Palantir.Data.Entities.Base; + +namespace Suspectus.Gandalf.Palantir.Data.Dto.Subject; + +public class SubjectListDto +{ + public required string Id { get; set; } + public required string Name { get; set; } + public required bool IsOwner { get; set; } + public required EntityVisibility Visibility { get; set; } +} \ No newline at end of file