add teanant apps list

This commit is contained in:
Christian Werner 2025-10-31 13:28:14 +01:00
parent 5a0ffcc73b
commit 7abcdcf8f1
16 changed files with 179 additions and 17 deletions

View File

@ -0,0 +1,5 @@
export interface AppListDto {
id: string;
name: string;
visibility: string;
}

View File

@ -1,7 +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'; import {SubjectListDto} from '../dtos/subject-list-dto';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'

View File

@ -1,7 +1,8 @@
import {Injectable} from "@angular/core"; import {Injectable} from "@angular/core";
import {lastValueFrom, Observable, of} from "rxjs"; import {lastValueFrom, Observable, of} from "rxjs";
import {PalantirService} from '../palantir.service'; import {PalantirService} from '../palantir.service';
import {TenantGridViewDto} from './dtos/tenant-grid-view-dto'; import {AppListDto} from '../dtos/app-list-dto';
import {TenantGridViewDto} from '../dtos/tenant-grid-view-dto';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -30,4 +31,12 @@ export class TenantService extends PalantirService {
getTenantAsync(id: string | null, onError?: (error: any) => void): Promise<TenantGridViewDto | null> { getTenantAsync(id: string | null, onError?: (error: any) => void): Promise<TenantGridViewDto | null> {
return lastValueFrom(this.getTenant$(id, onError)); return lastValueFrom(this.getTenant$(id, onError));
} }
getTenantApps$(id: string, onError?: (error: any) => void): Observable<AppListDto[]> {
return this.handleRequest(this.http.get<AppListDto[]>(this.baseUrl + `/${id}/apps`), onError);
}
getTenantAppsAsync(id: string, onError?: (error: any) => void): Promise<AppListDto[]> {
return lastValueFrom(this.getTenantApps$(id, onError));
}
} }

View File

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

View File

