220 lines
7.4 KiB
TypeScript
220 lines
7.4 KiB
TypeScript
import { Head, router } from '@inertiajs/react';
|
|
import { format } from 'date-fns';
|
|
import { Download } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import type { PaginationState } from '@/components/data-table';
|
|
import { DataTable } from '@/components/data-table';
|
|
import type { FilterField } from '@/components/filter-dialog';
|
|
import { FilterDialog } from '@/components/filter-dialog';
|
|
import { PageHeader } from '@/components/page-header';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { useServerTable } from '@/hooks/use-server-table';
|
|
import { index as logsIndex, download } from '@/routes/admin/developer/logs';
|
|
import type { LogEntry, LogFile } from '@/types/log-entry';
|
|
import { LogLevels } from '@/types/log-entry';
|
|
import { createLogColumns } from './columns';
|
|
|
|
type Props = {
|
|
entries: {
|
|
data: LogEntry[];
|
|
current_page: number;
|
|
last_page: number;
|
|
per_page: number;
|
|
total: number;
|
|
} | null;
|
|
files: LogFile[];
|
|
selectedFile: string | null;
|
|
filters: {
|
|
file?: string;
|
|
level?: string;
|
|
};
|
|
};
|
|
|
|
function formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) {
|
|
return `${bytes} B`;
|
|
}
|
|
|
|
if (bytes < 1024 * 1024) {
|
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
}
|
|
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
export default function LogIndex({
|
|
entries,
|
|
files,
|
|
selectedFile,
|
|
filters,
|
|
}: Props) {
|
|
const [detail, setDetail] = useState<LogEntry | null>(null);
|
|
|
|
const filterFields: FilterField[] = [
|
|
{
|
|
key: 'level',
|
|
label: 'Level',
|
|
options: LogLevels.map((level) => ({
|
|
value: level,
|
|
label: level,
|
|
})),
|
|
},
|
|
];
|
|
|
|
const pagination: PaginationState = {
|
|
current_page: entries?.current_page ?? 1,
|
|
last_page: entries?.last_page ?? 1,
|
|
per_page: entries?.per_page ?? 25,
|
|
total: entries?.total ?? 0,
|
|
};
|
|
|
|
const {
|
|
search,
|
|
handlePageChange,
|
|
handlePerPageChange,
|
|
handleSearchChange,
|
|
applyFilters,
|
|
} = useServerTable({
|
|
route: () => logsIndex.url(),
|
|
pagination,
|
|
filters,
|
|
});
|
|
|
|
function handleFileChange(file: string) {
|
|
router.get(
|
|
logsIndex.url(),
|
|
{ file, level: filters.level },
|
|
{ preserveState: true, replace: true },
|
|
);
|
|
}
|
|
|
|
const columns = createLogColumns({
|
|
handleViewDetail: (entry) => setDetail(entry),
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<Head title="Logs" />
|
|
|
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
<PageHeader
|
|
title="Logs"
|
|
actions={
|
|
selectedFile && (
|
|
<Button variant="outline" asChild>
|
|
<a
|
|
href={download.url({
|
|
query: { file: selectedFile },
|
|
})}
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
Unduh
|
|
</a>
|
|
</Button>
|
|
)
|
|
}
|
|
/>
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<Select
|
|
value={selectedFile ?? undefined}
|
|
onValueChange={handleFileChange}
|
|
>
|
|
<SelectTrigger className="w-full sm:w-80">
|
|
<SelectValue placeholder="Pilih file log" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{files.map((file) => (
|
|
<SelectItem key={file.name} value={file.name}>
|
|
{file.name} ·{' '}
|
|
{formatFileSize(file.size)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{entries ? (
|
|
<DataTable
|
|
columns={columns}
|
|
data={entries.data}
|
|
pagination={pagination}
|
|
onPageChange={handlePageChange}
|
|
onPerPageChange={handlePerPageChange}
|
|
onSearchChange={handleSearchChange}
|
|
searchValue={search}
|
|
searchKey="message"
|
|
searchPlaceholder="Cari pesan log..."
|
|
emptyText="Tidak ada entri log yang cocok."
|
|
toolbar={
|
|
<FilterDialog
|
|
fields={filterFields}
|
|
activeFilters={filters}
|
|
onApply={applyFilters}
|
|
/>
|
|
}
|
|
/>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">
|
|
Belum ada file log yang tersedia.
|
|
</p>
|
|
)}
|
|
|
|
<Dialog
|
|
open={detail !== null}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setDetail(null);
|
|
}
|
|
}}
|
|
>
|
|
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-3xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
{detail?.level && (
|
|
<Badge variant="outline">
|
|
{detail.level}
|
|
</Badge>
|
|
)}
|
|
<span>Detail Log</span>
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{detail &&
|
|
!Number.isNaN(
|
|
new Date(
|
|
detail.timestamp.replace(' ', 'T'),
|
|
).getTime(),
|
|
) &&
|
|
format(
|
|
new Date(
|
|
detail.timestamp.replace(' ', 'T'),
|
|
),
|
|
'd MMM yyyy, HH:mm:ss',
|
|
)}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<pre className="overflow-y-auto rounded-md bg-muted p-3 font-mono text-xs break-all whitespace-pre-wrap">
|
|
{detail?.raw}
|
|
</pre>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|