feat: implement system log detail view with dynamic status badges and optimize log file retrieval performance

This commit is contained in:
Yoga Pangestu 2026-04-22 20:44:39 +07:00
parent 37960a9dc5
commit 2552ca9511
7 changed files with 38 additions and 48 deletions

View File

@ -10,14 +10,10 @@
class LogController extends Controller
{
/**
* Display a listing of log entries.
*/
public function index(Request $request): Response
{
$logFiles = $this->getLogFiles();
// Default to the latest log file if not specified or doesn't exist
$selectedFile = $request->input('file');
if (! $selectedFile || ! in_array($selectedFile, $logFiles)) {
@ -33,9 +29,6 @@ public function index(Request $request): Response
]);
}
/**
* Get all .log files from storage/logs.
*/
private function getLogFiles(): array
{
$path = storage_path('logs');
@ -46,16 +39,14 @@ private function getLogFiles(): array
$files = File::files($path);
return collect($files)
->map(fn ($file) => $file->getFilename())
->filter(fn ($filename) => str_ends_with($filename, '.log'))
->sortDesc() // Latest dates first
->map(fn($file) => $file->getFilename())
->filter(fn($filename) => str_ends_with($filename, '.log'))
->sortDesc()
->take(10)
->values()
->toArray();
}
/**
* Parse a log file into an array of entries.
*/
private function parseLogFile($filename): array
{
$path = storage_path("logs/{$filename}");
@ -63,12 +54,8 @@ private function parseLogFile($filename): array
return [];
}
// Limit file reading to prevent memory issues with huge logs
// For simplicity, we read the whole file here, but in production consider chunking.
$content = File::get($path);
// Pattern: [timestamp] environment.LEVEL: Message
// Handles multi-line messages (like stack traces) using lookahead
$pattern = '/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+)\.(\w+): ([\s\S]*?)(?=\n^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]|$)/m';
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
@ -84,7 +71,6 @@ private function parseLogFile($filename): array
];
}
// Return latest logs first
return array_reverse($logs);
}
}

View File

@ -0,0 +1,18 @@
export const getLevelColor = (level: string) => {
switch (level.toUpperCase()) {
case 'EMERGENCY':
case 'ALERT':
case 'CRITICAL':
case 'ERROR':
return 'destructive';
case 'WARNING':
return 'outline';
case 'NOTICE':
case 'INFO':
return 'secondary';
case 'DEBUG':
return 'outline';
default:
return 'default';
}
}

View File

@ -2,7 +2,8 @@ import { Head, router } from '@inertiajs/react';
import { Card, CardContent } from '@/components/ui/card';
import { DataTable } from '@/components/data-table';
import { useState } from 'react';
import { getColumns, SystemLog } from './partials/columns';
import { getColumns } from './partials/columns';
import { SystemLog } from '@/types';
import system from '@/routes/system';
import {
Select,
@ -78,6 +79,7 @@ export default function SystemLogIndex({ logFiles, selectedFile, logs }: Props)
]
}
]}
showSelection={false}
/>
</CardContent>
</Card>

View File

@ -4,38 +4,13 @@ import { Badge } from '@/components/ui/badge';
import { Eye } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
export interface SystemLog {
id: number;
timestamp: string;
env: string;
level: string;
message: string;
}
import { SystemLog } from '@/types';
import { getLevelColor } from '@/lib/log-helpers';
interface ColumnProps {
onView: (log: SystemLog) => void;
}
const getLevelColor = (level: string) => {
switch (level.toUpperCase()) {
case 'EMERGENCY':
case 'ALERT':
case 'CRITICAL':
case 'ERROR':
return 'destructive';
case 'WARNING':
return 'outline'; // Fallback if no warning variant
case 'NOTICE':
case 'INFO':
return 'secondary';
case 'DEBUG':
return 'outline';
default:
return 'default';
}
}
export const getColumns = ({ onView }: ColumnProps): ColumnDef<SystemLog>[] => [
{
accessorKey: "timestamp",

View File

@ -5,7 +5,8 @@ import {
DialogTitle,
} from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
import { SystemLog } from "./columns"
import { SystemLog } from "@/types"
import { getLevelColor } from "@/lib/log-helpers"
interface Props {
log: SystemLog | null;
@ -21,7 +22,7 @@ export function LogDetailModal({ log, onClose }: Props) {
<DialogHeader>
<div className="flex items-center gap-3">
<DialogTitle>Detail Log</DialogTitle>
<Badge variant="outline">{log.level}</Badge>
<Badge variant={getLevelColor(log.level)}>{log.level}</Badge>
<span className="text-sm text-muted-foreground">{log.timestamp}</span>
</div>
</DialogHeader>

View File

@ -8,4 +8,5 @@ export type * from './payroll';
export type * from './purchase';
export type * from './general-setting';
export type * from './activity';
export type * from './system-log';

View File

@ -0,0 +1,7 @@
export interface SystemLog {
id: number;
timestamp: string;
env: string;
level: string;
message: string;
}