@ -0,0 +1,21 @@
:host {
display: flex;
flex-direction: column;
gap: .5rem;
app-panel {
display: flex;
align-items: center;
}
.title {
display: flex;
align-items: center;
}
.actions {
margin-left: auto;
}
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppListComponent } from './app-list.component';
describe('AppListComponent', () => {
let component: AppListComponent;
let fixture: ComponentFixture<AppListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AppListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,36 @@
import {Component, inject, input, OnInit, signal} from '@angular/core';
import {AppListDto} from '../../../clients/gandalf/mithrandir/dtos/app-list-dto';
import {TenantService} from '../../../clients/gandalf/mithrandir/tenant/tenant.service';
import {PanelComponent} from '../../panel/panel.component';
import {RouterLink} from '@angular/router';
@Component({
selector: 'app-app-list',
imports: [
PanelComponent,
RouterLink
],
templateUrl: './app-list.component.html',
styleUrl: './app-list.component.scss',
})
export class AppListComponent implements OnInit {
public tenantId = input<string>()
protected loading = signal<boolean>(true);
protected apps = signal<AppListDto[]>([]);
private tenantService = inject(TenantService);
async ngOnInit(): Promise<void> {
const tenantId = this.tenantId();
if (tenantId) {
const apps = await this.tenantService.getTenantAppsAsync(tenantId);
this.apps.set(apps);
this.loading.set(false);
}
}
}

View File

@ -3,7 +3,7 @@ import {PanelComponent} from '../../panel/panel.component';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {NgClass} from '@angular/common'; import {NgClass} from '@angular/common';
import {SubjectService} from '../../../clients/gandalf/mithrandir/subject/subject.service'; import {SubjectService} from '../../../clients/gandalf/mithrandir/subject/subject.service';
import {SubjectListDto} from '../../../clients/gandalf/mithrandir/subject/dtos/subject-list-dto'; import {SubjectListDto} from '../../../clients/gandalf/mithrandir/dtos/subject-list-dto';
@Component({ @Component({
selector: 'app-subject-list', selector: 'app-subject-list',

View File

@ -5,7 +5,7 @@
</ng-template> </ng-template>
<ng-template tabId="apps" let-tab> <ng-template tabId="apps" let-tab>
{{tab.name}} - content <app-app-list [tenantId]="tenantId()!"></app-app-list>
</ng-template> </ng-template>
<ng-template tabId="groups" let-tab> <ng-template tabId="groups" let-tab>

View File

@ -3,13 +3,15 @@ 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'; import {SubjectListComponent} from '../../subject/subject-list/subject-list.component';
import {AppListComponent} from '../../app/app-list/app-list.component';
@Component({ @Component({
selector: 'app-tenant-detail', selector: 'app-tenant-detail',
imports: [ imports: [
TabGroupComponent, TabGroupComponent,
ActiveTabDirective, ActiveTabDirective,
SubjectListComponent SubjectListComponent,
AppListComponent
], ],
templateUrl: './tenant-detail.component.html', templateUrl: './tenant-detail.component.html',
styleUrl: './tenant-detail.component.scss', styleUrl: './tenant-detail.component.scss',

View File

@ -1,10 +1,9 @@
import {Component, inject, OnInit, signal} from '@angular/core'; import {Component, inject, OnInit, signal} from '@angular/core';
import {TenantService} from '../../../clients/gandalf/mithrandir/tenant/tenant.service'; import {TenantService} from '../../../clients/gandalf/mithrandir/tenant/tenant.service';
import {TenantGridViewDto} from '../../../clients/gandalf/mithrandir/tenant/dtos/tenant-grid-view-dto';
import {PanelComponent} from '../../panel/panel.component'; import {PanelComponent} from '../../panel/panel.component';
import {LinkComponent} from '../../link/link.component';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {NgClass} from '@angular/common'; import {NgClass} from '@angular/common';
import {TenantGridViewDto} from '../../../clients/gandalf/mithrandir/dtos/tenant-grid-view-dto';
@Component({ @Component({
selector: 'app-tenant-grid', selector: 'app-tenant-grid',

View File

@ -1,8 +1,8 @@
import {ResolveFn} from "@angular/router"; import {ResolveFn} from "@angular/router";
import {TenantGridViewDto} from "../clients/gandalf/mithrandir/tenant/dtos/tenant-grid-view-dto";
import {inject} from "@angular/core"; import {inject} from "@angular/core";
import {TenantService} from "../clients/gandalf/mithrandir/tenant/tenant.service"; import {TenantService} from "../clients/gandalf/mithrandir/tenant/tenant.service";
import {map} from 'rxjs'; import {map} from 'rxjs';
import {TenantGridViewDto} from '../clients/gandalf/mithrandir/dtos/tenant-grid-view-dto';
export const tenantResolver: ResolveFn<TenantGridViewDto | null> = (route, state) => { export const tenantResolver: ResolveFn<TenantGridViewDto | null> = (route, state) => {
const tenantService = inject(TenantService); const tenantService = inject(TenantService);

View File

@ -4,7 +4,9 @@ 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.App;
using Suspectus.Gandalf.Palantir.Data.Dto.Tenant; using Suspectus.Gandalf.Palantir.Data.Dto.Tenant;
using Suspectus.Gandalf.Palantir.Data.Entities.Base;
namespace Suspectus.Gandalf.Palantir.Api.Controllers; namespace Suspectus.Gandalf.Palantir.Api.Controllers;
@ -21,7 +23,7 @@ public class TenantController : ControllerBase
_context = context; _context = context;
_hashids = hashids; _hashids = hashids;
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> Get(InvokerContext invokerContext, CancellationToken cancellationToken) public async Task<IActionResult> Get(InvokerContext invokerContext, CancellationToken cancellationToken)
{ {
@ -29,7 +31,7 @@ public class TenantController : ControllerBase
.Where(x => x.Id!.Value == invokerContext.Invoker!.SubjectId) .Where(x => x.Id!.Value == invokerContext.Invoker!.SubjectId)
.SelectMany(x => x.Tenants) .SelectMany(x => x.Tenants)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var dtos = tenantEntities.Select(x => new TenantGridViewDto var dtos = tenantEntities.Select(x => new TenantGridViewDto
{ {
Id = _hashids.EncodeLong(x.Id!.Value), Id = _hashids.EncodeLong(x.Id!.Value),
@ -41,23 +43,26 @@ public class TenantController : ControllerBase
}); });
return Ok(dtos); return Ok(dtos);
} }
[HttpGet("{idHash}")] [HttpGet("{idHash}")]
public async Task<IActionResult> Get(CancellationToken cancellationToken, string idHash, InvokerContext invokerContext) public async Task<IActionResult> Get(CancellationToken cancellationToken, string idHash,
InvokerContext invokerContext)
{ {
if (!_hashids.TryDecodeSingleLong(idHash, out var id)) if (!_hashids.TryDecodeSingleLong(idHash, out var id))
{ {
return BadRequest(); return BadRequest();
} }
var tenant = await _context.Tenants.SingleOrDefaultAsync(x => x.Id == id, cancellationToken); var tenant = await _context.Tenants.SingleOrDefaultAsync(x => x.Id == id, cancellationToken);
if (tenant is null) if (tenant is null)
{ {
return NotFound(); return NotFound();
} }
var userHasRelation = await _context.TenantSubjectRelations.AnyAsync(x => x.SubjectId == invokerContext.Invoker!.SubjectId && x.TenantId == id, cancellationToken: cancellationToken); var userHasRelation = await _context.TenantSubjectRelations.AnyAsync(
x => x.SubjectId == invokerContext.Invoker!.SubjectId && x.TenantId == id,
cancellationToken: cancellationToken);
if (!userHasRelation) if (!userHasRelation)
{ {
@ -73,7 +78,47 @@ public class TenantController : ControllerBase
OwnerId = _hashids.EncodeLong(tenant.OwnerId), OwnerId = _hashids.EncodeLong(tenant.OwnerId),
Visibility = tenant.Visibility Visibility = tenant.Visibility
}; };
return Ok(dto); return Ok(dto);
} }
[HttpGet("{tenantIdHash}/apps")]
public async Task<IActionResult> GetTenantApps(InvokerContext invokerContext, string tenantIdHash,
CancellationToken cancellationToken)
{
if (!_hashids.TryDecodeSingleLong(tenantIdHash, out var tenantId))
{
return BadRequest();
}
var tenantExists = await _context.Tenants.AnyAsync(x => x.Id == tenantId, cancellationToken);
if (!tenantExists)
{
return NotFound();
}
var relationExists = await _context.TenantSubjectRelations
.AnyAsync(x => x.SubjectId == invokerContext.Invoker!.SubjectId, cancellationToken);
if (!relationExists)
{
return Forbid();
}
var apps = await _context.AppSubjectRelations
.Where(x => x.SubjectId == invokerContext.Invoker!.SubjectId)
.Where(x => x.App!.TenantId == tenantId)
.Select(x => x.App!)
.ToListAsync(cancellationToken);
var dtos = apps.Select(x => new AppListDto
{
Id = _hashids.EncodeLong(x.Id!.Value),
Name = x.Name,
Visibility = x.Visibility
});
return Ok(dtos);
}
} }

View File

@ -0,0 +1,10 @@
using Suspectus.Gandalf.Palantir.Data.Entities.Base;
namespace Suspectus.Gandalf.Palantir.Data.Dto.App;
public class AppListDto
{
public required string Id { get; set; }
public required string Name { get; set; }
public required EntityVisibility Visibility { get; set; }
}