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
36 changes: 35 additions & 1 deletion src/Turnierplan.App/Client/src/app/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,21 +945,55 @@ export const de = {
InheritedTooltip: 'Diese Rollenzuweisung existiert implizit aufgrund der Zugehörigkeit zu einer anderen Resource',
ScopeType: {
Folder: {
Tooltip: 'Ordner',
NotInherited: 'Zuweisung liegt auf diesem Ordner'
},
Organization: {
Tooltip: 'Organisation',
NotInherited: 'Zuweisung liegt auf dieser Organisation'
},
Tournament: {
Tooltip: 'Turnier',
NotInherited: 'Zuweisung liegt auf diesem Turnier'
},
Venue: {
Tooltip: 'Spielstätte',
NotInherited: 'Zuweisung liegt auf dieser Spielstätte'
}
},
SuccessToast: {
DeletedSuccessToast: {
Title: 'Rollenzuweisung gelöscht',
Message: 'Die Rollenzuweisung wurde erfolgreich gelöscht.'
},
AddRoleAssignment: {
Title: 'Rollenzuweisung hinzufügen',
PreviousStep: 'Zurück',
NextStep: 'Weiter',
StepTitle: {
SelectRole: 'Rolle selektieren',
SelectPrincipal: 'Prinzipal selektieren'
},
AddAssignmentInfo:
'Suchen Sie den gewünschten Nutzer oder API-Schlüssel anhand von E-Mailadresse bzw. API-Schlüssel-ID. Durch Klick auf "Suchen & hinzufügen" wird der spezifizierte Prinzipal gesucht und - sofern gefunden - eine entsprechende Rollenzuweisung erstellt.',
SelectedRole: 'Gewählte Rolle:',
SearchPrincipalPlaceholder: {
ApiKey: 'ID des API-Schlüssels eingeben',
User: 'E-Mailadresse des Nutzers eingeben'
},
SearchPrincipalButton: 'Suchen & hinzufügen',
CreatingRoleAssignment: 'Die Rollenzuweisung wird erstellt',
CreateSuccessToast: {
Title: 'Rollenzuweisung erstellt',
Message: 'Die Rollenzuweisung wurde erfolgreich erstellt.'
},
PrincipalNotFoundToast: {
Title: 'Rollenzuweisung konnte nicht erstellt werden',
Message: 'Es existiert kein Prinzipal mit der angegebenen Api-Key-ID bzw. E-Mailadresse.'
},
AssignmentAlreadyExists: {
Title: 'Rollenzuweisung konnte nicht erstellt werden',
Message: 'Die angegebene Rolle ist bereits für den spezifizierten Prinzipal zugewiesen.'
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<div class="modal-header">
<h4 class="modal-title" translate="Portal.RbacManagement.AddRoleAssignment.Title"></h4>
<button type="button" class="btn-close" (click)="modal.dismiss()"></button>
</div>
<div class="modal-body">
<div class="text-secondary small mb-4">
<span
[translate]="'Portal.RbacManagement.AddRoleAssignment.StepTitle.SelectRole'"
[ngClass]="{ 'text-decoration-underline': step === 'SelectRole' }"></span>
<span> &middot; </span>
<span
[translate]="'Portal.RbacManagement.AddRoleAssignment.StepTitle.SelectPrincipal'"
[ngClass]="{ 'text-decoration-underline': step === 'SelectPrincipal' }"></span>
</div>

@switch (step) {
@case ('SelectRole') {
<table class="table table-bordered">
<tbody>
@for (role of availableRoles; track role) {
<tr>
<td>
<div
[ngClass]="{ 'fw-bold text-decoration-underline': role === selectedRole }"
[translate]="'Portal.RbacManagement.RoleName.' + role"></div>
<div class="small text-secondary" [translate]="'Portal.RbacManagement.RoleDescription.' + role"></div>
</td>
<td style="width: 1px" class="align-middle">
<tp-action-button [mode]="'IconOnly'" [icon]="'arrow-right-circle'" (click)="selectRole(role)" />
</td>
</tr>
}
</tbody>
</table>
}
@case ('SelectPrincipal') {
<div class="mb-3" translate="Portal.RbacManagement.AddRoleAssignment.AddAssignmentInfo"></div>
<div class="mb-3">
<span translate="Portal.RbacManagement.AddRoleAssignment.SelectedRole"></span>
<span class="ms-2 fw-bold" [translate]="'Portal.RbacManagement.RoleName.' + selectedRole"></span>
</div>
<div class="d-flex flex-row align-items-center gap-2">
<div class="btn-group" role="group">
<input
type="radio"
class="btn-check"
name="addAssignment_principalKind"
id="addAssignment_principalKind_user"
[value]="PrincipalKind.User"
[(ngModel)]="selectedPrincipalKind" />
<label
class="btn btn-sm btn-outline-secondary"
for="addAssignment_principalKind_user"
[ngbTooltip]="'Portal.RbacManagement.PrincipalKind.User' | translate">
<i class="bi bi-person"></i>
</label>
<input
type="radio"
class="btn-check"
name="addAssignment_principalKind"
id="addAssignment_principalKind_apiKey"
[value]="PrincipalKind.ApiKey"
[(ngModel)]="selectedPrincipalKind" />
<label
class="btn btn-sm btn-outline-secondary"
for="addAssignment_principalKind_apiKey"
[ngbTooltip]="'Portal.RbacManagement.PrincipalKind.ApiKey' | translate">
<i class="bi bi-key"></i>
</label>
</div>

<input
type="text"
[placeholder]="'Portal.RbacManagement.AddRoleAssignment.SearchPrincipalPlaceholder.' + selectedPrincipalKind | translate"
[(ngModel)]="searchPrincipalInput"
class="flex-grow-1 form-control form-control-sm" />

@let createDisabled = isCreatingRoleAssignment || searchPrincipalInput.length === 0;
<div [ngClass]="{ 'tp-cursor-not-allowed': createDisabled }">
<tp-action-button
[icon]="'search'"
[title]="'Portal.RbacManagement.AddRoleAssignment.SearchPrincipalButton'"
[disabled]="createDisabled"
(click)="addRoleAssignment()" />
</div>
</div>

@if (isCreatingRoleAssignment) {
<div class="mt-3 d-flex flex-row gap-2">
<tp-small-spinner />
<span translate="Portal.RbacManagement.AddRoleAssignment.CreatingRoleAssignment"></span>
</div>
}
}
}
</div>
<div class="modal-footer">
@if (step === 'SelectRole') {
<tp-action-button [type]="'outline-secondary'" [title]="'Portal.General.Cancel'" (click)="modal.dismiss()" />
} @else {
<tp-action-button
[type]="'outline-dark'"
[title]="'Portal.RbacManagement.AddRoleAssignment.PreviousStep'"
[icon]="'arrow-left-circle'"
(click)="previousStep()" />
}
<span class="flex-grow-1"></span>
@if (step === 'SelectRole') {
@let nextDisabled = selectedRole === undefined;
<div [ngClass]="{ 'tp-cursor-not-allowed': nextDisabled }">
<tp-action-button
[type]="'outline-dark'"
[title]="'Portal.RbacManagement.AddRoleAssignment.NextStep'"
[icon]="'arrow-right-circle'"
[disabled]="nextDisabled"
(click)="nextStep()" />
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.modal-body {
min-height: 20em;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Component, OnDestroy } from '@angular/core';
import { CreateRoleAssignmentEndpointRequest, PrincipalKind, Role, RoleAssignmentsService } from '../../../api';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { finalize, Observable, Subject } from 'rxjs';
import { NotificationService } from '../../../core/services/notification.service';

type Step = 'SelectRole' | 'SelectPrincipal';

@Component({
standalone: false,
templateUrl: './rbac-add-assignment.component.html',
styleUrl: './rbac-add-assignment.component.scss'
})
export class RbacAddAssignmentComponent implements OnDestroy {
protected readonly PrincipalKind = PrincipalKind;
protected readonly availableRoles = Object.keys(Role) as Role[];

protected step: Step = 'SelectRole';
protected selectedRole?: Role = undefined;
protected selectedPrincipalKind: PrincipalKind = PrincipalKind.User;
protected searchPrincipalInput: string = '';
protected isCreatingRoleAssignment = false;

private readonly errorSubject$ = new Subject<unknown>();
private targetScopeId: string = '';

constructor(
protected readonly modal: NgbActiveModal,
private readonly roleAssignmentsService: RoleAssignmentsService,
private readonly notificationService: NotificationService
) {}

public set scopeId(value: string) {
this.targetScopeId = value;
}

public get error$(): Observable<unknown> {
return this.errorSubject$.asObservable();
}

public ngOnDestroy(): void {
this.errorSubject$.complete();
}

protected previousStep(): void {
if (this.step === 'SelectPrincipal') {
this.step = 'SelectRole';
}
}

protected nextStep(): void {
if (this.step === 'SelectRole' && this.selectedRole !== undefined) {
this.step = 'SelectPrincipal';
}
}

protected selectRole(role: Role): void {
this.selectedRole = role;
this.step = 'SelectPrincipal';
}

protected addRoleAssignment(): void {
if (this.isCreatingRoleAssignment || this.selectedRole === undefined || this.searchPrincipalInput.trim().length === 0) {
return;
}

this.isCreatingRoleAssignment = true;

const request: CreateRoleAssignmentEndpointRequest = {
scopeId: this.targetScopeId,
role: this.selectedRole,
description: null, // TODO
apiKeyId: this.selectedPrincipalKind === PrincipalKind.ApiKey ? this.searchPrincipalInput.trim() : null,
userEmail: this.selectedPrincipalKind === PrincipalKind.User ? this.searchPrincipalInput.trim() : null
};

this.roleAssignmentsService
.createRoleAssignment({ body: request })
.pipe(finalize(() => (this.isCreatingRoleAssignment = false)))
.subscribe({
next: () => {
this.notificationService.showNotification(
'success',
'Portal.RbacManagement.AddRoleAssignment.CreateSuccessToast.Title',
'Portal.RbacManagement.AddRoleAssignment.CreateSuccessToast.Message'
);
this.modal.close();
},
error: (error: { status?: number }) => {
if (error.status === 400) {
this.notificationService.showNotification(
'error',
'Portal.RbacManagement.AddRoleAssignment.PrincipalNotFoundToast.Title',
'Portal.RbacManagement.AddRoleAssignment.PrincipalNotFoundToast.Message'
);
} else if (error.status === 409) {
this.notificationService.showNotification(
'error',
'Portal.RbacManagement.AddRoleAssignment.AssignmentAlreadyExists.Title',
'Portal.RbacManagement.AddRoleAssignment.AssignmentAlreadyExists.Message'
);
} else {
this.errorSubject$.next(error);
}
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
</div>
} @else {
<div class="mb-4 d-flex flex-row align-items-center">
<i class="bi bi-{{ targetIcon }} me-2"></i>
<i class="bi bi-{{ targetIcon }} me-2" [ngbTooltip]="scopeTranslationKey + '.Tooltip' | translate"></i>
<span class="fw-bold">
{{ target.name }}
</span>
<span class="flex-grow-1"></span>
<tp-action-button [title]="'Portal.RbacManagement.NewRoleAssignment'" [type]="'outline-success'" [icon]="'plus-circle'" />
<tp-action-button
[title]="'Portal.RbacManagement.NewRoleAssignment'"
[type]="'outline-success'"
[icon]="'plus-circle'"
(click)="showAddRoleAssignmentDialog()" />
</div>

<table class="mb-2 table table-bordered">
Expand All @@ -30,16 +34,7 @@
<td>
<div class="d-flex flex-column gap-2">
<div class="d-flex gap-2">
@switch (assignment.principal.kind) {
@case (PrincipalKind.ApiKey) {
<i class="bi bi-key" [ngbTooltip]="'Portal.RbacManagement.PrincipalKind.ApiKey' | translate"></i>
}
@case (PrincipalKind.User) {
<i class="bi bi-person" [ngbTooltip]="'Portal.RbacManagement.PrincipalKind.User' | translate"></i>
}
}
<!-- TODO: Resolve ApiKey/User name -->
<span>{{ assignment.principal.principalId }}</span>
<tp-rbac-principal [principal]="assignment.principal" />
@if (canDeleteAssignment(assignment)) {
<span class="flex-grow-1"></span>
<tp-delete-button [reducedFootprint]="true" (confirmed)="removeRoleAssignment(assignment.id)" />
Expand Down
Loading
Loading