Files
Toju/src/app/shared/components/debug-console/debug-console.component.ts
2026-03-13 00:48:33 +01:00

262 lines
7.1 KiB
TypeScript

import {
Component,
HostListener,
computed,
effect,
inject,
input,
signal
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideBug } from '@ng-icons/lucide';
import { DebuggingService, type DebugLogLevel } from '../../../core/services/debugging.service';
import { DebugConsoleEntryListComponent } from './debug-console-entry-list/debug-console-entry-list.component';
import { DebugConsoleNetworkMapComponent } from './debug-console-network-map/debug-console-network-map.component';
import { DebugConsoleToolbarComponent } from './debug-console-toolbar/debug-console-toolbar.component';
import { DebugConsoleResizeService } from './services/debug-console-resize.service';
import { DebugConsoleExportService, type DebugExportFormat } from './services/debug-console-export.service';
import { DebugConsoleEnvironmentService } from './services/debug-console-environment.service';
type DebugLevelState = Record<DebugLogLevel, boolean>;
type DebugConsoleTab = 'logs' | 'network';
type DebugConsoleLauncherVariant = 'floating' | 'inline' | 'compact';
@Component({
selector: 'app-debug-console',
standalone: true,
imports: [
CommonModule,
NgIcon,
DebugConsoleEntryListComponent,
DebugConsoleNetworkMapComponent,
DebugConsoleToolbarComponent
],
viewProviders: [
provideIcons({
lucideBug
})
],
templateUrl: './debug-console.component.html',
host: {
style: 'display: contents;',
'data-debug-console-root': 'true'
}
})
export class DebugConsoleComponent {
readonly debugging = inject(DebuggingService);
readonly resizeService = inject(DebugConsoleResizeService);
readonly exportService = inject(DebugConsoleExportService);
readonly envService = inject(DebugConsoleEnvironmentService);
readonly entries = this.debugging.entries;
readonly isOpen = this.debugging.isConsoleOpen;
readonly networkSnapshot = this.debugging.networkSnapshot;
readonly launcherVariant = input<DebugConsoleLauncherVariant>('floating');
readonly showLauncher = input(true);
readonly showPanel = input(true);
readonly activeTab = signal<DebugConsoleTab>('logs');
readonly detached = signal(false);
readonly searchTerm = signal('');
readonly selectedSource = signal('all');
readonly autoScroll = signal(true);
readonly panelHeight = this.resizeService.panelHeight;
readonly panelWidth = this.resizeService.panelWidth;
readonly panelLeft = this.resizeService.panelLeft;
readonly panelTop = this.resizeService.panelTop;
readonly levelState = signal<DebugLevelState>({
event: true,
info: true,
warn: true,
error: true,
debug: true
});
readonly sourceOptions = computed(() => {
return Array.from(new Set(this.entries().map((entry) => entry.source))).sort();
});
readonly filteredEntries = computed(() => {
const searchTerm = this.searchTerm().trim()
.toLowerCase();
const selectedSource = this.selectedSource();
const levelState = this.levelState();
return this.entries().filter((entry) => {
if (!levelState[entry.level])
return false;
if (selectedSource !== 'all' && entry.source !== selectedSource)
return false;
if (!searchTerm)
return true;
return [
entry.message,
entry.source,
entry.level,
entry.timeLabel,
entry.dateTimeLabel,
entry.payloadText || ''
].some((value) => value.toLowerCase().includes(searchTerm));
});
});
readonly levelCounts = computed<Record<DebugLogLevel, number>>(() => {
const counts: Record<DebugLogLevel, number> = {
event: 0,
info: 0,
warn: 0,
error: 0,
debug: 0
};
for (const entry of this.entries()) {
counts[entry.level] += entry.count;
}
return counts;
});
readonly visibleCount = computed(() => {
return this.filteredEntries().reduce((sum, entry) => sum + entry.count, 0);
});
readonly badgeCount = computed(() => {
const counts = this.levelCounts();
return counts.error > 0 ? counts.error : this.entries().reduce((sum, entry) => sum + entry.count, 0);
});
readonly hasErrors = computed(() => this.levelCounts().error > 0);
readonly networkSummary = computed(() => this.networkSnapshot().summary);
constructor() {
this.resizeService.syncBounds(this.detached());
effect(() => {
const selectedSource = this.selectedSource();
const sourceOptions = this.sourceOptions();
if (selectedSource !== 'all' && !sourceOptions.includes(selectedSource))
this.selectedSource.set('all');
});
}
@HostListener('window:mousemove', ['$event'])
onResizeMove(event: MouseEvent): void {
this.resizeService.onMouseMove(event, this.detached());
}
@HostListener('window:mouseup')
onResizeEnd(): void {
this.resizeService.onMouseUp();
}
@HostListener('window:resize')
onWindowResize(): void {
this.resizeService.syncBounds(this.detached());
}
toggleConsole(): void {
this.debugging.toggleConsole();
}
closeConsole(): void {
this.debugging.closeConsole();
}
updateSearchTerm(value: string): void {
this.searchTerm.set(value);
}
updateSelectedSource(source: string): void {
this.selectedSource.set(source);
}
setActiveTab(tab: DebugConsoleTab): void {
this.activeTab.set(tab);
}
exportLogs(format: DebugExportFormat): void {
const env = this.envService.getEnvironment();
const name = this.envService.getFilenameSafeDisplayName();
this.exportService.exportLogs(
this.filteredEntries(),
format,
env,
name
);
}
exportNetwork(format: DebugExportFormat): void {
const env = this.envService.getEnvironment();
const name = this.envService.getFilenameSafeDisplayName();
this.exportService.exportNetwork(
this.networkSnapshot(),
format,
env,
name
);
}
toggleDetached(): void {
const nextDetached = !this.detached();
this.detached.set(nextDetached);
this.resizeService.syncBounds(nextDetached);
if (nextDetached)
this.resizeService.initializeDetachedPosition();
}
toggleLevel(level: DebugLogLevel): void {
this.levelState.update((current) => ({
...current,
[level]: !current[level]
}));
}
toggleAutoScroll(): void {
this.autoScroll.update((enabled) => !enabled);
}
clearLogs(): void {
this.debugging.clear();
}
startTopResize(event: MouseEvent): void {
this.resizeService.startTopResize(event);
}
startBottomResize(event: MouseEvent): void {
this.resizeService.startBottomResize(event);
}
startLeftResize(event: MouseEvent): void {
this.resizeService.startLeftResize(event);
}
startRightResize(event: MouseEvent): void {
this.resizeService.startRightResize(event);
}
startCornerResize(event: MouseEvent): void {
this.resizeService.startCornerResize(event);
}
startDrag(event: MouseEvent): void {
if (!this.detached())
return;
this.resizeService.startDrag(event);
}
formatBadgeCount(count: number): string {
if (count > 99)
return '99+';
return count.toString();
}
}