Files
monitoring-app/src/frontend/routes/logs.tsx
amaliadwiy 3c6fac1943 feat: redesign /logs page with table UI and date range filter
- Replace timeline view with table layout (Time, Operator, Type, Message)
- Add date range filter using @mantine/dates DatePickerInput
- Add SegmentedControl for log type filter
- Disable App Logs and Settings menu on /dev
- Remove Activity Logs menu from /dev (moved to /logs)
- Add dateFrom/dateTo query params to /api/logs backend
- Import @mantine/dates/styles.css to fix datepicker styling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 11:27:05 +08:00

172 lines
5.8 KiB
TypeScript

import {
ActionIcon,
Badge,
Center,
Container,
Group,
Loader,
Pagination,
SegmentedControl,
Select,
Stack,
Table,
Text,
Title,
} from '@mantine/core'
import { DatePickerInput, type DatesRangeValue } from '@mantine/dates'
import dayjs from 'dayjs'
import 'dayjs/locale/id'
import { useMemo, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { TbRefresh } from 'react-icons/tb'
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import useSWR from 'swr'
import { API_URLS } from '../config/api'
export const Route = createFileRoute('/logs')({
component: GlobalLogsPage,
})
const fetcher = (url: string) => fetch(url, { credentials: 'include' }).then((r) => r.json())
const LOG_TYPES = ['all', 'LOGIN', 'LOGOUT', 'CREATE', 'UPDATE', 'DELETE'] as const
const LOG_TYPE_COLOR: Record<string, string> = {
LOGIN: 'green',
LOGOUT: 'gray',
CREATE: 'blue',
UPDATE: 'yellow',
DELETE: 'red',
}
function GlobalLogsPage() {
const [type, setType] = useState('all')
const [operatorId, setOperatorId] = useState('all')
const [dateRange, setDateRange] = useState<DatesRangeValue>([null, null])
const [page, setPage] = useState(1)
const { data: operatorsData } = useSWR(API_URLS.getLogOperators(), fetcher)
const operatorOptions = useMemo(() => {
if (!operatorsData || !Array.isArray(operatorsData)) return [{ value: 'all', label: 'Semua operator' }]
return [
{ value: 'all', label: 'Semua user' },
...operatorsData.map((op: any) => ({ value: op.id, label: op.name })),
]
}, [operatorsData])
const dateFrom = dateRange[0] ? dayjs(dateRange[0]).format('YYYY-MM-DD') : undefined
const dateTo = dateRange[1] ? dayjs(dateRange[1]).format('YYYY-MM-DD') : undefined
const { data, isLoading, mutate } = useSWR(
API_URLS.getGlobalLogs(page, '', type, operatorId, dateFrom, dateTo),
fetcher,
{ refreshInterval: 10_000 },
)
const logs: any[] = data?.data ?? []
const totalPages: number = data?.totalPages ?? 1
return (
<DashboardLayout>
<Container size="xl" py="lg">
<Stack>
<Group justify="space-between">
<Title order={3}>Activity Logs</Title>
<ActionIcon variant="subtle" color="gray" onClick={() => mutate()}>
<TbRefresh size={16} />
</ActionIcon>
</Group>
<Group gap="sm" wrap="wrap">
<Select
placeholder="Filter user"
value={operatorId}
onChange={(v) => { setOperatorId(v ?? 'all'); setPage(1) }}
data={operatorOptions}
w={180}
clearable
/>
<DatePickerInput
type="range"
placeholder="Filter tanggal"
value={dateRange}
onChange={(v) => { setDateRange(v); setPage(1) }}
locale="id"
valueFormat="DD MMM YYYY"
clearable
w={300}
/>
<SegmentedControl
value={type}
onChange={(v) => { setType(v); setPage(1) }}
data={LOG_TYPES.map((t) => ({ label: t === 'all' ? 'All' : t, value: t }))}
/>
</Group>
{isLoading ? (
<Center py="xl"><Loader /></Center>
) : (
<>
<Table.ScrollContainer minWidth={600}>
<Table striped highlightOnHover fz="xs" style={{ tableLayout: 'fixed', width: '100%' }}>
<colgroup>
<col style={{ width: 160 }} />
<col style={{ width: 200 }} />
<col style={{ width: 100 }} />
<col />
</colgroup>
<Table.Thead>
<Table.Tr>
<Table.Th>Time</Table.Th>
<Table.Th>Operator</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Message</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{logs.map((log: any) => (
<Table.Tr key={log.id}>
<Table.Td style={{ whiteSpace: 'nowrap' }}>
{new Date(log.createdAt).toLocaleString('id-ID')}
</Table.Td>
<Table.Td>
{log.user ? (
<div>
<Text fw={500} truncate>{log.user.name}</Text>
<Text c="dimmed" truncate>{log.user.email}</Text>
</div>
) : <Text c="dimmed"></Text>}
</Table.Td>
<Table.Td>
<Badge color={LOG_TYPE_COLOR[log.type] ?? 'gray'} variant="light">
{log.type}
</Badge>
</Table.Td>
<Table.Td>
<Text>{log.message}</Text>
</Table.Td>
</Table.Tr>
))}
{logs.length === 0 && (
<Table.Tr>
<Table.Td colSpan={4}>
<Center py="xl"><Text c="dimmed">Belum ada log aktivitas</Text></Center>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
{totalPages > 1 && (
<Center>
<Pagination total={totalPages} value={page} onChange={setPage} size="sm" />
</Center>
)}
</>
)}
</Stack>
</Container>
</DashboardLayout>
)
}