Files
monitoring-app/src/frontend/components/ErrorDataTable.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

261 lines
8.7 KiB
TypeScript

import {
Badge,
Box,
Button,
Code,
Divider,
Drawer,
Group,
Loader,
Paper,
ScrollArea,
SimpleGrid,
Stack,
Table,
Text,
ThemeIcon,
Title,
Tooltip,
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import dayjs from 'dayjs'
import { useState } from 'react'
import { TbBug, TbExternalLink, TbHistory, TbMessageReport } from 'react-icons/tb'
export interface ErrorDataTableProps {
appId?: string
}
const STATUS_COLOR: Record<string, string> = {
OPEN: 'red',
IN_PROGRESS: 'blue',
ON_HOLD: 'orange',
RESOLVED: 'teal',
RELEASED: 'green',
CLOSED: 'gray',
}
const STATUS_LABEL: Record<string, string> = {
OPEN: 'Open',
ON_HOLD: 'On Hold',
IN_PROGRESS: 'In Progress',
RESOLVED: 'Resolved',
RELEASED: 'Released',
CLOSED: 'Closed',
}
export function ErrorDataTable({ appId }: ErrorDataTableProps) {
const [opened, { open, close }] = useDisclosure(false)
const [selectedError, setSelectedError] = useState<any>(null)
const [showStackTrace, setShowStackTrace] = useState(false)
const { data: bugsData, isLoading } = useQuery({
queryKey: ['bugs', appId],
queryFn: () => fetch(`/api/bugs?app=${appId || 'all'}&limit=10`).then((r) => r.json()),
})
const bugs = bugsData?.data || []
const handleRowClick = (error: any) => {
setSelectedError(error)
setShowStackTrace(false)
open()
}
return (
<>
<Paper withBorder radius="2xl" className="glass" style={{ overflow: 'hidden' }}>
<Box p="lg" style={{ borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<Group justify="space-between">
<Group gap="sm">
<ThemeIcon variant="light" color="red" size="lg" radius="md">
<TbBug size={20} />
</ThemeIcon>
<Stack gap={0}>
<Text fw={700} size="sm">Latest Error Reports</Text>
<Text size="xs" c="dimmed">Most recent open bugs</Text>
</Stack>
</Group>
<Tooltip label="View all reports" withArrow>
<Button
component={Link}
to={appId ? `/apps/${appId}/errors` : '/bug-reports'}
variant="subtle"
size="compact-sm"
color="blue"
rightSection={<TbExternalLink size={14} />}
>
View All
</Button>
</Tooltip>
</Group>
</Box>
<ScrollArea>
<Table verticalSpacing="sm" highlightOnHover className="data-table">
<Table.Thead>
<Table.Tr>
<Table.Th px="lg">Error Description</Table.Th>
<Table.Th>Reporter</Table.Th>
<Table.Th>Version</Table.Th>
<Table.Th>Reported</Table.Th>
<Table.Th pr="lg">Status</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{isLoading ? (
<Table.Tr>
<Table.Td colSpan={5}>
<Group justify="center" py="xl">
<Loader size="sm" type="dots" />
</Group>
</Table.Td>
</Table.Tr>
) : bugs.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={5}>
<Stack align="center" gap="xs" py="xl">
<TbBug size={32} style={{ opacity: 0.25 }} />
<Text size="sm" c="dimmed">No error reports found.</Text>
</Stack>
</Table.Td>
</Table.Tr>
) : bugs.map((error: any) => (
<Table.Tr
key={error.id}
onClick={() => handleRowClick(error)}
style={{ cursor: 'pointer' }}
>
<Table.Td px="lg">
<Text size="sm" fw={600} lineClamp={1}>{error.description}</Text>
</Table.Td>
<Table.Td>
<Badge variant="light" color="brand-blue" size="sm">
{error.user?.name || error.userId || 'System'}
</Badge>
</Table.Td>
<Table.Td>
<Badge variant="light" color="gray" size="sm">
v{error.affectedVersion || 'N/A'}
</Badge>
</Table.Td>
<Table.Td>
<Group gap={4}>
<TbHistory size={12} color="gray" />
<Text size="xs" c="dimmed">
{dayjs(error.createdAt).format('D MMM YYYY, HH:mm')}
</Text>
</Group>
</Table.Td>
<Table.Td pr="lg">
<Badge
color={STATUS_COLOR[error.status?.toUpperCase()] ?? 'gray'}
variant="light"
size="sm"
>
{STATUS_LABEL[error.status?.toUpperCase()] ?? error.status}
</Badge>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollArea>
</Paper>
<Drawer
opened={opened}
onClose={close}
position="right"
size="md"
title={
<Group gap="xs">
<TbMessageReport color="#ef4444" size={22} />
<Title order={4}>Error Detail</Title>
</Group>
}
styles={{
header: { padding: '20px 24px', borderBottom: '1px solid var(--mantine-color-default-border)' },
}}
>
{selectedError && (
<Stack p="lg" gap="xl">
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Description</Text>
<Text fw={600} size="sm">{selectedError.description}</Text>
</Box>
<SimpleGrid cols={2} spacing="lg">
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Status</Text>
<Badge
color={STATUS_COLOR[selectedError.status?.toUpperCase()] ?? 'gray'}
variant="light"
size="sm"
>
{STATUS_LABEL[selectedError.status?.toUpperCase()] ?? selectedError.status}
</Badge>
</Box>
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Source</Text>
<Badge variant="light" color="gray" size="sm">{selectedError.source}</Badge>
</Box>
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">App Version</Text>
<Badge variant="light" color="gray" size="sm">v{selectedError.affectedVersion || 'N/A'}</Badge>
</Box>
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Reported</Text>
<Text size="sm" fw={500}>{dayjs(selectedError.createdAt).format('D MMM YYYY, HH:mm')}</Text>
</Box>
</SimpleGrid>
{selectedError.device && (
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Device</Text>
<Text size="sm">{selectedError.device} · {selectedError.os}</Text>
</Box>
)}
{selectedError.feedBack && (
<>
<Divider opacity={0.1} />
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Developer Feedback</Text>
<Text size="sm" style={{ whiteSpace: 'pre-wrap' }}>{selectedError.feedBack}</Text>
</Box>
</>
)}
<Divider opacity={0.1} />
<Box>
<Group justify="space-between" mb="sm">
<Text size="xs" fw={700} c="dimmed" tt="uppercase">Stack Trace</Text>
<Button
variant="subtle"
size="compact-xs"
color="gray"
onClick={() => setShowStackTrace((v) => !v)}
>
{showStackTrace ? 'Hide' : 'Show'}
</Button>
</Group>
{showStackTrace && (
<Code
block
color="red"
style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6, fontSize: 11, border: '1px solid var(--mantine-color-default-border)' }}
>
{selectedError.stackTrace || '(no stack trace)'}
</Code>
)}
</Box>
</Stack>
)}
</Drawer>
</>
)
}