import {
ChangeDetectionStrategy,
Component,
computed,
inject,
OnInit,
signal,
} from "@angular/core";
import { MatCardModule } from "@angular/material/card";
import { MatButtonModule } from "@angular/material/button";
import { MatListModule } from "@angular/material/list";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { MatSnackBar } from "@angular/material/snack-bar";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatDialog } from "@angular/material/dialog";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { ConfirmationDialogService } from "../../../common-components/confirmation-dialog/confirmation-dialog.service";
import { Logging } from "#src/app/core/logging/logging.service";
import { ConfigurableEnum } from "../../../basic-datatypes/configurable-enum/configurable-enum";
import { ConfigureEnumPopupComponent } from "../../../basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component";
import {
ConfigCleanupAnalysis,
ConfigurableEnumCleanupService,
ConfigurableEnumUsageSummary,
} from "./configurable-enum-cleanup.service";
@Component({
selector: "app-configurable-enum-cleanup",
templateUrl: "./configurable-enum-cleanup.component.html",
styleUrl: "./configurable-enum-cleanup.component.scss",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
MatCardModule,
MatButtonModule,
MatListModule,
MatProgressSpinnerModule,
MatTooltipModule,
FontAwesomeModule,
],
})
export class ConfigurableEnumCleanupComponent implements OnInit {
private readonly configCleanupService = inject(
ConfigurableEnumCleanupService,
);
private readonly confirmationDialog = inject(ConfirmationDialogService);
private readonly snackBar = inject(MatSnackBar);
private readonly dialog = inject(MatDialog);
protected readonly isLoading = signal(false);
protected readonly errorMessage = signal<string | undefined>(undefined);
private readonly analysis = signal<ConfigCleanupAnalysis | undefined>(
undefined,
);
private readonly deletingIds = signal<string[]>([]);
protected readonly totalEnums = computed(
() => this.analysis()?.totalEnums ?? 0,
);
protected readonly usedEnums = computed(
() => this.analysis()?.usedEnums ?? 0,
);
protected readonly usedEnumDetails = computed(
() => this.analysis()?.usedEnumDetails ?? [],
);
protected readonly unusedEnums = computed(
() => this.analysis()?.unusedEnums ?? [],
);
ngOnInit(): void {
void this.reload();
}
protected async reload() {
this.isLoading.set(true);
this.errorMessage.set(undefined);
try {
const analysis =
await this.configCleanupService.analyzeUnusedConfigurableEnums();
this.analysis.set(analysis);
} catch (error) {
Logging.error("Failed to analyze configurable enum cleanup data", error);
this.errorMessage.set(
$localize`Could not analyze configuration cleanup data. Please try again.`,
);
} finally {
this.isLoading.set(false);
}
}
protected isDeleting(enumId: string) {
return this.deletingIds().includes(enumId);
}
protected enumLabel(enumSummary: ConfigurableEnumUsageSummary): string {
return this.formatEnumIdAsLabel(enumSummary.enumEntity.getId(true));
}
protected openEnumOptions(enumEntity: ConfigurableEnum, event: Event) {
event.stopPropagation();
this.dialog.open(ConfigureEnumPopupComponent, { data: enumEntity });
}
private formatEnumIdAsLabel(enumId: string): string {
return enumId
.split(/[-_]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
protected async deleteEnum(unusedEnum: ConfigurableEnumUsageSummary) {
const enumId = unusedEnum.enumEntity.getId(true);
const enumLabel = this.enumLabel(unusedEnum);
const confirmed = await this.confirmationDialog.getConfirmation(
$localize`Delete unused enum?`,
$localize`Are you sure you want to delete the unused enum "${enumLabel}"?`,
);
if (!confirmed) {
return;
}
this.deletingIds.update((ids) => [...ids, enumId]);
try {
const deleted =
await this.configCleanupService.deleteUnusedConfigurableEnum(
unusedEnum.enumEntity,
);
if (deleted) {
this.snackBar.open(
$localize`Enum "${enumLabel}" deleted successfully.`,
undefined,
{
duration: 3000,
},
);
// Reload after delete to refresh the list
await this.reload();
} else {
this.snackBar.open(
$localize`Enum is now used in schema and cannot be deleted.`,
undefined,
{
duration: 5000,
},
);
}
} catch (error) {
Logging.error("Failed to delete configurable enum", error);
this.snackBar.open(
$localize`Could not delete enum. Please try again.`,
undefined,
{
duration: 5000,
},
);
} finally {
this.deletingIds.update((ids) => ids.filter((id) => id !== enumId));
}
}
}
<div class="flex-column gap-small">
<div class="flex-row justify-space-between align-start flex-wrap gap-small">
<button
mat-stroked-button
(click)="reload()"
[disabled]="isLoading()"
color="accent"
i18n
>
Refresh Analysis
</button>
</div>
@if (isLoading()) {
<div class="flex-row align-center gap-small" aria-live="polite">
<mat-spinner [diameter]="36" />
<span i18n>Analyzing configuration ...</span>
</div>
} @else {
@if (errorMessage(); as error) {
<mat-card appearance="outlined">
<mat-card-content>
<p class="remove-margin-bottom">{{ error }}</p>
</mat-card-content>
</mat-card>
}
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title i18n>Summary</mat-card-title>
</mat-card-header>
<mat-card-content class="summary-grid">
<div>
<span class="summary-label" i18n>Total ConfigurableEnums</span>
<strong>{{ totalEnums() }}</strong>
</div>
<div>
<span class="summary-label" i18n>Used by Schema</span>
<strong>{{ usedEnums() }}</strong>
</div>
<div>
<span class="summary-label" i18n>Unused</span>
<strong>{{ unusedEnums().length }}</strong>
</div>
</mat-card-content>
</mat-card>
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title i18n>Used ConfigurableEnums</mat-card-title>
<mat-card-subtitle i18n>
Includes configurable enums that are referenced in the active runtime
schema.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
@if (usedEnumDetails().length === 0) {
<p i18n>No used configurable enums found.</p>
} @else {
<mat-list>
@for (
enumSummary of usedEnumDetails();
track enumSummary.enumEntity.getId()
) {
<mat-list-item>
<div matListItemTitle>
{{ enumLabel(enumSummary) }}
</div>
<div matListItemMeta>
<button
mat-icon-button
color="accent"
(click)="openEnumOptions(enumSummary.enumEntity, $event)"
i18n-aria-label
aria-label="Configure enum options"
i18n-matTooltip
matTooltip="Configure options"
>
<fa-icon icon="wrench"></fa-icon>
</button>
</div>
</mat-list-item>
}
</mat-list>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title i18n>Unused ConfigurableEnums</mat-card-title>
<mat-card-subtitle i18n>
Based on active runtime entity schemas.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
@if (unusedEnums().length === 0) {
<p i18n>No unused configurable enums found.</p>
} @else {
<mat-list>
@for (
unusedEnum of unusedEnums();
track unusedEnum.enumEntity.getId()
) {
<mat-list-item>
<div matListItemTitle>
{{ enumLabel(unusedEnum) }}
</div>
<div matListItemMeta>
<button
mat-icon-button
color="accent"
(click)="openEnumOptions(unusedEnum.enumEntity, $event)"
i18n-aria-label
aria-label="Configure enum options"
i18n-matTooltip
matTooltip="Configure options"
>
<fa-icon icon="wrench"></fa-icon>
</button>
<button
mat-icon-button
color="accent"
(click)="deleteEnum(unusedEnum)"
[disabled]="isDeleting(unusedEnum.enumEntity.getId(true))"
i18n-aria-label
aria-label="Delete enum"
i18n-matTooltip
matTooltip="Delete"
>
<fa-icon icon="trash"></fa-icon>
</button>
</div>
</mat-list-item>
}
</mat-list>
}
</mat-card-content>
</mat-card>
}
</div>