src/app/core/admin/admin-overview/admin-overview.component.ts
Admin GUI giving administrative users different options/actions.
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-admin-overview |
| standalone | true |
| imports |
MatButtonModule
MatListModule
MatExpansionModule
MatIconModule
MatTooltipModule
|
| styleUrls | ./admin-overview.component.scss |
| templateUrl | ./admin-overview.component.html |
No results matching.
MatButtonModule
RouterLink
MatListModule
MatExpansionModule
MatIconModule
MatTooltipModule
WarningNotOptimizedForSmallScreenComponent
Properties |
|
Methods |
|
constructor()
|
| debugDatabase |
debugDatabase()
|
|
Send a reference of the PouchDB to the browser's developer console for real-time debugging.
Returns :
void
|
| Async downloadConfigClick |
downloadConfigClick()
|
|
Returns :
any
|
| editConfig |
editConfig()
|
|
Returns :
void
|
| Async emptyRecords |
emptyRecords()
|
|
Returns :
any
|
| isExpanded | ||||||
isExpanded(sectionId: string)
|
||||||
|
Parameters :
Returns :
boolean
|
| Async loadBackup | ||||||||
loadBackup(inputEvent: Event)
|
||||||||
|
Reset the database to the state from the loaded backup file.
Parameters :
Returns :
any
|
| Async resetLocalDevice |
resetLocalDevice()
|
|
Returns :
any
|
| Async resetSystem |
resetSystem()
|
|
Returns :
any
|
| Async saveBackup |
saveBackup()
|
|
Download a full backup of the database as (json) file.
Returns :
any
|
| Async saveCsvExport |
saveCsvExport()
|
|
Download a full export of the database as csv file.
Returns :
any
|
| setExpanded | ||||||
setExpanded(sectionId: string)
|
||||||
|
Parameters :
Returns :
void
|
| Async uploadConfigFile | ||||||
uploadConfigFile(inputEvent: Event)
|
||||||
|
Parameters :
Returns :
any
|
| Protected adminOverviewService |
Type : unknown
|
Default value : inject(AdminOverviewService)
|
| Public configurationMenuItems |
Type : MenuItem[]
|
Default value : []
|
| expandedSection |
Type : unknown
|
Default value : computed(() => this.sectionStateService.getExpanded())
|
| isSaasEnvironment |
Type : boolean
|
| isUploadingConfig |
Type : unknown
|
Default value : signal(false)
|
| Public templates |
Type : MenuItem[]
|
Default value : []
|
import {
Component,
inject,
computed,
signal,
ChangeDetectionStrategy,
} from "@angular/core";
import { AdminSectionStateService } from "./admin-section-state.service";
import { BackupService } from "../backup/backup.service";
import { SystemResetService } from "../system-reset/system-reset.service";
import { LocalDeviceResetService } from "../../database/local-device-reset.service";
import { ConfirmationDialogService } from "../../common-components/confirmation-dialog/confirmation-dialog.service";
import { MatSnackBar } from "@angular/material/snack-bar";
import { ConfigService } from "../../config/config.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { readFile } from "../../../utils/utils";
import { DatabaseResolverService } from "../../database/database-resolver.service";
import { MatButtonModule } from "@angular/material/button";
import { RouterLink } from "@angular/router";
import { DownloadService } from "../../export/download-service/download.service";
import { MatListModule } from "@angular/material/list";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatIconModule } from "@angular/material/icon";
import { MatTooltipModule } from "@angular/material/tooltip";
import { RouteTarget } from "../../../route-target";
import { MenuItem } from "../../ui/navigation/menu-item";
import { JsonEditorService } from "#src/app/core/admin/json-editor/json-editor.service";
import { EntityMapperService } from "#src/app/core/entity/entity-mapper/entity-mapper.service";
import { Config } from "#src/app/core/config/config";
import { environment } from "#src/environments/environment";
import moment from "moment";
import { AdminOverviewService } from "./admin-overview.service";
import { WarningNotOptimizedForSmallScreenComponent } from "#src/app/core/common-components/warning-not-optimized-for-small-screen/warning-not-optimized-for-small-screen.component";
import { Logging } from "#src/app/core/logging/logging.service";
/**
* Admin GUI giving administrative users different options/actions.
*/
@UntilDestroy()
@RouteTarget("Admin")
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-admin-overview",
templateUrl: "./admin-overview.component.html",
styleUrls: ["./admin-overview.component.scss"],
imports: [
MatButtonModule,
RouterLink,
MatListModule,
MatExpansionModule,
MatIconModule,
MatTooltipModule,
WarningNotOptimizedForSmallScreenComponent,
],
})
export class AdminOverviewComponent {
private backupService = inject(BackupService);
private readonly systemResetService = inject(SystemResetService);
private readonly localDeviceResetService = inject(LocalDeviceResetService);
private downloadService = inject(DownloadService);
private dbResolver = inject(DatabaseResolverService);
private confirmationDialog = inject(ConfirmationDialogService);
private snackBar = inject(MatSnackBar);
private configService = inject(ConfigService);
protected adminOverviewService = inject(AdminOverviewService);
private jsonEditorService = inject(JsonEditorService);
private entityMapper = inject(EntityMapperService);
private readonly sectionStateService = inject(AdminSectionStateService);
public templates: MenuItem[] = [];
public configurationMenuItems: MenuItem[] = [];
expandedSection = computed(() => this.sectionStateService.getExpanded());
isUploadingConfig = signal(false);
isSaasEnvironment: boolean;
constructor() {
this.templates = this.adminOverviewService.templates;
this.configurationMenuItems =
this.adminOverviewService.configurationMenuItems;
this.isSaasEnvironment = environment.SaaS === true;
}
setExpanded(sectionId: string) {
this.sectionStateService.setExpanded(sectionId);
}
isExpanded(sectionId: string): boolean {
return this.expandedSection() === sectionId;
}
/**
* Send a reference of the PouchDB to the browser's developer console for real-time debugging.
*/
debugDatabase() {
console.log(
'You can assign the following to a global variable in the browser console (right click > "Store as global variable") to run actions on the database with your scripts here:',
);
console.log("DatabaseResolverService", this.dbResolver);
console.log('"app" database', this.dbResolver.getDatabase());
}
/**
* Download a full backup of the database as (json) file.
*/
async saveBackup() {
const backup = await this.backupService.getDatabaseExport();
await this.downloadService.triggerDownload(backup, "json", "backup");
}
/**
* Download a full export of the database as csv file.
*/
async saveCsvExport() {
const backup = await this.backupService.getDatabaseExport();
await this.downloadService.triggerDownload(backup, "csv", "export");
}
async downloadConfigClick() {
const configString = this.configService.exportConfig();
await this.downloadService.triggerDownload(configString, "json", "config");
}
async uploadConfigFile(inputEvent: Event) {
this.isUploadingConfig.set(true);
try {
const loadedFile = await readFile(this.getFileFromInputEvent(inputEvent));
const parsed = JSON.parse(loadedFile);
if (Array.isArray(parsed)) {
const entities = parsed
.map((doc) => {
try {
return this.entityMapper.entityFromRawDoc(doc);
} catch (e) {
Logging.warn(
"Skipping unknown entity type during config upload",
doc["_id"],
e,
);
return null;
}
})
.filter(Boolean);
await this.entityMapper.saveAll(entities, true);
} else {
await this.configService.saveConfig(parsed);
}
this.snackBar.open($localize`Configuration updated`, undefined, {
duration: 3000,
});
} catch (e) {
Logging.error("Failed to upload configuration", e);
this.snackBar.open($localize`Upload failed: ${e.message}`, undefined, {
duration: 5000,
});
} finally {
this.isUploadingConfig.set(false);
}
}
editConfig() {
const originalData = this.configService.exportConfig(true);
this.jsonEditorService
.openJsonEditorDialog(originalData)
.subscribe(async (updatedData) => {
if (!updatedData) return;
const previousConfigBackup = new Config(
Config.CONFIG_KEY + ":" + moment().format("YYYY-MM-DD_HH-mm-ss"),
originalData,
);
try {
await this.entityMapper.save(previousConfigBackup);
await this.configService.saveConfig(updatedData);
this.showConfirmationWithUndoOption(async () => {
await this.configService.saveConfig(originalData);
await this.entityMapper.remove(previousConfigBackup);
});
} catch (e) {
Logging.error("Failed to save configuration changes", e);
this.snackBar.open(
$localize`Update failed: ${e.message}`,
undefined,
{
duration: 5000,
},
);
}
});
}
/**
* Show a snack bar with undo option and handle undo callback with progress dialog.
*/
private showConfirmationWithUndoOption(undoAction: () => Promise<void>) {
const snackBarRef = this.snackBar.open(
$localize`Configuration updated`,
$localize`Undo`,
{ duration: 8000 },
);
snackBarRef.onAction().subscribe(async () => {
const progressRef = this.confirmationDialog.showProgressDialog(
signal($localize`Reverting configuration changes ...`),
);
try {
await undoAction();
} catch (e) {
Logging.error("Failed to revert configuration changes", e);
this.snackBar.open($localize`Revert failed: ${e.message}`, undefined, {
duration: 5000,
});
} finally {
progressRef.close();
}
});
}
/**
* Reset the database to the state from the loaded backup file.
* @param inputEvent for the input where a file has been selected
*/
async loadBackup(inputEvent: Event) {
const restorePoint = await this.backupService.getDatabaseExport();
const dataToBeRestored = JSON.parse(
await readFile(this.getFileFromInputEvent(inputEvent)),
);
const confirmed = await this.confirmationDialog.getConfirmation(
`Overwrite complete database?`,
`Are you sure you want to restore this backup? This will
delete all ${restorePoint.length} existing records,
restoring ${dataToBeRestored.length} records from the loaded file.`,
);
if (!confirmed) {
return;
}
await this.backupService.clearDatabase();
await this.backupService.restoreData(dataToBeRestored, true);
const snackBarRef = this.snackBar.open(`Backup restored`, "Undo", {
duration: 8000,
});
snackBarRef
.onAction()
.pipe(untilDestroyed(this))
.subscribe(async () => {
await this.backupService.clearDatabase();
await this.backupService.restoreData(restorePoint, true);
});
}
private getFileFromInputEvent(inputEvent: Event): Blob {
const target = inputEvent.target as HTMLInputElement;
return target.files[0];
}
async emptyRecords() {
await this.systemResetService.emptyRecords();
}
async resetSystem() {
await this.systemResetService.resetSystem();
}
async resetLocalDevice() {
await this.localDeviceResetService.resetLocalDevice();
}
}
<h1 i18n>System Settings</h1>
<app-warning-not-optimized-for-small-screen />
<mat-accordion class="admin-sections margin-top-large" multi>
<!-- Subscription Section -->
@if (isSaasEnvironment) {
<mat-expansion-panel
[expanded]="isExpanded('subscription')"
(opened)="setExpanded('subscription')"
>
<mat-expansion-panel-header>
<mat-panel-title i18n>Subscription</mat-panel-title>
<mat-panel-description i18n
>Manage your subscription details, privacy settings, and access to
advanced features.</mat-panel-description
>
</mat-expansion-panel-header>
<mat-nav-list class="section-nav-list">
<mat-list-item [routerLink]="['/admin/subscription-info']">
<span matListItemTitle i18n>Subscription Info</span>
</mat-list-item>
<mat-list-item [routerLink]="['/admin/advanced-features']">
<span matListItemTitle i18n>Advanced Features</span>
</mat-list-item>
<mat-list-item [routerLink]="['/admin/data-privacy']">
<span matListItemTitle i18n>Data Privacy</span>
</mat-list-item>
</mat-nav-list>
</mat-expansion-panel>
}
<!-- Configuration and Site Wide Settings Section -->
<mat-expansion-panel
[expanded]="isExpanded('config')"
(opened)="setExpanded('config')"
>
<mat-expansion-panel-header>
<mat-panel-title i18n
>Configuration and Site Wide Settings</mat-panel-title
>
<mat-panel-description i18n
>Set up the essential elements that shape your site's structure and
system behaviour.</mat-panel-description
>
</mat-expansion-panel-header>
<mat-nav-list class="section-nav-list">
@for (item of configurationMenuItems; track item.label) {
@if (item.link) {
<mat-list-item
[routerLink]="[item.link]"
[matTooltip]="item.subtitle ?? ''"
[matTooltipDisabled]="!item.subtitle"
matTooltipPosition="before"
>
<span matListItemTitle>{{ item.label }}</span>
</mat-list-item>
}
}
</mat-nav-list>
</mat-expansion-panel>
<!-- Templates and Forms Section (items dynamic) -->
<mat-expansion-panel
[expanded]="isExpanded('templates')"
(opened)="setExpanded('templates')"
>
<mat-expansion-panel-header>
<mat-panel-title i18n>Templates and Forms</mat-panel-title>
<mat-panel-description i18n
>Customise your templates and forms.</mat-panel-description
>
</mat-expansion-panel-header>
@if (templates.length > 0) {
<mat-nav-list class="section-nav-list">
@for (item of templates; track item.label) {
@if (item.link) {
<mat-list-item
[routerLink]="[item.link]"
[matTooltip]="item.subtitle ?? ''"
[matTooltipDisabled]="!item.subtitle"
matTooltipPosition="before"
>
<span matListItemTitle>{{ item.label }}</span>
</mat-list-item>
}
}
</mat-nav-list>
}
</mat-expansion-panel>
<!-- User Management Section -->
<mat-expansion-panel
[expanded]="isExpanded('user-management')"
(opened)="setExpanded('user-management')"
>
<mat-expansion-panel-header>
<mat-panel-title i18n>User Management</mat-panel-title>
<mat-panel-description i18n
>Set up and manage user accounts, roles, and
permissions.</mat-panel-description
>
</mat-expansion-panel-header>
<mat-nav-list class="section-nav-list">
<mat-list-item
[routerLink]="['/admin/user-list']"
matTooltip="Create, edit and manage user accounts."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>User Accounts</span>
</mat-list-item>
<mat-list-item
[routerLink]="['/admin/user-roles']"
matTooltip="Set roles and manage role-based access."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>User Roles & Permissions</span>
</mat-list-item>
</mat-nav-list>
</mat-expansion-panel>
<!-- Export and Backups Section (special content) -->
<mat-expansion-panel
[expanded]="isExpanded('export')"
(opened)="setExpanded('export')"
>
<mat-expansion-panel-header>
<mat-panel-title i18n>Export and Backups</mat-panel-title>
<mat-panel-description i18n
>Define how your system keeps data protected and
accessible.</mat-panel-description
>
</mat-expansion-panel-header>
<mat-nav-list class="section-nav-list">
<mat-list-item
(click)="saveBackup()"
button
matTooltip="Download all data of the database."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>Download Backup (.json)</span>
</mat-list-item>
<mat-list-item
(click)="backupImport.click()"
button
matTooltip="Upload a previous backup to restore the database to that state."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>Restore Backup (.json)</span>
</mat-list-item>
<mat-list-item
(click)="saveCsvExport()"
button
matTooltip="Download all data of the database in a format that can be opened as a spreadsheet."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>Full Database Export (.csv)</span>
</mat-list-item>
</mat-nav-list>
<input #backupImport type="file" hidden (change)="loadBackup($event)" />
</mat-expansion-panel>
<!-- Technical Administration Section (special content) -->
<mat-expansion-panel
[expanded]="isExpanded('technical')"
(opened)="setExpanded('technical')"
>
<mat-expansion-panel-header>
<mat-panel-title i18n>Technical Administration</mat-panel-title>
<mat-panel-description i18n
>Manage advanced technical options for maintaining and troubleshooting
your system.</mat-panel-description
>
</mat-expansion-panel-header>
<mat-nav-list class="section-nav-list">
<mat-list-item
[routerLink]="['/admin/config-cleanup']"
matTooltip="Analyze and clean up active configuration issues, like unused dropdown options."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>Configuration Cleanup</span>
</mat-list-item>
<mat-list-item
(click)="editConfig()"
button
matTooltip="Open the raw configuration file to make advanced technical adjustments."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>Edit Application Configuration (JSON)</span>
</mat-list-item>
<mat-list-item
(click)="downloadConfigClick()"
button
matTooltip="Download the raw configuration file."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n
>Download Application Configuration (JSON)</span
>
</mat-list-item>
<mat-list-item
(click)="!isUploadingConfig() && configImport.click()"
button
[class.disabled]="isUploadingConfig()"
matTooltip="Upload a raw configuration file and overwrite the current system configuration. Accepts a single config object or an array of config documents."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n
>Upload Application Configuration (JSON)</span
>
</mat-list-item>
<mat-list-item
[routerLink]="['/admin/ai-agent']"
matTooltip="Download all configuration documents as a single JSON file to use as context for an AI agent session."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>Configure using AI Agents</span>
</mat-list-item>
<mat-list-item
(click)="debugDatabase()"
button
matTooltip="Make the database service available in the browser's developer console for advanced administrative tasks."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>PouchDB Debug: Send to console.log()</span>
</mat-list-item>
<mat-list-item
(click)="emptyRecords()"
button
matTooltip="Delete all data while keeping the system configuration and the profiles of users with an account."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle class="color-error" i18n>Empty Records</span>
</mat-list-item>
<mat-list-item
(click)="resetSystem()"
button
matTooltip="Delete everything, including all records and the complete system configuration, keeping only your own user profile, to set up this system from scratch."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle class="color-error" i18n>Reset System</span>
</mat-list-item>
<mat-list-item
(click)="resetLocalDevice()"
button
matTooltip="Clear all data cached on this device and re-synchronize from the server. Does not affect other users or delete anything on the server."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>Reset Local Device</span>
</mat-list-item>
<mat-list-item
[routerLink]="['/admin/conflicts']"
matTooltip="View any database records that could not be synced because multiple users edited the record at the same time while offline."
i18n-matTooltip
matTooltipPosition="before"
>
<span matListItemTitle i18n>Database Conflicts</span>
</mat-list-item>
</mat-nav-list>
<input
#configImport
type="file"
hidden
(change)="uploadConfigFile($event)"
/>
</mat-expansion-panel>
</mat-accordion>
./admin-overview.component.scss
@use "../../../../styles/variables/sizes";
mat-panel-title {
font-size: 1.3em;
}
mat-panel-description {
white-space: normal;
}
.admin-sections {
.section-nav-list {
margin: 0;
padding: 0;
}
// Clean expansion panel styling - seamless look
::ng-deep .mat-expansion-panel {
box-shadow: none;
border: none;
border-radius: 0;
margin-bottom: 0;
&:not(:last-child) {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
}
// Stack title and description vertically in expansion panel headers
::ng-deep .mat-expansion-panel-header {
padding: sizes.$regular sizes.$large;
height: auto !important; // Allow header to grow with content
.mat-content {
flex-direction: column;
align-items: flex-start;
gap: sizes.$x-small;
overflow: visible;
padding-right: sizes.$large;
}
&:hover {
background: rgba(0, 0, 0, 0.04);
}
}
::ng-deep .mat-expansion-panel-content .mat-expansion-panel-body {
/* add a slight left border to visually connect the related header */
padding-left: 1em;
padding-right: 1em;
padding-bottom: 0;
margin-bottom: sizes.$regular;
border-left: solid 1em #00000014;
border-bottom-left-radius: var(--mat-expansion-container-shape, 12px);
}
::ng-deep .mat-expansion-panel-header.mat-expanded {
/* add a slight background to visually connect the related sub-items */
background-color: #00000014;
border-top-left-radius: var(--mat-expansion-container-shape, 12px);
}
}