upd: overview desa

This commit is contained in:
2026-04-15 14:14:18 +08:00
parent c66ce4a39b
commit c67fc9a230
2 changed files with 50 additions and 60 deletions

View File

@@ -15,62 +15,35 @@ import {
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks' import { useDisclosure } from '@mantine/hooks'
import { useState } from 'react' import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { TbMessageReport, TbHistory, TbExternalLink, TbBug } from 'react-icons/tb' import { TbMessageReport, TbHistory, TbExternalLink, TbBug } from 'react-icons/tb'
const mockErrors = [ export interface ErrorDataTableProps {
{ appId?: string
id: 1, }
message: 'NullPointerException at village_sync.dart:45',
village: 'Sukatani',
version: 'v1.2.0',
timestamp: '2026-04-01 14:30:15',
severity: 'critical',
stackTrace: 'at com.desa.sync.VillageManager.sync(VillageManager.java:45)\nat com.desa.sync.SyncService.onHandleIntent(SyncService.java:120)'
},
{
id: 2,
message: 'Failed to load citizen record session',
village: 'Sukamaju',
version: 'v1.1.8',
timestamp: '2026-04-01 14:15:22',
severity: 'high',
stackTrace: 'Error: Connection timeout reaching upstream citizen-db\n at HttpClient.get (network.dart:88)'
},
{
id: 3,
message: 'SocketException: Connection timed out',
village: 'Cikini',
version: 'v1.2.0',
timestamp: '2026-04-01 13:55:10',
severity: 'medium',
stackTrace: 'SocketException: OS Error: Connection timed out, errno = 110, address = 10.0.2.2, port = 54332'
},
{
id: 4,
message: 'UI Thread blocking > 500ms',
village: 'Beji',
version: 'v1.1.2',
timestamp: '2026-04-01 13:40:00',
severity: 'low',
stackTrace: 'ANR (Application Not Responding) detected in main thread.'
},
]
export function ErrorDataTable() { export function ErrorDataTable({ appId }: ErrorDataTableProps) {
const [opened, { open, close }] = useDisclosure(false) const [opened, { open, close }] = useDisclosure(false)
const [selectedError, setSelectedError] = useState<any>(null) const [selectedError, setSelectedError] = useState<any>(null)
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) => { const handleRowClick = (error: any) => {
setSelectedError(error) setSelectedError(error)
open() open()
} }
const getSeverityColor = (sev: string) => { const getSeverityColor = (sev: string) => {
switch(sev) { switch(sev?.toUpperCase()) {
case 'critical': return 'red' case 'OPEN': return 'red'
case 'high': return 'orange' case 'IN_PROGRESS': return 'orange'
case 'medium': return 'yellow' case 'ON_HOLD': return 'yellow'
default: return 'gray' default: return 'gray'
} }
} }
@@ -86,7 +59,7 @@ export function ErrorDataTable() {
</ThemeIcon> </ThemeIcon>
<Text fw={700}>LATEST ERROR REPORTS</Text> <Text fw={700}>LATEST ERROR REPORTS</Text>
</Group> </Group>
<Button component={Link} to='/apps/desa-plus/errors' variant="subtle" size="compact-xs" color="blue" rightSection={<TbExternalLink size={14} />}> <Button component={Link} to={appId ? `/apps/${appId}/errors` : '/bug-reports'} variant="subtle" size="compact-xs" color="blue" rightSection={<TbExternalLink size={14} />}>
View All Reports View All Reports
</Button> </Button>
</Group> </Group>
@@ -97,37 +70,49 @@ export function ErrorDataTable() {
<Table.Thead bg="rgba(0,0,0,0.1)"> <Table.Thead bg="rgba(0,0,0,0.1)">
<Table.Tr> <Table.Tr>
<Table.Th px="xl">Error Message</Table.Th> <Table.Th px="xl">Error Message</Table.Th>
<Table.Th>Village</Table.Th> <Table.Th>Reporter</Table.Th>
<Table.Th>App Version</Table.Th> <Table.Th>App Version</Table.Th>
<Table.Th>Timestamp</Table.Th> <Table.Th>Timestamp</Table.Th>
<Table.Th pr="xl">Severity</Table.Th> <Table.Th pr="xl">Severity</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{mockErrors.map((error) => ( {isLoading ? (
<Table.Tr>
<Table.Td colSpan={5} align="center" py="xl">
Loading errors...
</Table.Td>
</Table.Tr>
) : bugs.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={5} align="center" py="xl">
No errors found.
</Table.Td>
</Table.Tr>
) : bugs.map((error: any) => (
<Table.Tr <Table.Tr
key={error.id} key={error.id}
onClick={() => handleRowClick(error)} onClick={() => handleRowClick(error)}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
> >
<Table.Td px="xl"> <Table.Td px="xl">
<Text size="sm" fw={600} lineClamp={1}>{error.message}</Text> <Text size="sm" fw={600} lineClamp={1}>{error.description}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Badge variant="dot" color="brand-blue" radius="sm">{error.village}</Badge> <Badge variant="dot" color="brand-blue" radius="sm">{error.user?.name || error.userId || 'System'}</Badge>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Text size="xs" fw={700} c="dimmed">{error.version}</Text> <Text size="xs" fw={700} c="dimmed">{error.affectedVersion || 'N/A'}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Group gap={6}> <Group gap={6}>
<TbHistory size={12} color="gray" /> <TbHistory size={12} color="gray" />
<Text size="xs" c="dimmed">{error.timestamp}</Text> <Text size="xs" c="dimmed">{new Date(error.createdAt).toLocaleString()}</Text>
</Group> </Group>
</Table.Td> </Table.Td>
<Table.Td pr="xl"> <Table.Td pr="xl">
<Badge color={getSeverityColor(error.severity)} variant="light" size="sm"> <Badge color={getSeverityColor(error.status)} variant="light" size="sm">
{error.severity.toUpperCase()} {(error.status || '').toUpperCase()}
</Badge> </Badge>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
@@ -156,17 +141,17 @@ export function ErrorDataTable() {
<Stack p="lg" gap="xl"> <Stack p="lg" gap="xl">
<Box> <Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>MESSAGE</Text> <Text size="xs" fw={700} c="dimmed" mb={4}>MESSAGE</Text>
<Text fw={700} size="lg" color="red">{selectedError.message}</Text> <Text fw={700} size="lg" color="red">{selectedError.description}</Text>
</Box> </Box>
<SimpleGrid cols={2} spacing="lg"> <SimpleGrid cols={2} spacing="lg">
<Box> <Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>VILLAGE</Text> <Text size="xs" fw={700} c="dimmed" mb={4}>REPORTER</Text>
<Text fw={600}>{selectedError.village}</Text> <Text fw={600}>{selectedError.user?.name || selectedError.userId || 'System'}</Text>
</Box> </Box>
<Box> <Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>APP VERSION</Text> <Text size="xs" fw={700} c="dimmed" mb={4}>APP VERSION</Text>
<Badge variant="outline">{selectedError.version}</Badge> <Badge variant="outline">{selectedError.affectedVersion || 'N/A'}</Badge>
</Box> </Box>
</SimpleGrid> </SimpleGrid>

View File

@@ -1,3 +1,4 @@
import { useQuery } from '@tanstack/react-query'
import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts' import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts'
import { ErrorDataTable } from '@/frontend/components/ErrorDataTable' import { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
import { SummaryCard } from '@/frontend/components/SummaryCard' import { SummaryCard } from '@/frontend/components/SummaryCard'
@@ -51,6 +52,11 @@ function AppOverviewPage() {
const { data: dailyRes, isLoading: dailyLoading, mutate: mutateDaily } = useSWR(isDesaPlus ? API_URLS.getDailyActivity() : null, fetcher) const { data: dailyRes, isLoading: dailyLoading, mutate: mutateDaily } = useSWR(isDesaPlus ? API_URLS.getDailyActivity() : null, fetcher)
const { data: comparisonRes, isLoading: comparisonLoading, mutate: mutateComparison } = useSWR(isDesaPlus ? API_URLS.getComparisonActivity() : null, fetcher) const { data: comparisonRes, isLoading: comparisonLoading, mutate: mutateComparison } = useSWR(isDesaPlus ? API_URLS.getComparisonActivity() : null, fetcher)
const { data: appData, isLoading: appLoading } = useQuery({
queryKey: ['apps', appId],
queryFn: () => fetch(`/api/apps/${appId}`).then((r) => r.json()),
})
const grid = gridRes?.data const grid = gridRes?.data
const dailyData = dailyRes?.data || [] const dailyData = dailyRes?.data || []
const comparisonData = comparisonRes?.data || [] const comparisonData = comparisonRes?.data || []
@@ -209,12 +215,11 @@ function AppOverviewPage() {
</SummaryCard> </SummaryCard>
<SummaryCard <SummaryCard
title="Errors Today" title="Errors Open"
value="12" value={appLoading ? '...' : (appData?.errors || '0')}
icon={TbAlertTriangle} icon={TbAlertTriangle}
color="red" color="red"
isError={true} isError={true}
trend={{ value: '4.8%', positive: false }}
/> />
</SimpleGrid> </SimpleGrid>
@@ -223,7 +228,7 @@ function AppOverviewPage() {
<VillageComparisonBarChart data={comparisonData} isLoading={comparisonLoading} /> <VillageComparisonBarChart data={comparisonData} isLoading={comparisonLoading} />
</SimpleGrid> </SimpleGrid>
<ErrorDataTable /> <ErrorDataTable appId={appId} />
</Stack> </Stack>
</> </>
) )