src/app/features/api-client/api-client-list/api-client-list.component.ts
Admin page for account managers to create and manage the credentials ("API clients") of external systems that access this system's data through the API.
| changeDetection | ChangeDetectionStrategy.OnPush |
||
| selector | app-api-client-list |
||
| standalone | true |
||
| imports |
MatTableModule
MatButtonModule
MatTooltipModule
MatProgressBarModule
CustomDatePipe
|
||
| styleUrls | ./api-client-list.component.scss |
||
| templateVariables |
|
||
| templateUrl | ./api-client-list.component.html |
||
| styleUrl | ./api-client-list.component.scss |
No results matching.
ViewTitleComponent
HintBoxComponent
MatTableModule
MatButtonModule
MatTooltipModule
MatProgressBarModule
FaIconComponent
CustomDatePipe
Properties |
|
Methods |
|
| Async create |
create()
|
|
Returns :
any
|
| Async delete | ||||||
delete(apiClient: ApiClient)
|
||||||
|
Parameters :
Returns :
any
|
| Async editScopes | ||||||
editScopes(apiClient: ApiClient)
|
||||||
|
Parameters :
Returns :
any
|
| Async regenerateSecret | ||||||
regenerateSecret(apiClient: ApiClient)
|
||||||
|
Parameters :
Returns :
any
|
| Readonly displayedColumns |
Type : []
|
Default value : [
"name",
"clientId",
"scopes",
"created",
"actions",
]
|
| Readonly isAvailable |
Type : unknown
|
Default value : this.apiClientService.isAvailable
|
| Readonly isCheckingAvailability |
Type : unknown
|
Default value : this.apiClientService.isCheckingAvailability
|
| Readonly loadingError |
Type : unknown
|
Default value : computed(() =>
this.apiClients.error()
? errorMessageOf(this.apiClients.error())
: undefined,
)
|
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
resource,
} from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { firstValueFrom } from "rxjs";
import { MatButtonModule } from "@angular/material/button";
import { MatTableModule } from "@angular/material/table";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { ViewTitleComponent } from "#src/app/core/common-components/view-title/view-title.component";
import { HintBoxComponent } from "#src/app/core/common-components/hint-box/hint-box.component";
import { ConfirmationDialogService } from "#src/app/core/common-components/confirmation-dialog/confirmation-dialog.service";
import { AlertService } from "#src/app/core/alerts/alert.service";
import { CustomDatePipe } from "#src/app/core/basic-datatypes/date/custom-date.pipe";
import {
ApiClient,
ApiClientCredentials,
ApiClientService,
errorMessageOf,
logRequestError,
} from "../api-client.service";
import { getScopeLabel } from "../api-client-scopes";
import { CreateApiClientDialogComponent } from "../create-api-client-dialog/create-api-client-dialog.component";
import { EditApiClientScopesDialogComponent } from "../edit-api-client-scopes-dialog/edit-api-client-scopes-dialog.component";
import {
ApiClientCredentialsDialogComponent,
ApiClientCredentialsDialogData,
} from "../api-client-credentials-dialog/api-client-credentials-dialog.component";
/**
* Admin page for account managers to create and manage the credentials ("API clients")
* of external systems that access this system's data through the API.
*/
@Component({
selector: "app-api-client-list",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ViewTitleComponent,
HintBoxComponent,
MatTableModule,
MatButtonModule,
MatTooltipModule,
MatProgressBarModule,
FaIconComponent,
CustomDatePipe,
],
templateUrl: "./api-client-list.component.html",
styleUrl: "./api-client-list.component.scss",
})
export class ApiClientListComponent {
private readonly apiClientService = inject(ApiClientService);
private readonly dialog = inject(MatDialog);
private readonly confirmationDialog = inject(ConfirmationDialogService);
private readonly alertService = inject(AlertService);
readonly isCheckingAvailability =
this.apiClientService.isCheckingAvailability;
readonly isAvailable = this.apiClientService.isAvailable;
/** the API clients, loaded once the backend is known to offer them */
readonly apiClients = resource({
params: () => (this.isAvailable() ? {} : undefined),
loader: async () =>
(await this.apiClientService.getApiClients()).map((apiClient) => ({
...apiClient,
scopeLabels: apiClient.scopes.map((scope) => getScopeLabel(scope)),
})),
});
readonly loadingError = computed(() =>
this.apiClients.error()
? errorMessageOf(this.apiClients.error())
: undefined,
);
readonly displayedColumns = [
"name",
"clientId",
"scopes",
"created",
"actions",
];
async create() {
const dialogRef = this.dialog.open<
CreateApiClientDialogComponent,
unknown,
ApiClientCredentialsDialogData
>(CreateApiClientDialogComponent, { width: "600px" });
const result = await firstValueFrom(dialogRef.afterClosed());
if (result) {
this.showCredentials(result);
this.apiClients.reload();
}
}
async editScopes(apiClient: ApiClient) {
const dialogRef = this.dialog.open<
EditApiClientScopesDialogComponent,
ApiClient,
ApiClient
>(EditApiClientScopesDialogComponent, { data: apiClient, width: "600px" });
if (await firstValueFrom(dialogRef.afterClosed())) {
this.apiClients.reload();
}
}
async regenerateSecret(apiClient: ApiClient) {
const confirmed = await this.confirmationDialog.getConfirmation(
$localize`Generate new secret?`,
$localize`The external system "${apiClient.name}" cannot access the API with its current secret anymore, until you give it the new one.`,
);
if (!confirmed) {
return;
}
let credentials: ApiClientCredentials;
try {
credentials = await this.apiClientService.regenerateSecret(
apiClient.clientId,
);
} catch (err) {
logRequestError("Could not regenerate the secret of an API client", err);
this.alertService.addDanger(errorMessageOf(err));
return;
}
this.showCredentials({ credentials, scopes: apiClient.scopes });
}
async delete(apiClient: ApiClient) {
const confirmed = await this.confirmationDialog.getConfirmation(
$localize`Delete API client?`,
$localize`The external system "${apiClient.name}" cannot access the API anymore. This cannot be undone.`,
);
if (!confirmed) {
return;
}
try {
await this.apiClientService.deleteApiClient(apiClient.clientId);
} catch (err) {
logRequestError("Could not delete an API client", err);
this.alertService.addDanger(errorMessageOf(err));
}
this.apiClients.reload();
}
private showCredentials(data: ApiClientCredentialsDialogData) {
this.dialog.open(ApiClientCredentialsDialogComponent, {
data,
width: "600px",
// the secret is not shown again, so it must not get lost by an accidental click outside
disableClose: true,
});
}
}
<div class="flex-row flex-wrap">
<div class="flex-grow">
<app-view-title i18n>API Access</app-view-title>
</div>
@if (isAvailable()) {
<button
mat-stroked-button
color="accent"
class="standard-add-button"
(click)="create()"
>
<fa-icon icon="plus-circle" class="standard-icon-with-text"></fa-icon>
<span i18n>New API Client</span>
</button>
}
</div>
<p i18n class="margin-bottom-regular">
External systems can access data of this system through the API, e.g. to
import report results into another platform or to let their users sign in
here. Each external system gets its own credentials ("API client"), which you
can create and revoke here.
</p>
@if (isCheckingAvailability()) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
} @else if (!isAvailable()) {
<app-hint-box>
<span i18n>
API access is not available for this system. Please contact your technical
support team to enable the API integration.
</span>
</app-hint-box>
} @else if (loadingError(); as loadingError) {
<p class="color-error" role="alert">{{ loadingError }}</p>
} @else if (apiClients.hasValue()) {
@let clients = apiClients.value();
@if (clients.length === 0) {
<p i18n>There are no API clients yet.</p>
} @else {
<div class="table-container">
<table mat-table [dataSource]="clients" class="full-width table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef i18n>Name</th>
<td mat-cell *matCellDef="let apiClient">{{ apiClient.name }}</td>
</ng-container>
<ng-container matColumnDef="clientId">
<th mat-header-cell *matHeaderCellDef i18n>Client ID</th>
<td mat-cell *matCellDef="let apiClient">
<code>{{ apiClient.clientId }}</code>
</td>
</ng-container>
<ng-container matColumnDef="scopes">
<th mat-header-cell *matHeaderCellDef i18n>Access</th>
<td mat-cell *matCellDef="let apiClient">
@for (label of apiClient.scopeLabels; track label) {
<div>{{ label }}</div>
}
</td>
</ng-container>
<ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef i18n>Created</th>
<td mat-cell *matCellDef="let apiClient">
{{ apiClient.createdAt | customDate: "mediumDate" }}
@if (apiClient.createdBy) {
<div class="hint-text">{{ apiClient.createdBy }}</div>
}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let apiClient" class="text-align-end">
<button
mat-icon-button
(click)="editScopes(apiClient)"
matTooltip="Change access"
i18n-matTooltip
aria-label="Change access"
i18n-aria-label
>
<fa-icon icon="pen"></fa-icon>
</button>
<button
mat-icon-button
(click)="regenerateSecret(apiClient)"
matTooltip="Generate new secret"
i18n-matTooltip
aria-label="Generate new secret"
i18n-aria-label
>
<fa-icon icon="key"></fa-icon>
</button>
<button
mat-icon-button
(click)="delete(apiClient)"
matTooltip="Delete"
i18n-matTooltip
aria-label="Delete"
i18n-aria-label
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
<div class="margin-top-regular">
<span class="hint-text" i18n>
Only API clients created on this page are listed here. Integrations that
your technical support team set up for you are managed by them.
</span>
</div>
} @else {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
}
./api-client-list.component.scss
@use "mixins/list-table";
// same table styling as the other admin lists (user accounts, roles)
@include list-table.container-and-table;