This commit is contained in:
Flaminel
2025-05-22 19:18:47 +03:00
parent a8e188aa01
commit 100528ab06
3 changed files with 204 additions and 424 deletions

View File

@@ -1,158 +1,202 @@
<div class="logs-container">
<!-- Connection Status Card - only shown when disconnected -->
<p-card *ngIf="!isConnected()" styleClass="mb-3 connection-status-card">
<div class="flex flex-column align-items-center gap-3 py-5">
<p-progressSpinner styleClass="w-4rem h-4rem" strokeWidth="4" fill="var(--surface-ground)" animationDuration=".5s"></p-progressSpinner>
<span class="text-xl font-medium">Connecting to server...</span>
</div>
</p-card>
<!-- Main Logs Card -->
<p-card styleClass="logs-card">
<!-- Card Header -->
<ng-template pTemplate="header">
<div class="flex align-items-center justify-content-between p-3 border-bottom-1 surface-border">
<div class="flex align-items-center gap-2">
<h2 class="m-0">Application Logs</h2>
<p-tag [severity]="isConnected() ? 'success' : 'danger'"
[value]="isConnected() ? 'Connected' : 'Disconnected'"
[pTooltip]="isConnected() ? 'Connected to log hub' : 'Attempting to reconnect...'"
tooltipPosition="right"></p-tag>
<!-- Connection Status Card - only shown when disconnected -->
<p-card *ngIf="!isConnected()" styleClass="mb-3 connection-status-card">
<div class="flex flex-column align-items-center gap-3 py-5">
<p-progressSpinner
styleClass="w-4rem h-4rem"
strokeWidth="4"
fill="var(--surface-ground)"
animationDuration=".5s"
></p-progressSpinner>
<span class="text-xl font-medium">Connecting to server...</span>
</div>
<button pButton icon="pi pi-refresh" class="p-button-rounded p-button-text"
(click)="refresh()" pTooltip="Refresh logs"
[loading]="false"></button>
</div>
</ng-template>
</p-card>
<!-- Filters Section -->
<div class="filter-container flex align-items-center justify-content-between flex-wrap gap-3">
<div class="flex align-items-center gap-2 flex-wrap">
<!-- Level Filter -->
<p-dropdown [options]="levels()" placeholder="Filter by level"
[showClear]="true" (onChange)="onLevelFilterChange($event.value)"
styleClass="fixed-height-dropdown" [disabled]="!isConnected()">
<ng-template pTemplate="selectedItem">
<div class="level-indicator" *ngIf="levelFilter()">
<p-tag [severity]="getSeverity(levelFilter() || '')" [value]="levelFilter() || ''" styleClass="level-tag"></p-tag>
</div>
</ng-template>
<ng-template let-level pTemplate="item">
<div class="level-indicator">
<p-tag [severity]="getSeverity(level.value)" [value]="level.label" styleClass="level-tag"></p-tag>
</div>
</ng-template>
</p-dropdown>
<!-- Category Filter -->
<p-dropdown [options]="categories()" placeholder="Filter by category"
[showClear]="true" (onChange)="onCategoryFilterChange($event.value)"
styleClass="fixed-height-dropdown" [disabled]="!isConnected()">
</p-dropdown>
</div>
<div class="flex align-items-center gap-2 flex-wrap">
<!-- Search Filter -->
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<input type="text" pInputText [(ngModel)]="searchFilter"
(input)="onSearchChange($event)"
placeholder="Search logs" [disabled]="!isConnected()"/>
</span>
<!-- Clear Filters Button -->
<button pButton icon="pi pi-filter-slash"
label="Clear Filters"
class="p-button-outlined"
(click)="clearFilters()"
[disabled]="!isConnected() || (!levelFilter() && !categoryFilter() && !searchFilter)"></button>
</div>
</div>
<!-- Logs Table -->
<p-table [value]="filteredLogs()"
styleClass="p-datatable-sm logs-table"
[scrollable]="true"
scrollHeight="calc(100vh - 260px)"
[paginator]="true"
[rows]="25"
[showCurrentPageReport]="true"
[rowsPerPageOptions]="[10, 25, 50, 100]"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} logs"
[rowHover]="true"
[loading]="!isConnected()">
<!-- Table Header -->
<ng-template pTemplate="header">
<tr>
<th style="width: 180px">Timestamp</th>
<th style="width: 100px">Level</th>
<th style="width: 120px">Category</th>
<th>Message</th>
<th style="width: 120px" *ngIf="hasJobInfo()">Job Name</th>
<th style="width: 120px" *ngIf="hasInstanceInfo()">Instance</th>
</tr>
</ng-template>
<!-- Table Body -->
<ng-template pTemplate="body" let-log>
<!-- Log Entry Row -->
<tr [ngClass]="{
'error-row': log.level === 'Error' || log.level === 'Fatal' || log.level === 'Critical',
'warning-row': log.level === 'Warning'
}">
<td>
<span class="font-medium">{{ log.timestamp | date: 'yyyy-MM-dd' }}</span>
<div class="text-sm text-color-secondary">{{ log.timestamp | date: 'HH:mm:ss' }}</div>
</td>
<td>
<p-tag [severity]="getSeverity(log.level)" [value]="log.level"></p-tag>
</td>
<td>
<span class="log-category">{{ log.category }}</span>
</td>
<td>
<span [pTooltip]="log.exception ? 'Click to view stack trace' : undefined"
tooltipPosition="top"
[tooltipStyleClass]="log.exception ? 'visible' : 'invisible'">
{{ log.message }}
</span>
</td>
<td *ngIf="hasJobInfo()" class="font-medium">{{ log.jobName }}</td>
<td *ngIf="hasInstanceInfo()" class="text-sm">{{ log.instanceName }}</td>
</tr>
<!-- Exception Row -->
<tr *ngIf="log.exception" class="exception-row">
<td [attr.colspan]="hasJobInfo() && hasInstanceInfo() ? 6 : (hasJobInfo() || hasInstanceInfo() ? 5 : 4)" class="exception-cell">
<div class="exception-content">
<pre>{{ log.exception }}</pre>
</div>
</td>
</tr>
</ng-template>
<!-- Empty State -->
<ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="hasJobInfo() && hasInstanceInfo() ? 6 : (hasJobInfo() || hasInstanceInfo() ? 5 : 4)">
<div class="empty-message">
<i class="pi pi-inbox"></i>
<div class="empty-text" *ngIf="isConnected(); else disconnectedMessage">
No logs found
</div>
<p *ngIf="isConnected()">Waiting for new logs or try adjusting your filters</p>
<ng-template #disconnectedMessage>
<div class="flex flex-column align-items-center gap-3">
<div class="empty-text">Not connected to log hub</div>
<p>Attempting to reconnect to the server...</p>
<p-progressSpinner styleClass="w-3rem h-3rem" strokeWidth="4" fill="var(--surface-ground)" animationDuration=".5s"></p-progressSpinner>
<!-- Main Logs Card -->
<p-card styleClass="logs-card">
<!-- Card Header -->
<ng-template pTemplate="header">
<div class="flex align-items-center justify-content-between p-3 border-bottom-1 surface-border">
<div class="flex align-items-center gap-2">
<h2 class="m-0">Application Logs</h2>
<p-tag
[severity]="isConnected() ? 'success' : 'danger'"
[value]="isConnected() ? 'Connected' : 'Disconnected'"
[pTooltip]="isConnected() ? 'Connected to log hub' : 'Attempting to reconnect...'"
tooltipPosition="right"
></p-tag>
</div>
</ng-template>
<button
pButton
icon="pi pi-refresh"
class="p-button-rounded p-button-text"
(click)="refresh()"
pTooltip="Refresh logs"
[loading]="false"
></button>
</div>
</td>
</tr>
</ng-template>
</p-table>
</p-card>
</ng-template>
<!-- Filters Section -->
<div class="filter-container flex align-items-center justify-content-between flex-wrap gap-3">
<div class="flex align-items-center gap-2 flex-wrap">
<i class="pi pi-search"></i>
<!-- Search Filter -->
<span class="p-input-icon-left">
<input
type="text"
pInputText
[(ngModel)]="searchFilter"
(input)="onSearchChange($event)"
placeholder="Search logs"
[disabled]="!isConnected()"
/>
</span>
<!-- Level Filter -->
<p-select
[options]="levels()"
[(ngModel)]="levelFilter"
placeholder="Filter by level"
[showClear]="true"
(onChange)="onLevelFilterChange($event.value)"
styleClass="level-dropdown"
[disabled]="!isConnected()"
>
</p-select>
<!-- Category Filter -->
<p-select
[options]="categories()"
[(ngModel)]="categoryFilter"
placeholder="Filter by category"
[showClear]="true"
(onChange)="onCategoryFilterChange($event.value)"
styleClass="category-dropdown"
[disabled]="!isConnected()"
>
</p-select>
<!-- Clear Filters Button -->
<button
pButton
icon="pi pi-filter-slash"
label="Clear Filters"
class="p-button-outlined"
(click)="clearFilters()"
[disabled]="!isConnected() || (!levelFilter() && !categoryFilter() && !searchFilter())"
></button>
</div>
<div class="flex align-items-center gap-2 flex-wrap"></div>
</div>
<!-- Logs Table -->
<p-table
[value]="filteredLogs()"
styleClass="p-datatable-sm logs-table"
[scrollable]="true"
scrollHeight="calc(100vh - 500px)"
[paginator]="true"
[rows]="25"
[showCurrentPageReport]="true"
[rowsPerPageOptions]="[10, 25, 50, 100]"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} logs"
[rowHover]="true"
[loading]="!isConnected()"
>
<!-- Table Header -->
<ng-template pTemplate="header">
<tr>
<th style="width: 180px">Timestamp</th>
<th style="width: 100px">Level</th>
<th style="width: 120px">Category</th>
<th>Message</th>
<th style="width: 120px" *ngIf="hasJobInfo()">Job Name</th>
<th style="width: 120px" *ngIf="hasInstanceInfo()">Instance</th>
</tr>
</ng-template>
<!-- Table Body -->
<ng-template pTemplate="body" let-log>
<!-- Log Entry Row -->
<tr
[ngClass]="{
'error-row': log.level === 'Error' || log.level === 'Fatal' || log.level === 'Critical',
'warning-row': log.level === 'Warning'
}"
>
<td>
<span class="font-medium">{{ log.timestamp | date : "yyyy-MM-dd" }}</span>
<div class="text-sm text-color-secondary">
{{ log.timestamp | date : "HH:mm:ss" }}
</div>
</td>
<td>
<p-tag [severity]="getSeverity(log.level)" [value]="log.level"></p-tag>
</td>
<td>
<span class="log-category">{{ log.category }}</span>
</td>
<td>
<span
[pTooltip]="log.exception ? 'Click to view stack trace' : undefined"
tooltipPosition="top"
[tooltipStyleClass]="log.exception ? 'visible' : 'invisible'"
>
{{ log.message }}
</span>
</td>
<td *ngIf="hasJobInfo()" class="font-medium">{{ log.jobName }}</td>
<td *ngIf="hasInstanceInfo()" class="text-sm">
{{ log.instanceName }}
</td>
</tr>
<!-- Exception Row -->
<tr *ngIf="log.exception" class="exception-row">
<td
[attr.colspan]="
hasJobInfo() && hasInstanceInfo() ? 6 : hasJobInfo() || hasInstanceInfo() ? 5 : 4
"
class="exception-cell"
>
<div class="exception-content">
<pre>{{ log.exception }}</pre>
</div>
</td>
</tr>
</ng-template>
<!-- Empty State -->
<ng-template pTemplate="emptymessage">
<tr>
<td
[attr.colspan]="
hasJobInfo() && hasInstanceInfo() ? 6 : hasJobInfo() || hasInstanceInfo() ? 5 : 4
"
>
<div class="empty-message">
<i class="pi pi-inbox"></i>
<div class="empty-text" *ngIf="isConnected(); else disconnectedMessage">No logs found</div>
<p *ngIf="isConnected()">Waiting for new logs or try adjusting your filters</p>
<ng-template #disconnectedMessage>
<div class="flex flex-column align-items-center gap-3">
<div class="empty-text">Not connected to log hub</div>
<p>Attempting to reconnect to the server...</p>
<p-progressSpinner
styleClass="w-3rem h-3rem"
strokeWidth="4"
fill="var(--surface-ground)"
animationDuration=".5s"
></p-progressSpinner>
</div>
</ng-template>
</div>
</td>
</tr>
</ng-template>
</p-table>
</p-card>
</div>

