Apply uniform design system across all routes and components: - Consistent header pattern with gradient-text titles, dimmed subtitles - Loader type="dots" replacing text-based loading states - Icon + text empty/error states with Paper+glass containers - Full STATUS_COLOR/STATUS_LABEL maps for all BugStatus values - dayjs timestamps, Tooltip on action icons, size="sm" on badges/pagination - Modals with overlayProps blur and gradient save buttons - Replace left-border Papers with clean Stack headers - Translate all remaining Indonesian UI strings to English - New monitoring-themed SVG logo and redesigned splash screen
242 lines
8.5 KiB
TypeScript
242 lines
8.5 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Badge,
|
|
Container,
|
|
Group,
|
|
Loader,
|
|
Pagination,
|
|
Paper,
|
|
SegmentedControl,
|
|
Select,
|
|
Stack,
|
|
Table,
|
|
Text,
|
|
Title,
|
|
Tooltip,
|
|
} 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 { TbHistory, 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_LABEL: Record<string, string> = {
|
|
all: 'All',
|
|
LOGIN: 'Login',
|
|
LOGOUT: 'Logout',
|
|
CREATE: 'Create',
|
|
UPDATE: 'Update',
|
|
DELETE: 'Delete',
|
|
}
|
|
const LOG_TYPE_COLOR: Record<string, string> = {
|
|
LOGIN: 'teal',
|
|
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: 'All users' }]
|
|
return [
|
|
{ value: 'all', label: 'All users' },
|
|
...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 gap="xl">
|
|
<Group justify="space-between" align="flex-start">
|
|
<Stack gap={4}>
|
|
<Title order={2} className="gradient-text">Activity Logs</Title>
|
|
<Text size="sm" c="dimmed">
|
|
Track all user actions and system events across the platform.
|
|
</Text>
|
|
</Stack>
|
|
<Tooltip label="Refresh logs" withArrow>
|
|
<ActionIcon
|
|
variant="light"
|
|
color="brand-blue"
|
|
size="lg"
|
|
onClick={() => mutate()}
|
|
loading={isLoading}
|
|
>
|
|
<TbRefresh size={16} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
|
|
<Paper withBorder radius="xl" p="md" className="glass">
|
|
<Group gap="sm" wrap="wrap" align="flex-end">
|
|
<Select
|
|
label="User"
|
|
placeholder="All users"
|
|
value={operatorId}
|
|
onChange={(v) => { setOperatorId(v ?? 'all'); setPage(1) }}
|
|
data={operatorOptions}
|
|
w={200}
|
|
clearable
|
|
size="sm"
|
|
/>
|
|
<DatePickerInput
|
|
type="range"
|
|
label="Date range"
|
|
placeholder="Pick a date range"
|
|
value={dateRange}
|
|
onChange={(v) => { setDateRange(v); setPage(1) }}
|
|
locale="id"
|
|
valueFormat="DD MMM YYYY"
|
|
clearable
|
|
w={280}
|
|
size="sm"
|
|
/>
|
|
<Stack gap={4}>
|
|
<Text size="xs" fw={500} c="dimmed">Action type</Text>
|
|
<SegmentedControl
|
|
value={type}
|
|
onChange={(v) => { setType(v); setPage(1) }}
|
|
size="sm"
|
|
data={LOG_TYPES.map((t) => ({ label: LOG_TYPE_LABEL[t] ?? t, value: t }))}
|
|
/>
|
|
</Stack>
|
|
</Group>
|
|
</Paper>
|
|
|
|
{isLoading && !data ? (
|
|
<Group justify="center" py="xl">
|
|
<Loader type="dots" />
|
|
</Group>
|
|
) : (
|
|
<Paper withBorder radius="2xl" className="glass" p="md">
|
|
<Table.ScrollContainer minWidth={600}>
|
|
<Table
|
|
className="data-table"
|
|
highlightOnHover
|
|
verticalSpacing="sm"
|
|
fz="sm"
|
|
style={{ tableLayout: 'fixed', width: '100%' }}
|
|
>
|
|
<colgroup>
|
|
<col style={{ width: 155 }} />
|
|
<col style={{ width: 210 }} />
|
|
<col style={{ width: 105 }} />
|
|
<col />
|
|
</colgroup>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Timestamp</Table.Th>
|
|
<Table.Th>User</Table.Th>
|
|
<Table.Th>Action</Table.Th>
|
|
<Table.Th>Description</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{logs.map((log: any) => (
|
|
<Table.Tr key={log.id}>
|
|
<Table.Td style={{ whiteSpace: 'nowrap' }}>
|
|
<Stack gap={0}>
|
|
<Text size="xs" fw={500}>
|
|
{dayjs(log.createdAt).locale('id').format('D MMM YYYY')}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{dayjs(log.createdAt).format('HH:mm:ss')}
|
|
</Text>
|
|
</Stack>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
{log.user ? (
|
|
<Stack gap={0}>
|
|
<Text size="sm" fw={600} truncate>{log.user.name}</Text>
|
|
<Text size="xs" c="dimmed" truncate>{log.user.email}</Text>
|
|
</Stack>
|
|
) : (
|
|
<Text c="dimmed" size="sm">—</Text>
|
|
)}
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Badge
|
|
color={LOG_TYPE_COLOR[log.type] ?? 'gray'}
|
|
variant="light"
|
|
size="sm"
|
|
tt="capitalize"
|
|
>
|
|
{LOG_TYPE_LABEL[log.type] ?? log.type}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Tooltip
|
|
label={log.message}
|
|
multiline
|
|
maw={340}
|
|
withArrow
|
|
position="top-start"
|
|
disabled={(log.message?.length ?? 0) < 60}
|
|
>
|
|
<Text size="sm" lineClamp={2} style={{ cursor: 'default' }}>
|
|
{log.message}
|
|
</Text>
|
|
</Tooltip>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
{logs.length === 0 && (
|
|
<Table.Tr>
|
|
<Table.Td colSpan={4}>
|
|
<Stack align="center" gap="xs" py="xl">
|
|
<TbHistory size={32} style={{ opacity: 0.25 }} />
|
|
<Text c="dimmed" size="sm">
|
|
No activity logs found for the selected filters.
|
|
</Text>
|
|
</Stack>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
)}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Table.ScrollContainer>
|
|
{totalPages > 1 && (
|
|
<Group justify="center" mt="md">
|
|
<Pagination total={totalPages} value={page} onChange={setPage} size="sm" />
|
|
</Group>
|
|
)}
|
|
</Paper>
|
|
)}
|
|
</Stack>
|
|
</Container>
|
|
</DashboardLayout>
|
|
)
|
|
}
|