mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-05-14 01:35:20 -04:00
Add handling for items that are not being blocked (#346)
This commit is contained in:
@@ -8,6 +8,15 @@ export interface AppEvent {
|
||||
trackingId?: string;
|
||||
}
|
||||
|
||||
export interface ManualEvent {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
message: string;
|
||||
data?: string;
|
||||
severity: string;
|
||||
isResolved: boolean;
|
||||
}
|
||||
|
||||
export interface EventStats {
|
||||
totalEvents: number;
|
||||
eventsBySeverity: { severity: string; count: number }[];
|
||||
|
||||
@@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import * as signalR from '@microsoft/signalr';
|
||||
import { LogEntry } from '../models/signalr.models';
|
||||
import { AppEvent } from '../models/event.models';
|
||||
import { AppEvent, ManualEvent } from '../models/event.models';
|
||||
import { AppStatus } from '../models/app-status.model';
|
||||
import { JobInfo } from '../models/job.models';
|
||||
import { ApplicationPathService } from './base-path.service';
|
||||
@@ -18,12 +18,14 @@ export class AppHubService {
|
||||
private connectionStatusSubject = new BehaviorSubject<boolean>(false);
|
||||
private logsSubject = new BehaviorSubject<LogEntry[]>([]);
|
||||
private eventsSubject = new BehaviorSubject<AppEvent[]>([]);
|
||||
private manualEventsSubject = new BehaviorSubject<ManualEvent[]>([]);
|
||||
private appStatusSubject = new BehaviorSubject<AppStatus | null>(null);
|
||||
private jobsSubject = new BehaviorSubject<JobInfo[]>([]);
|
||||
private readonly ApplicationPathService = inject(ApplicationPathService);
|
||||
|
||||
|
||||
private logBuffer: LogEntry[] = [];
|
||||
private eventBuffer: AppEvent[] = [];
|
||||
private manualEventBuffer: ManualEvent[] = [];
|
||||
private readonly bufferSize = 1000;
|
||||
|
||||
constructor() { }
|
||||
@@ -117,6 +119,24 @@ export class AppHubService {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle individual manual event messages
|
||||
this.hubConnection.on('ManualEventReceived', (event: ManualEvent) => {
|
||||
this.addManualEventToBuffer(event);
|
||||
const currentEvents = this.manualEventsSubject.value;
|
||||
this.manualEventsSubject.next([...currentEvents, event]);
|
||||
});
|
||||
|
||||
// Handle bulk manual event messages (initial load)
|
||||
this.hubConnection.on('ManualEventsReceived', (events: ManualEvent[]) => {
|
||||
if (events && events.length > 0) {
|
||||
// Set all manual events at once
|
||||
this.manualEventsSubject.next(events);
|
||||
// Update buffer
|
||||
this.manualEventBuffer = [...events];
|
||||
this.trimBuffer(this.manualEventBuffer, this.bufferSize);
|
||||
}
|
||||
});
|
||||
|
||||
this.hubConnection.on('AppStatusUpdated', (status: AppStatus | null) => {
|
||||
if (!status) {
|
||||
this.appStatusSubject.next(null);
|
||||
@@ -158,6 +178,7 @@ export class AppHubService {
|
||||
private requestInitialData(): void {
|
||||
this.requestRecentLogs();
|
||||
this.requestRecentEvents();
|
||||
this.requestRecentManualEvents();
|
||||
this.requestJobStatus();
|
||||
}
|
||||
|
||||
@@ -180,12 +201,22 @@ export class AppHubService {
|
||||
.catch(err => console.error('Error requesting recent events:', err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Request recent manual events from the server
|
||||
*/
|
||||
public requestRecentManualEvents(count: number = 100): void {
|
||||
if (this.isConnected()) {
|
||||
this.hubConnection.invoke('GetRecentManualEvents', count)
|
||||
.catch(err => console.error('Error requesting recent manual events:', err));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the connection is established
|
||||
*/
|
||||
private isConnected(): boolean {
|
||||
return this.hubConnection &&
|
||||
return this.hubConnection &&
|
||||
this.hubConnection.state === signalR.HubConnectionState.Connected;
|
||||
}
|
||||
|
||||
@@ -222,7 +253,15 @@ export class AppHubService {
|
||||
this.eventBuffer.push(event);
|
||||
this.trimBuffer(this.eventBuffer, this.bufferSize);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a manual event to the buffer
|
||||
*/
|
||||
private addManualEventToBuffer(event: ManualEvent): void {
|
||||
this.manualEventBuffer.push(event);
|
||||
this.trimBuffer(this.manualEventBuffer, this.bufferSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim a buffer to the specified size
|
||||
*/
|
||||
@@ -248,6 +287,13 @@ export class AppHubService {
|
||||
return this.eventsSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manual events as an observable
|
||||
*/
|
||||
public getManualEvents(): Observable<ManualEvent[]> {
|
||||
return this.manualEventsSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get jobs as an observable
|
||||
*/
|
||||
@@ -307,7 +353,15 @@ export class AppHubService {
|
||||
this.eventsSubject.next([]);
|
||||
this.eventBuffer = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear manual events
|
||||
*/
|
||||
public clearManualEvents(): void {
|
||||
this.manualEventsSubject.next([]);
|
||||
this.manualEventBuffer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear logs
|
||||
*/
|
||||
@@ -315,4 +369,13 @@ export class AppHubService {
|
||||
this.logsSubject.next([]);
|
||||
this.logBuffer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific manual event from the subject
|
||||
*/
|
||||
public removeManualEvent(eventId: string): void {
|
||||
const currentEvents = this.manualEventsSubject.value;
|
||||
const filteredEvents = currentEvents.filter(e => e.id !== eventId);
|
||||
this.manualEventsSubject.next(filteredEvents);
|
||||
}
|
||||
}
|
||||
|
||||
109
code/frontend/src/app/core/services/manual-events.service.ts
Normal file
109
code/frontend/src/app/core/services/manual-events.service.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ManualEvent } from '../models/event.models';
|
||||
import { ApplicationPathService } from './base-path.service';
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalCount: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface ManualEventFilter {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
isResolved?: boolean;
|
||||
severity?: string;
|
||||
fromDate?: Date;
|
||||
toDate?: Date;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface ManualEventStats {
|
||||
totalEvents: number;
|
||||
unresolvedEvents: number;
|
||||
resolvedEvents: number;
|
||||
eventsBySeverity: { severity: string; count: number }[];
|
||||
unresolvedBySeverity: { severity: string; count: number }[];
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ManualEventsService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly applicationPathService = inject(ApplicationPathService);
|
||||
private readonly baseUrl = this.applicationPathService.buildApiUrl('/manualevents');
|
||||
|
||||
/**
|
||||
* Get manual events with pagination and filtering
|
||||
*/
|
||||
getManualEvents(filter?: ManualEventFilter): Observable<PaginatedResult<ManualEvent>> {
|
||||
let params = new HttpParams();
|
||||
|
||||
if (filter) {
|
||||
if (filter.page !== undefined) {
|
||||
params = params.set('page', filter.page.toString());
|
||||
}
|
||||
if (filter.pageSize !== undefined) {
|
||||
params = params.set('pageSize', filter.pageSize.toString());
|
||||
}
|
||||
if (filter.isResolved !== undefined) {
|
||||
params = params.set('isResolved', filter.isResolved.toString());
|
||||
}
|
||||
if (filter.severity) {
|
||||
params = params.set('severity', filter.severity);
|
||||
}
|
||||
if (filter.fromDate) {
|
||||
params = params.set('fromDate', filter.fromDate.toISOString());
|
||||
}
|
||||
if (filter.toDate) {
|
||||
params = params.set('toDate', filter.toDate.toISOString());
|
||||
}
|
||||
if (filter.search) {
|
||||
params = params.set('search', filter.search);
|
||||
}
|
||||
}
|
||||
|
||||
return this.http.get<PaginatedResult<ManualEvent>>(this.baseUrl, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific manual event by ID
|
||||
*/
|
||||
getManualEvent(id: string): Observable<ManualEvent> {
|
||||
return this.http.get<ManualEvent>(`${this.baseUrl}/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a manual event as resolved
|
||||
*/
|
||||
resolveManualEvent(id: string): Observable<void> {
|
||||
return this.http.post<void>(`${this.baseUrl}/${id}/resolve`, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manual event statistics
|
||||
*/
|
||||
getManualEventStats(): Observable<ManualEventStats> {
|
||||
return this.http.get<ManualEventStats>(`${this.baseUrl}/stats`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available severities
|
||||
*/
|
||||
getSeverities(): Observable<string[]> {
|
||||
return this.http.get<string[]>(`${this.baseUrl}/severities`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger cleanup of old resolved events
|
||||
*/
|
||||
cleanupOldResolvedEvents(retentionDays: number = 30): Observable<{ deletedCount: number }> {
|
||||
const params = new HttpParams().set('retentionDays', retentionDays.toString());
|
||||
return this.http.post<{ deletedCount: number }>(`${this.baseUrl}/cleanup`, {}, { params });
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,104 @@
|
||||
<div class="mb-4" *ngIf="showSupportSection()">
|
||||
<app-support-section></app-support-section>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Manual Events Card (Action Required) -->
|
||||
<div class="mb-4" *ngIf="currentManualEvent()">
|
||||
<div class="glow-event-card-wrapper" [ngClass]="getManualEventSeverityClass(currentManualEvent()!.severity)">
|
||||
<p-card styleClass="dashboard-card">
|
||||
<ng-template pTemplate="header">
|
||||
<div class="flex align-items-center justify-content-between p-3 border-bottom-1 surface-border">
|
||||
<div class="header-title-container">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-exclamation-triangle text-2xl"></i>
|
||||
<h2 class="card-title m-0">Action Required</h2>
|
||||
</div>
|
||||
<span class="card-subtitle">Manual intervention needed</span>
|
||||
</div>
|
||||
<div class="flex align-items-center gap-2">
|
||||
<p-tag
|
||||
[severity]="getEventSeverity(currentManualEvent()!.severity)"
|
||||
[value]="currentManualEvent()!.severity"
|
||||
></p-tag>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="card-content">
|
||||
<!-- Event Message -->
|
||||
<div class="event-message mb-3">
|
||||
<h3 class="text-xl font-semibold mb-2" [innerHTML]="processManualEventMessage(currentManualEvent()!.message)"></h3>
|
||||
<p class="text-sm text-color-secondary">
|
||||
{{ currentManualEvent()!.timestamp | date: 'yyyy-MM-dd HH:mm:ss' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Event Data (Collapsible) -->
|
||||
<div class="event-data mb-3" *ngIf="currentManualEvent()!.data">
|
||||
<p-accordion [multiple]="false">
|
||||
<p-accordion-panel [value]="0">
|
||||
<p-accordion-header>
|
||||
<ng-template #toggleicon let-active="active">
|
||||
@if (active) {
|
||||
<i class="pi pi-chevron-up"></i>
|
||||
} @else {
|
||||
<i class="pi pi-chevron-down"></i>
|
||||
}
|
||||
</ng-template>
|
||||
Event Details
|
||||
</p-accordion-header>
|
||||
<p-accordion-content>
|
||||
<div class="event-data-content">
|
||||
<pre class="text-sm">{{ parseEventData(currentManualEvent()!.data) | json }}</pre>
|
||||
</div>
|
||||
</p-accordion-content>
|
||||
</p-accordion-panel>
|
||||
</p-accordion>
|
||||
</div>
|
||||
|
||||
<!-- Navigation and Action Controls -->
|
||||
<div class="glow-card-footer">
|
||||
<div class="flex align-items-center justify-content-between gap-3">
|
||||
<!-- Navigation -->
|
||||
<div class="flex align-items-center gap-2">
|
||||
<button
|
||||
pButton
|
||||
icon="pi pi-chevron-left"
|
||||
class="p-button-outlined p-button-sm"
|
||||
(click)="previousManualEvent()"
|
||||
[disabled]="!canNavigatePrevious()"
|
||||
pTooltip="Previous event"
|
||||
tooltipPosition="top"
|
||||
></button>
|
||||
<span class="text-sm font-medium">
|
||||
{{ currentManualEventIndex() + 1 }} of {{ unresolvedManualEvents().length }}
|
||||
</span>
|
||||
<button
|
||||
pButton
|
||||
icon="pi pi-chevron-right"
|
||||
class="p-button-outlined p-button-sm"
|
||||
(click)="nextManualEvent()"
|
||||
[disabled]="!canNavigateNext()"
|
||||
pTooltip="Next event"
|
||||
tooltipPosition="top"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<!-- Dismiss Button -->
|
||||
<button
|
||||
pButton
|
||||
label="Dismiss"
|
||||
icon="pi pi-check"
|
||||
class="p-button-success"
|
||||
(click)="dismissManualEvent(currentManualEvent()!.id)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Real-time Cards -->
|
||||
<div class="grid">
|
||||
<!-- Recent Logs Card -->
|
||||
|
||||
@@ -1,3 +1,224 @@
|
||||
// Glowing event card wrapper styles
|
||||
.glow-event-card-wrapper {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
animation: slideInUp 0.6s ease-out forwards, shake 3s ease-in-out infinite;
|
||||
animation-delay: 0s, 0.6s;
|
||||
|
||||
::ng-deep .dashboard-card.p-card {
|
||||
border: 3px solid !important;
|
||||
border-radius: 10px !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
// Override hover effect from dashboard-card
|
||||
::ng-deep .dashboard-card.p-card:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.severity-error {
|
||||
box-shadow:
|
||||
0 0 20px rgba(239, 68, 68, 0.7),
|
||||
0 0 40px rgba(239, 68, 68, 0.5),
|
||||
0 0 60px rgba(239, 68, 68, 0.3);
|
||||
animation: slideInUp 0.6s ease-out forwards, glowBorderError 2s ease-in-out infinite, shake 3s ease-in-out infinite;
|
||||
animation-delay: 0s, 0.6s, 0.6s;
|
||||
|
||||
::ng-deep .dashboard-card.p-card {
|
||||
border-color: var(--red-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.severity-warning {
|
||||
box-shadow:
|
||||
0 0 20px rgba(234, 179, 8, 0.7),
|
||||
0 0 40px rgba(234, 179, 8, 0.5),
|
||||
0 0 60px rgba(234, 179, 8, 0.3);
|
||||
animation: slideInUp 0.6s ease-out forwards, glowBorderWarning 2s ease-in-out infinite, shake 3s ease-in-out infinite;
|
||||
animation-delay: 0s, 0.6s, 0.6s;
|
||||
|
||||
::ng-deep .dashboard-card.p-card {
|
||||
border-color: var(--yellow-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.severity-important {
|
||||
box-shadow:
|
||||
0 0 20px rgba(249, 115, 22, 0.7),
|
||||
0 0 40px rgba(249, 115, 22, 0.5),
|
||||
0 0 60px rgba(249, 115, 22, 0.3);
|
||||
animation: slideInUp 0.6s ease-out forwards, glowBorderImportant 2s ease-in-out infinite, shake 3s ease-in-out infinite;
|
||||
animation-delay: 0s, 0.6s, 0.6s;
|
||||
|
||||
::ng-deep .dashboard-card.p-card {
|
||||
border-color: var(--orange-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.severity-info {
|
||||
box-shadow:
|
||||
0 0 20px rgba(59, 130, 246, 0.7),
|
||||
0 0 40px rgba(59, 130, 246, 0.5),
|
||||
0 0 60px rgba(59, 130, 246, 0.3);
|
||||
animation: slideInUp 0.6s ease-out forwards, glowBorderInfo 2s ease-in-out infinite, shake 3s ease-in-out infinite;
|
||||
animation-delay: 0s, 0.6s, 0.6s;
|
||||
|
||||
::ng-deep .dashboard-card.p-card {
|
||||
border-color: var(--blue-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.severity-default {
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 100, 100, 0.5),
|
||||
0 0 40px rgba(100, 100, 100, 0.3),
|
||||
0 0 60px rgba(100, 100, 100, 0.2);
|
||||
animation: slideInUp 0.6s ease-out forwards, glowBorderDefault 2s ease-in-out infinite, shake 3s ease-in-out infinite;
|
||||
animation-delay: 0s, 0.6s, 0.6s;
|
||||
|
||||
::ng-deep .dashboard-card.p-card {
|
||||
border-color: var(--surface-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.event-data-content {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: var(--surface-50);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.glow-card-footer {
|
||||
border-top: 1px solid var(--surface-border);
|
||||
padding-top: 1rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flex.align-items-center.justify-content-between {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
|
||||
.flex.align-items-center.gap-2 {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Glowing border animations
|
||||
@keyframes glowBorderError {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 20px rgba(239, 68, 68, 0.7),
|
||||
0 0 40px rgba(239, 68, 68, 0.5),
|
||||
0 0 60px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 35px rgba(239, 68, 68, 0.9),
|
||||
0 0 60px rgba(239, 68, 68, 0.7),
|
||||
0 0 90px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowBorderImportant {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 20px rgba(249, 115, 22, 0.7),
|
||||
0 0 40px rgba(249, 115, 22, 0.5),
|
||||
0 0 60px rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 35px rgba(249, 115, 22, 0.9),
|
||||
0 0 60px rgba(249, 115, 22, 0.7),
|
||||
0 0 90px rgba(249, 115, 22, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowBorderWarning {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 20px rgba(234, 179, 8, 0.7),
|
||||
0 0 40px rgba(234, 179, 8, 0.5),
|
||||
0 0 60px rgba(234, 179, 8, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 35px rgba(234, 179, 8, 0.9),
|
||||
0 0 60px rgba(234, 179, 8, 0.7),
|
||||
0 0 90px rgba(234, 179, 8, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowBorderInfo {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 20px rgba(59, 130, 246, 0.7),
|
||||
0 0 40px rgba(59, 130, 246, 0.5),
|
||||
0 0 60px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 35px rgba(59, 130, 246, 0.9),
|
||||
0 0 60px rgba(59, 130, 246, 0.7),
|
||||
0 0 90px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowBorderDefault {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 100, 100, 0.5),
|
||||
0 0 40px rgba(100, 100, 100, 0.3),
|
||||
0 0 60px rgba(100, 100, 100, 0.2);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 35px rgba(100, 100, 100, 0.7),
|
||||
0 0 60px rgba(100, 100, 100, 0.5),
|
||||
0 0 90px rgba(100, 100, 100, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// Shake animation
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
2% {
|
||||
transform: translateX(-3px) rotate(-0.5deg);
|
||||
}
|
||||
4% {
|
||||
transform: translateX(3px) rotate(0.5deg);
|
||||
}
|
||||
6% {
|
||||
transform: translateX(-3px) rotate(-0.5deg);
|
||||
}
|
||||
8% {
|
||||
transform: translateX(3px) rotate(0.5deg);
|
||||
}
|
||||
10% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
@@ -571,4 +792,16 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slide in up animation for manual event card
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,14 @@ import { ButtonModule } from 'primeng/button';
|
||||
import { TagModule } from 'primeng/tag';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import { ProgressSpinnerModule } from 'primeng/progressspinner';
|
||||
import { AccordionModule } from 'primeng/accordion';
|
||||
|
||||
// Services & Models
|
||||
import { AppHubService } from '../../core/services/app-hub.service';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { ManualEventsService } from '../../core/services/manual-events.service';
|
||||
import { LogEntry } from '../../core/models/signalr.models';
|
||||
import { AppEvent } from '../../core/models/event.models';
|
||||
import { AppEvent, ManualEvent } from '../../core/models/event.models';
|
||||
import { GeneralConfig } from '../../shared/models/general-config.model';
|
||||
|
||||
// Components
|
||||
@@ -34,6 +36,7 @@ import { JobsManagementComponent } from '../../shared/components/jobs-management
|
||||
TagModule,
|
||||
TooltipModule,
|
||||
ProgressSpinnerModule,
|
||||
AccordionModule,
|
||||
SupportSectionComponent,
|
||||
JobsManagementComponent
|
||||
],
|
||||
@@ -43,11 +46,14 @@ import { JobsManagementComponent } from '../../shared/components/jobs-management
|
||||
export class DashboardPageComponent implements OnInit, OnDestroy {
|
||||
private appHubService = inject(AppHubService);
|
||||
private configurationService = inject(ConfigurationService);
|
||||
private manualEventsService = inject(ManualEventsService);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// Signals for reactive state
|
||||
recentLogs = signal<LogEntry[]>([]);
|
||||
recentEvents = signal<AppEvent[]>([]);
|
||||
manualEvents = signal<ManualEvent[]>([]);
|
||||
currentManualEventIndex = signal<number>(0);
|
||||
connected = signal<boolean>(false);
|
||||
generalConfig = signal<GeneralConfig | null>(null);
|
||||
|
||||
@@ -57,13 +63,38 @@ export class DashboardPageComponent implements OnInit, OnDestroy {
|
||||
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) // Sort chronologically (oldest first)
|
||||
.slice(-5); // Take the last 10 (most recent);
|
||||
});
|
||||
|
||||
|
||||
displayEvents = computed(() => {
|
||||
return this.recentEvents()
|
||||
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) // Sort chronologically (oldest first)
|
||||
.slice(-5); // Take the last 10 (most recent)
|
||||
});
|
||||
|
||||
// Filter only unresolved manual events, sorted oldest first
|
||||
unresolvedManualEvents = computed(() => {
|
||||
return this.manualEvents()
|
||||
.filter(e => !e.isResolved)
|
||||
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
});
|
||||
|
||||
// Get the current manual event to display
|
||||
currentManualEvent = computed(() => {
|
||||
const events = this.unresolvedManualEvents();
|
||||
const index = this.currentManualEventIndex();
|
||||
return events.length > 0 && index < events.length ? events[index] : null;
|
||||
});
|
||||
|
||||
// Check if we can navigate to previous event
|
||||
canNavigatePrevious = computed(() => {
|
||||
return this.currentManualEventIndex() > 0;
|
||||
});
|
||||
|
||||
// Check if we can navigate to next event
|
||||
canNavigateNext = computed(() => {
|
||||
const events = this.unresolvedManualEvents();
|
||||
return this.currentManualEventIndex() < events.length - 1;
|
||||
});
|
||||
|
||||
// Computed value for showing support section
|
||||
showSupportSection = computed(() => {
|
||||
return this.generalConfig()?.displaySupportBanner ?? false;
|
||||
@@ -112,6 +143,13 @@ export class DashboardPageComponent implements OnInit, OnDestroy {
|
||||
this.recentEvents.set(events);
|
||||
});
|
||||
|
||||
// Subscribe to manual events
|
||||
this.appHubService.getManualEvents()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((events: ManualEvent[]) => {
|
||||
this.manualEvents.set(events);
|
||||
});
|
||||
|
||||
// Subscribe to connection status
|
||||
this.appHubService.getConnectionStatus()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -272,4 +310,94 @@ export class DashboardPageComponent implements OnInit, OnDestroy {
|
||||
// Convert PascalCase to readable format
|
||||
return eventType.replace(/([A-Z])/g, ' $1').trim();
|
||||
}
|
||||
|
||||
// Manual event navigation methods
|
||||
nextManualEvent(): void {
|
||||
if (this.canNavigateNext()) {
|
||||
this.currentManualEventIndex.update(i => i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
previousManualEvent(): void {
|
||||
if (this.canNavigatePrevious()) {
|
||||
this.currentManualEventIndex.update(i => i - 1);
|
||||
}
|
||||
}
|
||||
|
||||
dismissManualEvent(eventId: string): void {
|
||||
this.manualEventsService.resolveManualEvent(eventId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
// Remove from local state immediately
|
||||
this.appHubService.removeManualEvent(eventId);
|
||||
|
||||
// Adjust index if needed
|
||||
const events = this.unresolvedManualEvents();
|
||||
const currentIndex = this.currentManualEventIndex();
|
||||
|
||||
if (currentIndex >= events.length && currentIndex > 0) {
|
||||
// If we dismissed the last event, go to the previous one
|
||||
this.currentManualEventIndex.set(events.length - 1);
|
||||
} else if (events.length === 0) {
|
||||
// Reset to 0 if no more events
|
||||
this.currentManualEventIndex.set(0);
|
||||
}
|
||||
// Otherwise, stay at the same index (which now shows the next event)
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to dismiss manual event:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to parse JSON data safely
|
||||
parseEventData(data: string | undefined): any {
|
||||
if (!data) return null;
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Process message to convert URLs to clickable links and handle newlines
|
||||
processManualEventMessage(message: string): string {
|
||||
if (!message) return '';
|
||||
|
||||
// First, escape HTML to prevent XSS
|
||||
const escaped = message
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
// Convert newlines to <br> tags
|
||||
let processed = escaped.replace(/\\n/g, '<br>').replace(/\n/g, '<br>');
|
||||
|
||||
// Convert URLs to clickable links
|
||||
const urlRegex = /(https?:\/\/[^\s<]+)/g;
|
||||
processed = processed.replace(urlRegex, '<a href="$1" target="_blank" rel="noopener noreferrer" class="manual-event-link">$1</a>');
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
// Get severity class for manual events
|
||||
getManualEventSeverityClass(severity: string): string {
|
||||
const normalizedSeverity = severity?.toLowerCase() || '';
|
||||
|
||||
switch (normalizedSeverity) {
|
||||
case 'error':
|
||||
return 'severity-error';
|
||||
case 'warning':
|
||||
return 'severity-warning';
|
||||
case 'information':
|
||||
return 'severity-info';
|
||||
case 'important':
|
||||
return 'severity-important';
|
||||
default:
|
||||
return 'severity-default';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user