View File

@@ -13,101 +13,7 @@
}
::ng-deep {
/* Card styling - add subtle animation on hover */
.p-card {
height: 100%;
display: flex;
flex-direction: column;
transition: box-shadow 0.3s;
.p-card-content {
flex: 1;
padding: 0 !important;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
/* Table styling - improved for readability */
.p-datatable {
flex: 1;
display: flex;
flex-direction: column;
.p-datatable-wrapper {
flex: 1;
}
.p-datatable-header {
background-color: var(--surface-card);
padding: 1rem;
border-bottom: 1px solid var(--surface-border);
}
.p-datatable-thead > tr > th {
background-color: var(--surface-section);
color: var(--text-color);
border-color: var(--surface-border);
padding: 0.75rem 1rem;
font-weight: 600;
position: sticky;
top: 0;
z-index: 1;
transition: background-color 0.2s;
&:hover {
background-color: var(--surface-hover);
}
}
.p-datatable-tbody {
> tr {
transition: background-color 0.2s;
border-radius: var(--border-radius);
&:hover {
background-color: var(--surface-hover);
}
&:nth-child(even) {
background-color: var(--surface-ground);
}
> td {
padding: 0.75rem 1rem;
border-color: var(--surface-border);
transition: background-color 0.2s;
}
/* Highlight rows with errors */
&.error-row > td:first-child {
border-left: 4px solid var(--red-500);
}
&.warning-row > td:first-child {
border-left: 4px solid var(--yellow-500);
}
}
}
.p-paginator {
background-color: var(--surface-section);
border-color: var(--surface-border);
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
}
/* Exception handling with improved styling */
.exception-row {
background-color: var(--surface-hover) !important;
transition: max-height 0.3s;
overflow: hidden;
}
.exception-cell {
padding: 0 !important;
}
@@ -127,143 +33,6 @@
transition: all 0.3s;
}
/* Tag styling with improved visibility */
.p-tag {
font-weight: 600;
border-radius: var(--border-radius);
transition: all 0.2s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
&.p-tag-danger {
background-color: var(--red-500);
color: var(--red-50);
}
&.p-tag-warning {
background-color: var(--yellow-500);
color: var(--yellow-900);
}
&.p-tag-info {
background-color: var(--blue-500);
color: var(--blue-50);
}
&.p-tag-success {
background-color: var(--green-500);
color: var(--green-50);
}
}
/* Form controls styling */
.p-dropdown {
min-width: 180px;
border-radius: var(--border-radius);
transition: box-shadow 0.2s, border-color 0.2s;
&:hover, &.p-focus {
border-color: var(--primary-color);
}
&.p-focus {
box-shadow: 0 0 0 1px var(--primary-100);
}
.p-dropdown-label {
padding: 0.5rem 0.75rem;
}
/* Fix for consistent height dropdowns */
&.fixed-height-dropdown {
.p-dropdown-label {
display: flex;
align-items: center;
height: 2.5rem; /* Fixed height for consistency */
}
.level-indicator {
display: flex;
align-items: center;
height: 100%;
}
/* Ensure tags inside dropdowns maintain proper sizing */
.level-tag {
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
height: auto;
line-height: 1;
}
}
}
.p-input-icon-left {
width: 100%;
max-width: 300px;
input {
border-radius: var(--border-radius);
width: 100%;
transition: box-shadow 0.2s, border-color 0.2s;
padding: 0.5rem 0.75rem 0.5rem 2rem;
&:hover, &:focus {
border-color: var(--primary-color);
}
&:focus {
box-shadow: 0 0 0 1px var(--primary-100);
}
}
i {
left: 0.75rem;
color: var(--text-color-secondary);
}
}
/* Button styling with micro-interactions */
.p-button {
border-radius: var(--border-radius);
transition: background-color 0.2s, color 0.2s, border-color 0.2s, transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
}
&.p-button-outlined {
background-color: transparent;
color: var(--primary-color);
border-color: var(--primary-color);
&:hover {
background-color: var(--primary-50);
}
}
&.p-button-text {
&:hover {
background-color: var(--surface-hover);
}
}
&.p-button-rounded {
&:hover {
transform: translateY(-1px) rotate(15deg);
}
}
}
/* Empty state styling */
.empty-message {
padding: 2rem;
@@ -288,37 +57,4 @@
}
}
}
}
/* Responsive adjustments */
@media screen and (max-width: 768px) {
.logs-container {
height: calc(100vh - 7rem);
.filter-container {
flex-direction: column;
gap: 1rem;
> div {
width: 100%;
.p-dropdown, .p-input-icon-left {
width: 100%;
max-width: 100%;
}
}
}
::ng-deep {
.p-datatable {
.p-datatable-thead > tr > th {
padding: 0.5rem;
}
.p-datatable-tbody > tr > td {
padding: 0.5rem;
}
}
}
}
}

