- 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>
172 lines
5.8 KiB
TypeScript
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>
|
|
)
|
|
}
|