add tenant subject list

This commit is contained in:
Christian Werner 2025-10-31 12:18:03 +01:00
parent b1519f4307
commit 8a0dbf71dc
12 changed files with 178 additions and 7 deletions

View File

@ -0,0 +1,6 @@
export interface SubjectListDto {
id: string;
name: string;
isOwner: boolean;
visibility: string;
}

View File

@ -1,6 +1,7 @@
import {Injectable} from "@angular/core"; import {Injectable} from "@angular/core";
import {lastValueFrom, Observable} from "rxjs"; import {lastValueFrom, Observable} from "rxjs";
import {PalantirService} from "./palantir.service"; import {PalantirService} from '../palantir.service';
import {SubjectListDto} from './dtos/subject-list-dto';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -16,4 +17,12 @@ export class SubjectService extends PalantirService {
meAsync(onError?: (error: any) => void): Promise<{ "name": string }> { meAsync(onError?: (error: any) => void): Promise<{ "name": string }> {
return lastValueFrom(this.me$(onError)); return lastValueFrom(this.me$(onError));
} }
forTenant$(tenantId: string, onError?: (error: any) => void): Observable<SubjectListDto[]> {
return this.handleRequest(this.http.get<SubjectListDto[]>(this.baseUrl + '/for-tenant/' + tenantId), onError);
}
forTenantAsync(tenantId: string, onError?: (error: any) => void): Promise<SubjectListDto[]> {
return lastValueFrom(this.forTenant$(tenantId, onError));
}
} }

View File

@ -0,0 +1,12 @@
@if (loading()) {
Loading...
} @else {
@for (subject of subjects(); track subject.id) {
<app-panel class="neutral-80">
<span class="title" [ngClass]="{'owner': subject.isOwner}">{{subject.name}}</span>
<span class="actions">
<button [routerLink]="'subject/' + subject.id" class="primary outline">View Details</button>
</span>
</app-panel>
}
}

View File

@ -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;
}
}

View File

@ -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<SubjectListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SubjectListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SubjectListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<string>()
protected loading = signal<boolean>(true);
protected subjects = signal<SubjectListDto[]>([]);
private subjectService = inject(SubjectService);
async ngOnInit(): Promise<void> {
const tenantId = this.tenantId();
if (tenantId) {
const subjects = await this.subjectService.forTenantAsync(tenantId);
this.subjects.set(subjects);
this.loading.set(false);
}
}
}

View File

@ -1,7 +1,7 @@
<app-tab-group [tabs]="tabs" (tabChanged)="tabChanged($event)" [activeTabId]="activeTabId()"> <app-tab-group [tabs]="tabs" (tabChanged)="tabChanged($event)" [activeTabId]="activeTabId()">
<ng-template tabId="subjects" let-tab> <ng-template tabId="subjects" let-tab>
{{tab.name}} - content <app-subject-list [tenantId]="tenantId()!"></app-subject-list>
</ng-template> </ng-template>
<ng-template tabId="apps" let-tab> <ng-template tabId="apps" let-tab>

View File

@ -2,12 +2,14 @@ import {Component, inject, signal} from '@angular/core';
import {TabGroup, TabGroupComponent} from '../../tab-group/tab-group.component'; import {TabGroup, TabGroupComponent} from '../../tab-group/tab-group.component';
import {ActiveTabDirective} from '../../tab-group/active-tab.directive'; import {ActiveTabDirective} from '../../tab-group/active-tab.directive';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {SubjectListComponent} from '../../subject/subject-list/subject-list.component';
@Component({ @Component({
selector: 'app-tenant-detail', selector: 'app-tenant-detail',
imports: [ imports: [
TabGroupComponent, TabGroupComponent,
ActiveTabDirective ActiveTabDirective,
SubjectListComponent
], ],
templateUrl: './tenant-detail.component.html', templateUrl: './tenant-detail.component.html',
styleUrl: './tenant-detail.component.scss', styleUrl: './tenant-detail.component.scss',
@ -38,13 +40,17 @@ export class TenantDetailComponent {
] ]
protected activeTabId = signal<string | null>(null) protected activeTabId = signal<string | null>(null)
protected tenantId = signal<string | null>(null)
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router) private router = inject(Router)
constructor() { 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.activeTabId.set(tabId)
this.tenantId.set(tenantId);
} }
async tabChanged(tab: TabGroup) { async tabChanged(tab: TabGroup) {

View File

@ -4,7 +4,7 @@ import { ResolveFn } from '@angular/router';
import { subjectResolver } from './subject.resolver'; import { subjectResolver } from './subject.resolver';
describe('subjectResolver', () => { describe('subjectResolver', () => {
const executeResolver: ResolveFn<boolean> = (...resolverParameters) => const executeResolver: ResolveFn<boolean> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => subjectResolver(...resolverParameters)); TestBed.runInInjectionContext(() => subjectResolver(...resolverParameters));
beforeEach(() => { beforeEach(() => {

View File

@ -1,7 +1,7 @@
import {ResolveFn} from '@angular/router'; import {ResolveFn} from '@angular/router';
import {inject} from '@angular/core'; import {inject} from '@angular/core';
import {map} from 'rxjs'; 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<string> = (route, state) => { export const subjectResolver: ResolveFn<string> = (route, state) => {
const subjectService = inject(SubjectService); const subjectService = inject(SubjectService);

View File

@ -1,13 +1,16 @@
using HashidsNet;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Suspectus.Gandalf.Palantir.Abstractions; using Suspectus.Gandalf.Palantir.Abstractions;
using Suspectus.Gandalf.Palantir.Data.Database; 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; namespace Suspectus.Gandalf.Palantir.Api.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class SubjectController(InvokerContext invokerContext, ApplicationContext context) : ControllerBase public class SubjectController(InvokerContext invokerContext, ApplicationContext context, IHashids hashids) : ControllerBase
{ {
[HttpGet("me")] [HttpGet("me")]
public async Task<IActionResult> GetSubject() public async Task<IActionResult> 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(); var subject = await context.Subjects.Where(x => x.Id == invokerContext.Invoker!.SubjectId).Select(x => x.Name).SingleAsync();
return Ok(new { Name = subject }); return Ok(new { Name = subject });
} }
[HttpGet("for-tenant/{tenantIdHash}")]
public async Task<IActionResult> 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);
}
} }

View File

@ -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; }
}