View File

@@ -7,7 +7,7 @@ import { Subject, takeUntil } from 'rxjs';
import { TableModule } from 'primeng/table';
import { InputTextModule } from 'primeng/inputtext';
import { ButtonModule } from 'primeng/button';
import { DropdownModule } from 'primeng/dropdown';
import { SelectModule } from 'primeng/select';
import { TagModule } from 'primeng/tag';
import { CardModule } from 'primeng/card';
import { ToolbarModule } from 'primeng/toolbar';
@@ -29,7 +29,7 @@ import { LogEntry } from '../../core/models/signalr.models';
TableModule,
InputTextModule,
ButtonModule,
DropdownModule,
SelectModule,
TagModule,
CardModule,
ToolbarModule,
@@ -51,7 +51,7 @@ export class LogsViewerComponent implements OnInit, OnDestroy {
// Filter state
levelFilter = signal<string | null>(null);
categoryFilter = signal<string | null>(null);
searchFilter = '';
searchFilter = signal<string>('');
// Computed values
filteredLogs = computed(() => {
@@ -66,7 +66,7 @@ export class LogsViewerComponent implements OnInit, OnDestroy {
}
if (this.searchFilter) {
const search = this.searchFilter.toLowerCase();
const search = this.searchFilter().toLowerCase();
filtered = filtered.filter(log =>
log.message.toLowerCase().includes(search) ||
(log.exception && log.exception.toLowerCase().includes(search)));
@@ -112,22 +112,22 @@ export class LogsViewerComponent implements OnInit, OnDestroy {
this.destroy$.complete();
}
onLevelFilterChange(level: string | null): void {
onLevelFilterChange(level: string): void {
this.levelFilter.set(level);
}
onCategoryFilterChange(category: string | null): void {
onCategoryFilterChange(category: string): void {
this.categoryFilter.set(category);
}
onSearchChange(event: Event): void {
this.searchFilter = (event.target as HTMLInputElement).value;
this.searchFilter.set((event.target as HTMLInputElement).value);
}
clearFilters(): void {
this.levelFilter.set(null);
this.categoryFilter.set(null);
this.searchFilter = '';
this.searchFilter.set('');
}
getSeverity(level: string): string {