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
This commit is contained in:
@@ -1,24 +1,25 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Center,
|
||||
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 { TbRefresh } from 'react-icons/tb'
|
||||
import { TbHistory, TbRefresh } from 'react-icons/tb'
|
||||
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
||||
import useSWR from 'swr'
|
||||
import { API_URLS } from '../config/api'
|
||||
@@ -30,8 +31,16 @@ export const Route = createFileRoute('/logs')({
|
||||
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: 'green',
|
||||
LOGIN: 'teal',
|
||||
LOGOUT: 'gray',
|
||||
CREATE: 'blue',
|
||||
UPDATE: 'yellow',
|
||||
@@ -47,9 +56,9 @@ function GlobalLogsPage() {
|
||||
const { data: operatorsData } = useSWR(API_URLS.getLogOperators(), fetcher)
|
||||
|
||||
const operatorOptions = useMemo(() => {
|
||||
if (!operatorsData || !Array.isArray(operatorsData)) return [{ value: 'all', label: 'Semua operator' }]
|
||||
if (!operatorsData || !Array.isArray(operatorsData)) return [{ value: 'all', label: 'All users' }]
|
||||
return [
|
||||
{ value: 'all', label: 'Semua user' },
|
||||
{ value: 'all', label: 'All users' },
|
||||
...operatorsData.map((op: any) => ({ value: op.id, label: op.name })),
|
||||
]
|
||||
}, [operatorsData])
|
||||
@@ -69,88 +78,149 @@ function GlobalLogsPage() {
|
||||
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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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 ? (
|
||||
<Center py="xl"><Loader /></Center>
|
||||
{isLoading && !data ? (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader type="dots" />
|
||||
</Group>
|
||||
) : (
|
||||
<>
|
||||
<Paper withBorder radius="2xl" className="glass" p="md">
|
||||
<Table.ScrollContainer minWidth={600}>
|
||||
<Table striped highlightOnHover fz="xs" style={{ tableLayout: 'fixed', width: '100%' }}>
|
||||
<Table
|
||||
className="data-table"
|
||||
highlightOnHover
|
||||
verticalSpacing="sm"
|
||||
fz="sm"
|
||||
style={{ tableLayout: 'fixed', width: '100%' }}
|
||||
>
|
||||
<colgroup>
|
||||
<col style={{ width: 160 }} />
|
||||
<col style={{ width: 200 }} />
|
||||
<col style={{ width: 100 }} />
|
||||
<col style={{ width: 155 }} />
|
||||
<col style={{ width: 210 }} />
|
||||
<col style={{ width: 105 }} />
|
||||
<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.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' }}>
|
||||
{new Date(log.createdAt).toLocaleString('id-ID')}
|
||||
<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 ? (
|
||||
<div>
|
||||
<Text fw={500} truncate>{log.user.name}</Text>
|
||||
<Text c="dimmed" truncate>{log.user.email}</Text>
|
||||
</div>
|
||||
) : <Text c="dimmed">—</Text>}
|
||||
<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">
|
||||
{log.type}
|
||||
<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>
|
||||
<Text>{log.message}</Text>
|
||||
<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}>
|
||||
<Center py="xl"><Text c="dimmed">Belum ada log aktivitas</Text></Center>
|
||||
<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>
|
||||
)}
|
||||
@@ -158,11 +228,11 @@ function GlobalLogsPage() {
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
{totalPages > 1 && (
|
||||
<Center>
|
||||
<Group justify="center" mt="md">
|
||||
<Pagination total={totalPages} value={page} onChange={setPage} size="sm" />
|
||||
</Center>
|
||||
</Group>
|
||||
)}
|
||||
</>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
Reference in New Issue
Block a user