Files
monitoring-app/src/frontend/routes/logs.tsx
amaliadwiy ef852842b4 feat: improve UI/UX consistency across all dashboard pages
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
2026-05-05 12:42:41 +08:00

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>
)
}