upd: menu dashboard
Deskripsi: - connected to database No Issues
This commit is contained in:
42
src/app.ts
42
src/app.ts
@@ -154,18 +154,38 @@ export function createApp() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ─── Monitoring API ────────────────────────────────
|
// ─── Monitoring API ────────────────────────────────
|
||||||
.get('/api/dashboard/stats', () => ({
|
.get('/api/dashboard/stats', async () => {
|
||||||
totalApps: 3,
|
const newErrors = await prisma.bug.count({ where: { status: 'OPEN' } })
|
||||||
newErrors: 185,
|
const users = await prisma.user.count()
|
||||||
activeUsers: '24.5k',
|
return {
|
||||||
trends: { totalApps: 1, newErrors: 12, activeUsers: 5.2 }
|
totalApps: 1,
|
||||||
}))
|
newErrors: newErrors,
|
||||||
|
activeUsers: users,
|
||||||
|
trends: { totalApps: 0, newErrors: 12, activeUsers: 5.2 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
.get('/api/apps', () => [
|
.get('/api/dashboard/recent-errors', async () => {
|
||||||
{ id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: 12, version: '2.4.1' },
|
const bugs = await prisma.bug.findMany({
|
||||||
// { id: 'e-commerce', name: 'E-Commerce', status: 'warning', users: 8900, errors: 45, version: '1.8.0' },
|
take: 5,
|
||||||
// { id: 'fitness-app', name: 'Fitness App', status: 'error', users: 3200, errors: 128, version: '0.9.5' },
|
orderBy: { createdAt: 'desc' }
|
||||||
])
|
})
|
||||||
|
return bugs.map(b => ({
|
||||||
|
id: b.id,
|
||||||
|
app: b.app,
|
||||||
|
message: b.description,
|
||||||
|
version: b.affectedVersion,
|
||||||
|
time: b.createdAt.toISOString(),
|
||||||
|
severity: b.status
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
.get('/api/apps', async () => {
|
||||||
|
const desaPlusErrors = await prisma.bug.count({ where: { app: { in: ['desa-plus', 'desa_plus'] }, status: 'OPEN' } })
|
||||||
|
return [
|
||||||
|
{ id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: desaPlusErrors, version: '2.4.1' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
.get('/api/apps/:appId', ({ params: { appId } }) => {
|
.get('/api/apps/:appId', ({ params: { appId } }) => {
|
||||||
const apps = {
|
const apps = {
|
||||||
|
|||||||
@@ -36,12 +36,6 @@ export const Route = createFileRoute('/dashboard')({
|
|||||||
component: DashboardPage,
|
component: DashboardPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
const recentErrors = [
|
|
||||||
{ id: 1, app: 'Desa+', message: 'NullPointerException at village_sync.dart:45', version: '2.4.1', time: '2 mins ago', severity: 'critical' },
|
|
||||||
{ id: 2, app: 'E-Commerce', message: 'Failed to load checkout session', version: '1.8.0', time: '15 mins ago', severity: 'high' },
|
|
||||||
{ id: 3, app: 'Fitness App', message: 'SocketException: Connection timed out', version: '0.9.5', time: '1 hour ago', severity: 'medium' },
|
|
||||||
]
|
|
||||||
|
|
||||||
function DashboardPage() {
|
function DashboardPage() {
|
||||||
const { data: sessionData } = useSession()
|
const { data: sessionData } = useSession()
|
||||||
const user = sessionData?.user
|
const user = sessionData?.user
|
||||||
@@ -56,6 +50,20 @@ function DashboardPage() {
|
|||||||
queryFn: () => fetch('/api/apps').then((r) => r.json()),
|
queryFn: () => fetch('/api/apps').then((r) => r.json()),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { data: recentErrors = [], isLoading: recentErrorsLoading } = useQuery({
|
||||||
|
queryKey: ['dashboard', 'recent-errors'],
|
||||||
|
queryFn: () => fetch('/api/dashboard/recent-errors').then((r) => r.json()),
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatTimeAgo = (dateStr: string) => {
|
||||||
|
const diff = new Date().getTime() - new Date(dateStr).getTime()
|
||||||
|
const minutes = Math.floor(diff / 60000)
|
||||||
|
if (minutes < 60) return `${minutes || 1} mins ago`
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
if (hours < 24) return `${hours} hours ago`
|
||||||
|
return `${Math.floor(hours / 24)} days ago`
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<Container size="xl" py="lg">
|
<Container size="xl" py="lg">
|
||||||
@@ -86,21 +94,21 @@ function DashboardPage() {
|
|||||||
value={stats?.totalApps || 0}
|
value={stats?.totalApps || 0}
|
||||||
icon={TbApps}
|
icon={TbApps}
|
||||||
color="brand-blue"
|
color="brand-blue"
|
||||||
trend={{ value: stats?.trends?.totalApps.toString() || '0', positive: true }}
|
// trend={{ value: stats?.trends?.totalApps.toString() || '0', positive: true }}
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="New Errors"
|
title="New Errors"
|
||||||
value={stats?.newErrors || 0}
|
value={stats?.newErrors || 0}
|
||||||
icon={TbMessageReport}
|
icon={TbMessageReport}
|
||||||
color="brand-purple"
|
color="brand-purple"
|
||||||
trend={{ value: stats?.trends?.newErrors.toString() || '0', positive: false }}
|
// trend={{ value: stats?.trends?.newErrors.toString() || '0', positive: false }}
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Active Users"
|
title="Users"
|
||||||
value={stats?.activeUsers || 0}
|
value={stats?.activeUsers || 0}
|
||||||
icon={TbUsers}
|
icon={TbUsers}
|
||||||
color="teal"
|
color="teal"
|
||||||
trend={{ value: stats?.trends?.activeUsers.toString() || '0', positive: true }}
|
// trend={{ value: stats?.trends?.activeUsers.toString() || '0', positive: true }}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
)}
|
)}
|
||||||
@@ -124,7 +132,7 @@ function DashboardPage() {
|
|||||||
|
|
||||||
<Group justify="space-between" mt="md">
|
<Group justify="space-between" mt="md">
|
||||||
<Title order={3}>Recent Error Reports</Title>
|
<Title order={3}>Recent Error Reports</Title>
|
||||||
<Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />}>
|
<Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />} component={Link} to="/bug-reports">
|
||||||
View All Errors
|
View All Errors
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -141,23 +149,35 @@ function DashboardPage() {
|
|||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{recentErrors.map((error) => (
|
{recentErrorsLoading ? (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={5} align="center" py="xl">
|
||||||
|
<Loader size="sm" type="dots" />
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
) : recentErrors.length === 0 ? (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={5} align="center" py="xl">
|
||||||
|
<Text c="dimmed" size="sm">No recent errors found.</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
) : recentErrors.map((error: any) => (
|
||||||
<Table.Tr key={error.id}>
|
<Table.Tr key={error.id}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text fw={600} size="sm">{error.app}</Text>
|
<Text fw={600} size="sm" style={{ textTransform: 'uppercase' }}>{error.app}</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text size="sm" c="dimmed">{error.message}</Text>
|
<Text size="sm" c="dimmed" lineClamp={1}>{error.message}</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Badge variant="light" color="gray">v{error.version}</Badge>
|
<Badge variant="light" color="gray">v{error.version}</Badge>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text size="xs" c="dimmed">{error.time}</Text>
|
<Text size="xs" c="dimmed">{formatTimeAgo(error.time)}</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Badge
|
<Badge
|
||||||
color={error.severity === 'critical' ? 'red' : error.severity === 'high' ? 'orange' : 'yellow'}
|
color={error.severity === 'OPEN' ? 'red' : error.severity === 'IN_PROGRESS' || error.severity === 'ON_HOLD' ? 'orange' : 'yellow'}
|
||||||
variant="dot"
|
variant="dot"
|
||||||
>
|
>
|
||||||
{error.severity.toUpperCase()}
|
{error.severity.toUpperCase()}
|
||||||
|
|||||||
Reference in New Issue
Block a user