upd: overview desa
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user