This commit is contained in:
Flaminel
2025-05-22 15:13:25 +03:00
parent 0bd4e77e9d
commit 91bd85708c
14 changed files with 1514 additions and 154 deletions

View File

@@ -10,5 +10,5 @@ import { MainLayoutComponent } from './layout/main-layout/main-layout.component'
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'CleanupErr';
title = 'Cleanuparr';
}

View File

@@ -7,6 +7,7 @@ import Aura from '@primeng/themes/aura';
import { routes } from './app.routes';
import { provideServiceWorker } from '@angular/service-worker';
import Noir from './app.preset';
export const appConfig: ApplicationConfig = {
providers: [
@@ -16,7 +17,7 @@ export const appConfig: ApplicationConfig = {
provideAnimationsAsync(),
providePrimeNG({
theme: {
preset: Aura
preset: Noir
}
}),
provideServiceWorker('ngsw-worker.js', {

View File

@@ -0,0 +1,53 @@
import { definePreset } from '@primeng/themes';
import Aura from '@primeng/themes/aura';
const Noir = definePreset(Aura, {
semantic: {
// primary: {
// 50: '{violet.50}',
// 100: '{violet.100}',
// 200: '{violet.200}',
// 300: '{violet.300}',
// 400: '{violet.400}',
// 500: '{violet.500}',
// 600: '{violet.600}',
// 700: '{violet.700}',
// 800: '{violet.800}',
// 900: '{violet.900}',
// 950: '{violet.950}'
// },
// colorScheme: {
// light: {
// primary: {
// color: '{violet.950}',
// inverseColor: '#ffffff',
// hoverColor: '{violet.900}',
// activeColor: '{violet.800}'
// },
// highlight: {
// background: '{violet.950}',
// focusBackground: '{violet.700}',
// color: '#ffffff',
// focusColor: '#ffffff'
// }
// },
// dark: {
// primary: {
// color: '{violet.50}',
// inverseColor: '{violet.950}',
// hoverColor: '{violet.100}',
// activeColor: '{violet.200}'
// },
// highlight: {
// background: 'rgba(250, 250, 250, .16)',
// focusBackground: 'rgba(250, 250, 250, .24)',
// color: 'rgba(255,255,255,.87)',
// focusColor: 'rgba(255,255,255,.87)'
// }
// }
// }
}
});
export default Noir;

View File

@@ -1,33 +1,190 @@
<div class="dashboard-container">
<h1>Dashboard</h1>
<div class="dashboard-container content-section">
<div class="flex align-items-center justify-content-between mb-4">
<h1 class="m-0">Dashboard</h1>
<p-button icon="pi pi-sync" styleClass="p-button-text" label="Refresh" (onClick)="refreshDashboard()"></p-button>
</div>
<div class="grid">
<!-- Status Overview -->
<div class="grid grid-container">
<div class="col-12 md:col-6 xl:col-3">
<div class="overview-box bg-primary">
<div class="overview-icon">
<i class="pi pi-bolt"></i>
</div>
<div class="overview-data">
<div class="overview-value">System Status</div>
<div class="overview-label">Operational</div>
</div>
</div>
</div>
<div class="col-12 md:col-6 xl:col-3">
<div class="overview-box bg-success">
<div class="overview-icon">
<i class="pi pi-server"></i>
</div>
<div class="overview-data">
<div class="overview-value">Services</div>
<div class="overview-label">All Running</div>
</div>
</div>
</div>
<div class="col-12 md:col-6 xl:col-3">
<div class="overview-box bg-warning">
<div class="overview-icon">
<i class="pi pi-exclamation-triangle"></i>
</div>
<div class="overview-data">
<div class="overview-value">Warnings</div>
<div class="overview-label">0 Warnings</div>
</div>
</div>
</div>
<div class="col-12 md:col-6 xl:col-3">
<div class="overview-box bg-danger">
<div class="overview-icon">
<i class="pi pi-times-circle"></i>
</div>
<div class="overview-data">
<div class="overview-value">Errors</div>
<div class="overview-label">0 Errors</div>
</div>
</div>
</div>
</div>
<!-- Main Cards -->
<div class="grid grid-container">
<div class="col-12 md:col-6 lg:col-4">
<p-card header="Logs Summary" subheader="Recent logging activity">
<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>
<h2 class="card-title m-0">Logs Summary</h2>
<span class="card-subtitle">Recent logging activity</span>
</div>
<i class="pi pi-list text-xl"></i>
</div>
</ng-template>
<div class="card-content">
<p>Monitor your application logs and error statuses.</p>
<div class="mb-3">
<div class="flex align-items-center mb-2">
<i class="pi pi-check-circle text-success mr-2"></i>
<span class="font-medium">Info Logs</span>
<span class="ml-auto font-medium">24</span>
</div>
<p-progressBar [value]="30" styleClass="mb-2" [style]="{'height': '8px'}"></p-progressBar>
</div>
<div class="mb-3">
<div class="flex align-items-center mb-2">
<i class="pi pi-exclamation-triangle text-warning mr-2"></i>
<span class="font-medium">Warning Logs</span>
<span class="ml-auto font-medium">10</span>
</div>
<p-progressBar [value]="12" styleClass="mb-2" [style]="{'height': '8px'}" [styleClass]="'warning-progress'"></p-progressBar>
</div>
<div class="mb-3">
<div class="flex align-items-center mb-2">
<i class="pi pi-times-circle text-danger mr-2"></i>
<span class="font-medium">Error Logs</span>
<span class="ml-auto font-medium">3</span>
</div>
<p-progressBar [value]="5" styleClass="mb-2" [style]="{'height': '8px'}" [styleClass]="'danger-progress'"></p-progressBar>
</div>
<div class="card-footer">
<button pButton label="View Logs" icon="pi pi-list" routerLink="/logs"></button>
<button pButton label="View All Logs" icon="pi pi-arrow-right" routerLink="/logs" iconPos="right"></button>
</div>
</div>
</p-card>
</div>
<div class="col-12 md:col-6 lg:col-4">
<p-card header="System Status" subheader="Current health status">
<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>
<h2 class="card-title m-0">System Status</h2>
<span class="card-subtitle">Current health status</span>
</div>
<i class="pi pi-shield text-xl"></i>
</div>
</ng-template>
<div class="card-content">
<p>All systems operational</p>
<div class="status-item mb-3 flex align-items-center">
<span class="status-indicator active mr-2"></span>
<div class="flex-1">
<div class="font-medium">API Service</div>
<div class="text-sm text-color-secondary">Running since 2 days</div>
</div>
<p-tag severity="success" value="Active"></p-tag>
</div>
<div class="status-item mb-3 flex align-items-center">
<span class="status-indicator active mr-2"></span>
<div class="flex-1">
<div class="font-medium">Log Hub</div>
<div class="text-sm text-color-secondary">Connected</div>
</div>
<p-tag severity="success" value="Active"></p-tag>
</div>
<div class="status-item mb-3 flex align-items-center">
<span class="status-indicator active mr-2"></span>
<div class="flex-1">
<div class="font-medium">Database</div>
<div class="text-sm text-color-secondary">Operational</div>
</div>
<p-tag severity="success" value="Active"></p-tag>
</div>
<div class="card-footer">
<button pButton label="Details" icon="pi pi-server" class="p-button-outlined"></button>
<button pButton label="System Details" icon="pi pi-server" class="p-button-outlined"></button>
</div>
</div>
</p-card>
</div>
<div class="col-12 md:col-6 lg:col-4">
<p-card header="Available Updates" subheader="System updates">
<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>
<h2 class="card-title m-0">Updates & Info</h2>
<span class="card-subtitle">System updates</span>
</div>
<i class="pi pi-info-circle text-xl"></i>
</div>
</ng-template>
<div class="card-content">
<p>Your application is up to date.</p>
<div class="update-item mb-3 p-2 border-round surface-hover flex align-items-center">
<i class="pi pi-check-circle text-success mr-2"></i>
<div>
<div class="font-medium">System up to date</div>
<div class="text-sm text-color-secondary">Latest version installed</div>
</div>
</div>
<div class="update-item mb-3 p-2 border-round surface-hover flex align-items-center">
<i class="pi pi-calendar mr-2"></i>
<div>
<div class="font-medium">Last scan</div>
<div class="text-sm text-color-secondary">Today at 10:30 AM</div>
</div>
</div>
<div class="update-item mb-3 p-2 border-round surface-hover flex align-items-center">
<i class="pi pi-history mr-2"></i>
<div>
<div class="font-medium">Next scan scheduled</div>
<div class="text-sm text-color-secondary">Tomorrow at 02:00 AM</div>
</div>
</div>
<div class="card-footer">
<button pButton label="Check for Updates" icon="pi pi-refresh" class="p-button-secondary"></button>
</div>
@@ -35,4 +192,28 @@
</p-card>
</div>
</div>
<!-- Recent Activity Section -->
<div class="grid">
<div class="col-12">
<p-card styleClass="dashboard-card" header="Recent Activity">
<p-timeline [value]="activityItems" styleClass="activity-timeline">
<ng-template pTemplate="content" let-item>
<div class="activity-item p-2">
<div class="font-medium">{{item.title}}</div>
<div class="text-sm text-color-secondary">{{item.description}}</div>
<div class="text-xs mt-2">{{item.time}}</div>
</div>
</ng-template>
<ng-template pTemplate="opposite" let-item>
<div class="activity-icon-container">
<span class="activity-icon" [ngClass]="item.iconClass">
<i [class]="item.icon"></i>
</span>
</div>
</ng-template>
</p-timeline>
</p-card>
</div>
</div>
</div>

View File

@@ -1,37 +1,259 @@
.dashboard-container {
padding: 1.5rem;
h1 {
margin-top: 0;
margin-bottom: 1.5rem;
font-weight: 600;
padding: 0;
.grid-container {
margin-bottom: 2rem;
}
/* Overview Boxes */
.overview-box {
border-radius: var(--border-radius);
padding: 1.5rem;
display: flex;
align-items: center;
color: white;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s, box-shadow 0.3s;
height: 100%;
&:hover {
transform: translateY(-5px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
.overview-icon {
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
i {
font-size: 1.5rem;
}
}
.overview-data {
flex: 1;
.overview-value {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.overview-label {
font-size: 0.9rem;
opacity: 0.9;
}
}
&.bg-primary {
background-color: var(--primary-color);
background-image: linear-gradient(30deg, var(--primary-600), var(--primary-400));
}
&.bg-success {
background-color: var(--green-500);
background-image: linear-gradient(30deg, var(--green-600), var(--green-400));
}
&.bg-warning {
background-color: var(--yellow-500);
background-image: linear-gradient(30deg, var(--yellow-600), var(--yellow-400));
color: var(--yellow-900);
}
&.bg-danger {
background-color: var(--red-500);
background-image: linear-gradient(30deg, var(--red-600), var(--red-400));
}
}
/* Card styling */
::ng-deep {
.dashboard-card {
height: 100%;
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-3px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
.p-card-header {
padding: 0;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.card-subtitle {
color: var(--text-color-secondary);
font-size: 0.875rem;
}
.p-card-body {
height: 100%;
display: flex;
flex-direction: column;
padding: 0;
}
.p-card-content {
flex-grow: 1;
padding: 1.5rem;
}
}
/* Progress bar styling */
.p-progressbar {
height: 8px;
background-color: var(--surface-200);
border-radius: 4px;
overflow: hidden;
.p-progressbar-value {
background-color: var(--primary-color);
transition: width 0.5s ease;
}
&.warning-progress .p-progressbar-value {
background-color: var(--yellow-500);
}
&.danger-progress .p-progressbar-value {
background-color: var(--red-500);
}
}
/* Timeline customizations */
.activity-timeline {
.p-timeline-event-opposite {
flex: 0;
padding: 0 1rem;
}
.p-timeline-event-content {
padding: 0 1rem 1.5rem 1rem;
}
.activity-icon-container {
display: flex;
justify-content: center;
.activity-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
&.bg-primary {
background-color: var(--primary-color);
}
&.bg-success {
background-color: var(--green-500);
}
&.bg-info {
background-color: var(--blue-500);
}
&.bg-warning {
background-color: var(--yellow-500);
}
i {
font-size: 1rem;
}
}
}
.activity-item {
background-color: var(--surface-card);
border-radius: var(--border-radius);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateX(3px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
}
}
}
/* Status indicators */
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
&.active {
background-color: var(--green-500);
box-shadow: 0 0 0 3px rgba(80, 200, 120, 0.2);
}
&.inactive {
background-color: var(--text-color-secondary);
}
&.warning {
background-color: var(--yellow-500);
box-shadow: 0 0 0 3px rgba(255, 213, 79, 0.2);
}
&.error {
background-color: var(--red-500);
box-shadow: 0 0 0 3px rgba(255, 86, 48, 0.2);
}
}
/* Card content styling */
.card-content {
min-height: 100px;
min-height: 180px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-footer {
margin-top: 1rem;
margin-top: 1.5rem;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
::ng-deep .p-card {
height: 100%;
.p-card-body {
height: 100%;
display: flex;
flex-direction: column;
/* Responsive adjustments */
@media screen and (max-width: 768px) {
.overview-box {
margin-bottom: 1rem;
}
.p-card-content {
flex-grow: 1;
padding-bottom: 0;
::ng-deep .activity-timeline {
.p-timeline-event {
flex-direction: column !important;
.p-timeline-event-opposite {
padding: 0;
margin-bottom: 0.5rem;
}
.p-timeline-event-content {
padding: 0 0 1.5rem 2rem;
}
}
}
}
}

View File

@@ -1,21 +1,86 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { CommonModule, NgClass } from '@angular/common';
import { RouterLink } from '@angular/router';
// PrimeNG Components
import { CardModule } from 'primeng/card';
import { ButtonModule } from 'primeng/button';
import { ProgressBarModule } from 'primeng/progressbar';
import { TagModule } from 'primeng/tag';
import { TimelineModule } from 'primeng/timeline';
// Models
interface ActivityItem {
title: string;
description: string;
time: string;
icon: string;
iconClass: string;
}
@Component({
selector: 'app-dashboard-page',
standalone: true,
imports: [
CommonModule,
NgClass,
RouterLink,
CardModule,
ButtonModule
ButtonModule,
ProgressBarModule,
TagModule,
TimelineModule
],
templateUrl: './dashboard-page.component.html',
styleUrl: './dashboard-page.component.scss'
})
export class DashboardPageComponent {
export class DashboardPageComponent implements OnInit {
// Sample activity items for the timeline
activityItems: ActivityItem[] = [];
ngOnInit() {
// Initialize dashboard data
this.initializeActivityData();
}
refreshDashboard() {
console.log('Refreshing dashboard data...');
// Here you would normally fetch new data
// For now, we'll just reinitialize the demo data
this.initializeActivityData();
}
private initializeActivityData() {
// Sample activity data
this.activityItems = [
{
title: 'System started',
description: 'Application services initialized successfully',
time: '10 minutes ago',
icon: 'pi pi-power-off',
iconClass: 'bg-primary'
},
{
title: 'Database backup completed',
description: 'Automatic backup task executed successfully',
time: '2 hours ago',
icon: 'pi pi-database',
iconClass: 'bg-success'
},
{
title: 'Configuration updated',
description: 'System configuration changes applied',
time: 'Yesterday, 14:23',
icon: 'pi pi-cog',
iconClass: 'bg-info'
},
{
title: 'System update available',
description: 'New version 1.2.5 is available for installation',
time: '2 days ago',
icon: 'pi pi-download',
iconClass: 'bg-warning'
}
];
}
}

View File

@@ -9,7 +9,7 @@
aria-label="Toggle menu">
</button>
<div class="logo-container">
<span class="app-name">CleanupErr</span>
<span class="app-name">Cleanuparr</span>
</div>
</div>
<div class="p-toolbar-group-end">
@@ -25,7 +25,7 @@
<p-sidebar [visible]="mobileSidebarVisible()" (visibleChange)="mobileSidebarVisible.set($event)" position="left" styleClass="mobile-sidebar">
<div class="sidebar-content">
<div class="sidebar-header">
<h3>CleanupErr</h3>
<h3>Cleanuparr</h3>
</div>
<div class="sidebar-menu">
<ul class="menu-list">
@@ -47,7 +47,7 @@
<div class="sidebar">
<div class="sidebar-content">
<div class="sidebar-logo">
<span class="app-logo">CE</span>
<span class="app-logo">Logo</span>
</div>
<ul class="menu-list">
<li *ngFor="let item of menuItems" class="menu-item">

View File

@@ -1,31 +1,40 @@
<div class="logs-container">
<p-card *ngIf="!isConnected()" styleClass="mb-3">
<!-- 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>
<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">
<h2>Application Logs</h2>
<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'"></p-tag>
[value]="isConnected() ? 'Connected' : 'Disconnected'"
[pTooltip]="isConnected() ? 'Connected to log hub' : 'Attempting to reconnect...'"
tooltipPosition="right"></p-tag>
</div>
<button pButton icon="pi pi-refresh" class="p-button-rounded p-button-text"
(click)="refresh()" pTooltip="Refresh logs"></button>
(click)="refresh()" pTooltip="Refresh logs"
[loading]="false"></button>
</div>
</ng-template>
<div class="filter-container mb-3 flex align-items-center justify-content-between flex-wrap gap-2">
<div class="flex align-items-center gap-2">
<!-- 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)">
[showClear]="true" (onChange)="onLevelFilterChange($event.value)"
styleClass="level-dropdown" [disabled]="!isConnected()">
<ng-template pTemplate="selectedItem">
<div class="flex align-items-center gap-2" *ngIf="levelFilter()">
<p-tag [severity]="getSeverity(levelFilter() || '')" [value]="levelFilter() || ''">
</p-tag>
<p-tag [severity]="getSeverity(levelFilter() || '')" [value]="levelFilter() || ''"></p-tag>
</div>
</ng-template>
<ng-template let-level pTemplate="item">
@@ -33,36 +42,45 @@
</ng-template>
</p-dropdown>
<!-- Category Filter -->
<p-dropdown [options]="categories()" placeholder="Filter by category"
[showClear]="true" (onChange)="onCategoryFilterChange($event.value)">
[showClear]="true" (onChange)="onCategoryFilterChange($event.value)"
styleClass="category-dropdown" [disabled]="!isConnected()">
</p-dropdown>
</div>
<div class="flex align-items-center gap-2">
<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"/>
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()"></button>
(click)="clearFilters()"
[disabled]="!isConnected() || (!levelFilter() && !categoryFilter() && !searchFilter)"></button>
</div>
</div>
<!-- Logs Table -->
<p-table [value]="filteredLogs()"
styleClass="p-datatable-sm"
styleClass="p-datatable-sm logs-table"
[scrollable]="true"
scrollHeight="70vh"
scrollHeight="calc(100vh - 260px)"
[paginator]="true"
[rows]="25"
[showCurrentPageReport]="true"
[rowsPerPageOptions]="[10, 25, 50, 100]"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} logs">
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>
@@ -74,17 +92,35 @@
</tr>
</ng-template>
<!-- Table Body -->
<ng-template pTemplate="body" let-log>
<tr>
<td>{{ log.timestamp | date: 'medium' }}</td>
<!-- 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>{{ log.category }}</td>
<td>{{ log.message }}</td>
<td *ngIf="hasJobInfo()">{{ log.jobName }}</td>
<td *ngIf="hasInstanceInfo()">{{ log.instanceName }}</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">
@@ -94,15 +130,20 @@
</tr>
</ng-template>
<!-- Empty State -->
<ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="hasJobInfo() && hasInstanceInfo() ? 6 : (hasJobInfo() || hasInstanceInfo() ? 5 : 4)" class="text-center">
<div class="p-5 text-center">
<i class="pi pi-inbox text-5xl mb-3" style="color: var(--text-color-secondary)"></i>
<p class="text-xl" *ngIf="isConnected(); else disconnectedMessage">No logs found. Waiting for new logs...</p>
<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">
<p class="text-xl">Not connected to log hub. Reconnecting...</p>
<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>

View File

@@ -1,24 +1,48 @@
.logs-container {
min-height: 85vh;
margin-bottom: 2rem;
background-color: var(--surface-card);
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
padding: 1.5rem;
height: calc(100vh - 8rem); /* Adjust based on your layout */
display: flex;
flex-direction: column;
/* Filter section styling */
.filter-container {
padding-bottom: 1rem;
padding: 1rem 0;
margin-bottom: 1rem;
border-bottom: 1px solid var(--surface-border);
transition: background-color var(--app-transition-speed);
}
::ng-deep {
// Table overrides
/* 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-section);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
background-color: var(--surface-card);
padding: 1rem;
border-bottom: 1px solid var(--surface-border);
}
.p-datatable-thead > tr > th {
@@ -27,31 +51,61 @@
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 {
&:nth-child(even) {
background-color: var(--surface-ground);
}
.p-datatable-tbody {
> tr {
transition: background-color 0.2s;
border-radius: var(--border-radius);
&:hover {
background-color: var(--surface-hover);
}
> td {
padding: 0.75rem 1rem;
border-color: var(--surface-border);
&: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-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
border-color: var(--surface-border);
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
}
// Exception handling
/* Exception handling with improved styling */
.exception-row {
background-color: var(--surface-hover) !important;
margin: 0;
transition: max-height 0.3s;
overflow: hidden;
}
.exception-cell {
@@ -65,15 +119,25 @@
white-space: pre-wrap;
font-size: 0.85rem;
overflow: auto;
max-height: 200px;
max-height: 300px;
color: var(--text-color);
border-top: 1px dashed var(--surface-border);
border-radius: 0 0 var(--border-radius) var(--border-radius);
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
}
// Tag styling
/* 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);
@@ -96,10 +160,23 @@
}
}
// Dropdowns and inputs
/* 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;
}
}
.p-input-icon-left {
@@ -109,12 +186,37 @@
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
/* 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;
@@ -125,13 +227,75 @@
background-color: var(--primary-50);
}
}
&.p-button-text {
&:hover {
background-color: var(--surface-hover);
}
}
&.p-button-rounded {
&:hover {
transform: translateY(-1px) rotate(15deg);
}
}
}
// Refresh button container
.refresh-container {
margin-bottom: 1rem;
/* Empty state styling */
.empty-message {
padding: 2rem;
text-align: center;
color: var(--text-color-secondary);
display: flex;
justify-content: flex-end;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
color: var(--primary-color);
}
.empty-text {
font-size: 1.2rem;
margin-bottom: 1rem;
}
}
}
}
/* 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

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy, signal, computed, inject } from '@angular/core';
import { DatePipe, NgIf } from '@angular/common';
import { DatePipe, NgIf, NgClass } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Subject, takeUntil } from 'rxjs';
@@ -23,6 +23,7 @@ import { LogEntry } from '../../core/models/signalr.models';
standalone: true,
imports: [
NgIf,
NgClass,
DatePipe,
FormsModule,
TableModule,

View File

@@ -1,41 +1,148 @@
<div class="settings-container">
<h1>Settings</h1>
<div class="settings-container content-section">
<div class="flex align-items-center justify-content-between mb-4">
<h1 class="m-0">Settings</h1>
<div>
<button pButton label="Save Changes" icon="pi pi-save" class="p-button-success mr-2" (click)="saveSettings()"></button>
<button pButton label="Reset to Defaults" icon="pi pi-refresh" class="p-button-secondary p-button-outlined" (click)="resetToDefaults()"></button>
</div>
</div>
<p-accordion [multiple]="true" [activeIndex]="[0]">
<p-accordionTab header="API Connection Settings">
<div class="field-row">
<label for="apiUrl">API URL</label>
<input id="apiUrl" type="text" pInputText [(ngModel)]="apiUrl" />
</div>
</p-accordionTab>
<p-accordionTab header="Logging Settings">
<div class="field-row">
<label for="enableLogs">Enable Logging</label>
<p-inputSwitch id="enableLogs" [(ngModel)]="enableLogs"></p-inputSwitch>
</div>
<div class="grid">
<div class="col-12 md:col-6">
<p-card header="General Settings" styleClass="settings-card mb-4">
<div class="settings-section">
<h3 class="settings-section-title">API Connection</h3>
<div class="field-row">
<label for="apiUrl">API URL</label>
<div class="field-input">
<span class="p-input-icon-left w-full">
<i class="pi pi-link"></i>
<input id="apiUrl" type="text" pInputText [(ngModel)]="apiUrl" class="w-full" placeholder="https://api.example.com" />
</span>
<small class="form-helper-text">The base URL of the Cleanuparr API service</small>
</div>
</div>
<div class="field-row">
<label for="apiKey">API Key</label>
<div class="field-input">
<div class="p-inputgroup">
<input id="apiKey" type="password" pInputText [(ngModel)]="apiKey" class="w-full" placeholder="Enter API key" />
<button type="button" pButton icon="pi pi-eye" class="p-button-outlined"></button>
</div>
<small class="form-helper-text">Required for authenticated API requests</small>
</div>
</div>
<div class="field-row">
<label for="apiTimeout">Timeout (seconds)</label>
<div class="field-input">
<p-inputNumber id="apiTimeout" [(ngModel)]="apiTimeout" [min]="5" [max]="120" [showButtons]="true" suffix=" sec" buttonLayout="horizontal" spinnerMode="horizontal"
inputStyleClass="text-right" [step]="5" decrementButtonClass="p-button-secondary" incrementButtonClass="p-button-secondary"></p-inputNumber>
<small class="form-helper-text">Maximum time to wait for API responses</small>
</div>
</div>
</div>
</p-card>
<div class="field-row">
<label for="enableNotifications">Show Log Notifications</label>
<p-inputSwitch id="enableNotifications" [(ngModel)]="enableNotifications"></p-inputSwitch>
</div>
</p-accordionTab>
<p-card header="UI Preferences" styleClass="settings-card">
<div class="settings-section">
<div class="field-row align-items-center">
<label for="theme">Theme</label>
<div class="field-input">
<div class="flex gap-3 align-items-center">
<p-radioButton id="themeLight" name="theme" value="light" [(ngModel)]="theme" inputId="light"></p-radioButton>
<label for="light" class="mr-5">Light</label>
<p-radioButton id="themeDark" name="theme" value="dark" [(ngModel)]="theme" inputId="dark"></p-radioButton>
<label for="dark">Dark</label>
</div>
</div>
</div>
<div class="field-row align-items-center">
<label for="fontSize">UI Font Size</label>
<div class="field-input">
<p-slider [(ngModel)]="fontSize" [min]="12" [max]="18" class="w-full mb-3"></p-slider>
<div class="text-center font-medium">{{ fontSize }}px</div>
</div>
</div>
</div>
</p-card>
</div>
<p-accordionTab header="UI Settings">
<div class="field-row">
<label for="autoRefresh">Auto-refresh Logs</label>
<p-inputSwitch id="autoRefresh" [(ngModel)]="autoRefresh"></p-inputSwitch>
</div>
<div class="col-12 md:col-6">
<p-card header="Logging Configuration" styleClass="settings-card mb-4">
<div class="settings-section">
<div class="field-row">
<label for="enableLogs">Enable Logging</label>
<div class="field-input">
<p-inputSwitch id="enableLogs" [(ngModel)]="enableLogs"></p-inputSwitch>
<small class="form-helper-text">When enabled, application events will be logged</small>
</div>
</div>
<div class="field-row" [class.field-disabled]="!enableLogs">
<label for="logLevel">Log Level</label>
<div class="field-input">
<p-dropdown id="logLevel" [options]="logLevels" [(ngModel)]="logLevel" optionLabel="label" [disabled]="!enableLogs"
placeholder="Select log level" [showClear]="false" styleClass="w-full">
<ng-template pTemplate="selectedItem">
<div class="flex align-items-center gap-2" *ngIf="logLevel">
<p-tag [severity]="getSeverity(logLevel.value)" [value]="logLevel.label"></p-tag>
</div>
</ng-template>
<ng-template let-level pTemplate="item">
<p-tag [severity]="getSeverity(level.value)" [value]="level.label"></p-tag>
</ng-template>
</p-dropdown>
<small class="form-helper-text">Minimum level of logs to capture</small>
</div>
</div>
<div class="field-row" [class.field-disabled]="!enableLogs">
<label for="enableNotifications">Show Notifications</label>
<div class="field-input">
<p-inputSwitch id="enableNotifications" [(ngModel)]="enableNotifications" [disabled]="!enableLogs"></p-inputSwitch>
<small class="form-helper-text">Show desktop notifications for important logs</small>
</div>
</div>
</div>
</p-card>
<div class="field-row" *ngIf="autoRefresh">
<label for="refreshInterval">Refresh Interval (seconds)</label>
<input id="refreshInterval" type="number" pInputText [(ngModel)]="refreshInterval" min="5" max="300" />
</div>
</p-accordionTab>
</p-accordion>
<p-card header="Log Viewer Settings" styleClass="settings-card">
<div class="settings-section">
<div class="field-row">
<label for="autoRefresh">Auto-refresh Logs</label>
<div class="field-input">
<p-inputSwitch id="autoRefresh" [(ngModel)]="autoRefresh"></p-inputSwitch>
<small class="form-helper-text">Automatically refresh logs at the specified interval</small>
</div>
</div>
<div class="field-row" [class.field-disabled]="!autoRefresh">
<label for="refreshInterval">Refresh Interval</label>
<div class="field-input">
<p-slider [(ngModel)]="refreshInterval" [min]="5" [max]="60" [disabled]="!autoRefresh" class="w-full mb-3"></p-slider>
<div class="text-center font-medium">{{ refreshInterval }} seconds</div>
</div>
</div>
<div class="field-row">
<label for="maxLogEntries">Max Log Entries</label>
<div class="field-input">
<p-dropdown id="maxLogEntries" [options]="maxLogOptions" [(ngModel)]="maxLogEntries"
optionLabel="label" optionValue="value" styleClass="w-full"></p-dropdown>
<small class="form-helper-text">Maximum number of log entries to display at once</small>
</div>
</div>
</div>
</p-card>
</div>
</div>
<div class="button-container">
<button pButton label="Save Changes" icon="pi pi-save" class="p-button-success"></button>
<button pButton label="Reset to Defaults" icon="pi pi-refresh" class="p-button-secondary p-button-outlined"></button>
<div class="fixed bottom-0 right-0 p-3" *ngIf="showSaveNotification">
<p-toast position="bottom-right" key="settings"></p-toast>
</div>
</div>

View File

@@ -1,32 +1,160 @@
.settings-container {
padding: 1.5rem;
padding: 0;
h1 {
margin-top: 0;
margin-bottom: 1.5rem;
.settings-section {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
.settings-section-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--surface-border);
color: var(--primary-color);
}
.field-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0;
margin-bottom: 1.5rem;
label {
font-weight: 500;
margin-right: 1rem;
&:last-child {
margin-bottom: 0;
}
input {
max-width: 300px;
&.field-disabled {
opacity: 0.6;
}
label {
width: 30%;
min-width: 150px;
font-weight: 500;
padding-top: 0.5rem;
}
.field-input {
width: 70%;
.form-helper-text {
display: block;
color: var(--text-color-secondary);
margin-top: 0.5rem;
font-size: 0.85rem;
}
input, .p-dropdown, .p-inputnumber {
width: 100%;
}
}
}
.button-container {
margin-top: 1.5rem;
display: flex;
gap: 0.5rem;
justify-content: flex-end;
/* Card styling */
::ng-deep {
.settings-card {
height: 100%;
transition: box-shadow 0.3s;
&:hover {
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
}
.p-card-header {
padding: 1rem 1.25rem;
background-color: var(--surface-section);
border-bottom: 1px solid var(--surface-border);
font-weight: 600;
}
.p-card-content {
padding: 1.5rem;
}
}
/* Input styling */
.p-inputtext, .p-dropdown, .p-inputnumber {
&:hover {
border-color: var(--primary-color);
}
&:focus, &.p-focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 1px var(--primary-100);
}
}
.p-dropdown {
.p-dropdown-label {
padding: 0.5rem 0.75rem;
}
}
/* Input Group */
.p-inputgroup {
.p-button {
background-color: var(--surface-200);
color: var(--text-color-secondary);
border-color: var(--surface-border);
&:hover {
background-color: var(--surface-300);
color: var(--text-color);
}
}
}
/* Slider styling */
.p-slider {
background-color: var(--surface-200);
.p-slider-handle {
background-color: var(--primary-color);
border-color: var(--primary-color);
&:hover {
background-color: var(--primary-600);
border-color: var(--primary-600);
}
}
.p-slider-range {
background-color: var(--primary-color);
}
}
/* Radio Button */
.p-radiobutton {
.p-radiobutton-box {
&.p-highlight {
border-color: var(--primary-color);
background-color: var(--primary-color);
}
&:not(.p-disabled):hover {
border-color: var(--primary-color);
}
}
}
}
/* Responsive adjustments */
@media screen and (max-width: 768px) {
.field-row {
flex-direction: column;
align-items: flex-start;
label {
width: 100%;
margin-bottom: 0.5rem;
}
.field-input {
width: 100%;
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -7,7 +7,24 @@ import { CardModule } from 'primeng/card';
import { InputTextModule } from 'primeng/inputtext';
import { InputSwitchModule } from 'primeng/inputswitch';
import { ButtonModule } from 'primeng/button';
import { AccordionModule } from 'primeng/accordion';
import { DropdownModule } from 'primeng/dropdown';
import { SliderModule } from 'primeng/slider';
import { RadioButtonModule } from 'primeng/radiobutton';
import { InputNumberModule } from 'primeng/inputnumber';
import { TagModule } from 'primeng/tag';
import { ToastModule } from 'primeng/toast';
import { MessageService } from 'primeng/api';
// Define interfaces for our settings
interface LogLevel {
label: string;
value: string;
}
interface MaxLogOption {
label: string;
value: number;
}
@Component({
selector: 'app-settings-page',
@@ -19,16 +36,148 @@ import { AccordionModule } from 'primeng/accordion';
InputTextModule,
InputSwitchModule,
ButtonModule,
AccordionModule
DropdownModule,
SliderModule,
RadioButtonModule,
InputNumberModule,
TagModule,
ToastModule
],
providers: [MessageService],
templateUrl: './settings-page.component.html',
styleUrl: './settings-page.component.scss'
})
export class SettingsPageComponent {
// Sample settings
export class SettingsPageComponent implements OnInit {
// API Settings
apiUrl = 'http://localhost:5000';
apiKey = '';
apiTimeout = 30;
// UI Settings
theme = 'light';
fontSize = 14;
// Logging Settings
enableLogs = true;
logLevel: LogLevel = { label: 'Information', value: 'Information' };
enableNotifications = true;
// Log Viewer Settings
autoRefresh = false;
refreshInterval = 30;
maxLogEntries: number = 100;
// Available options for select lists
logLevels: LogLevel[] = [
{ label: 'Debug', value: 'Debug' },
{ label: 'Information', value: 'Information' },
{ label: 'Warning', value: 'Warning' },
{ label: 'Error', value: 'Error' },
{ label: 'Critical', value: 'Critical' }
];
maxLogOptions: MaxLogOption[] = [
{ label: '50 entries', value: 50 },
{ label: '100 entries', value: 100 },
{ label: '250 entries', value: 250 },
{ label: '500 entries', value: 500 },
{ label: '1000 entries', value: 1000 }
];
// UI state
showSaveNotification = false;
constructor(private messageService: MessageService) {}
ngOnInit() {
// Load saved settings if available
this.loadSettings();
}
getSeverity(level: string): string {
const normalizedLevel = level?.toLowerCase() || '';
switch (normalizedLevel) {
case 'error':
case 'critical':
case 'fatal':
return 'danger';
case 'warning':
return 'warning';
case 'information':
case 'info':
return 'info';
case 'debug':
case 'trace':
return 'success';
default:
return 'info';
}
}
saveSettings() {
// Here we would normally save the settings to local storage or a backend API
console.log('Saving settings:', {
apiUrl: this.apiUrl,
apiKey: this.apiKey ? '******' : null, // Don't log actual API key for security
apiTimeout: this.apiTimeout,
theme: this.theme,
fontSize: this.fontSize,
enableLogs: this.enableLogs,
logLevel: this.logLevel,
enableNotifications: this.enableNotifications,
autoRefresh: this.autoRefresh,
refreshInterval: this.refreshInterval,
maxLogEntries: this.maxLogEntries
});
// Show success message
this.messageService.add({
key: 'settings',
severity: 'success',
summary: 'Settings Saved',
detail: 'Your settings have been successfully saved.',
life: 3000
});
this.showSaveNotification = true;
setTimeout(() => {
this.showSaveNotification = false;
}, 3000);
}
resetToDefaults() {
// Reset to default values
this.apiUrl = 'http://localhost:5000';
this.apiKey = '';
this.apiTimeout = 30;
this.theme = 'light';
this.fontSize = 14;
this.enableLogs = true;
this.logLevel = { label: 'Information', value: 'Information' };
this.enableNotifications = true;
this.autoRefresh = false;
this.refreshInterval = 30;
this.maxLogEntries = 100;
// Show info message
this.messageService.add({
key: 'settings',
severity: 'info',
summary: 'Settings Reset',
detail: 'All settings have been reset to their default values.',
life: 3000
});
this.showSaveNotification = true;
setTimeout(() => {
this.showSaveNotification = false;
}, 3000);
}
private loadSettings() {
// In a real application, we would load settings from local storage or an API
// For now, we'll just use the default values set in the class
console.log('Loading settings from storage...');
}
}

View File

@@ -1 +1,249 @@
/* You can add global styles to this file, and also import other style files */
/* Global styles and PrimeNG theme setup */
/* Import PrimeNG Theme (Lara Light by default) */
@import "primeicons/primeicons.css";
@import "primeflex/primeflex.css";
/* Global Variables */
:root {
--app-font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--app-primary: var(--primary-color);
--app-card-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
--app-content-padding: 1.5rem;
--app-border-radius: 6px;
--app-transition-speed: 0.3s;
}
/* Base Styles */
html {
font-size: 14px;
}
body {
font-family: var(--app-font-family);
margin: 0;
padding: 0;
background-color: var(--surface-ground);
color: var(--text-color);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 600;
line-height: 1.2;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
a {
color: var(--primary-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
/* Layout Helpers */
.content-section {
padding: var(--app-content-padding);
border-radius: var(--border-radius);
background-color: var(--surface-card);
box-shadow: var(--app-card-shadow);
}
.grid-container {
margin-bottom: 1rem;
}
/* PrimeNG Component Customizations */
/* Card styles */
.p-card {
border-radius: var(--border-radius);
box-shadow: var(--app-card-shadow) !important;
transition: transform var(--app-transition-speed), box-shadow var(--app-transition-speed);
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
}
.p-card-title {
font-size: 1.25rem;
font-weight: 600;
}
.p-card-subtitle {
font-weight: 400;
color: var(--text-color-secondary);
margin-bottom: 1rem;
}
.p-card-content {
padding: 0.5rem 0;
}
.p-card-footer {
padding-top: 1rem;
display: flex;
gap: 0.5rem;
}
}
/* Table styles */
.p-datatable {
.p-datatable-header {
background-color: var(--surface-section);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
padding: 1rem;
}
.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;
}
.p-datatable-tbody > tr {
transition: background-color 0.2s;
&:hover {
background-color: var(--surface-hover);
}
&:nth-child(even) {
background-color: var(--surface-ground);
}
> td {
padding: 0.75rem 1rem;
border-color: var(--surface-border);
}
}
.p-paginator {
background-color: var(--surface-section);
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
border-color: var(--surface-border);
}
}
/* Button styling */
.p-button {
border-radius: var(--border-radius);
transition: background-color 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s;
&:focus {
box-shadow: 0 0 0 2px var(--surface-ground), 0 0 0 4px var(--primary-color), 0 1px 2px rgba(0, 0, 0, 0.2);
}
&.p-button-outlined {
background-color: transparent;
&:hover {
background-color: var(--primary-50);
}
}
&.p-button-text {
background-color: transparent;
color: var(--primary-color);
border-color: transparent;
&:hover {
background-color: var(--surface-hover);
color: var(--primary-600);
}
}
}
/* Form elements */
.form-field {
margin-bottom: 1.5rem;
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
}
.field-row {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
label {
min-width: 200px;
font-weight: 500;
}
}
/* Responsive adjustments */
@media screen and (max-width: 768px) {
html {
font-size: 13px;
}
.p-card .p-card-content {
padding: 0.25rem 0;
}
.field-row {
flex-direction: column;
align-items: flex-start;
label {
margin-bottom: 0.5rem;
}
}
.p-dropdown {
width: 100%;
}
}
.dark-mode {
/* Dark mode specific adjustments */
.p-button {
&.p-button-outlined {
&:hover {
background-color: rgba(255, 255, 255, 0.03);
}
}
}
.content-section {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
}
.p-card {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2) !important;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3) !important;
}
}
}