195 lines
7.0 KiB
TypeScript
195 lines
7.0 KiB
TypeScript
import { AppCard } from '@/frontend/components/AppCard'
|
|
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
|
import { StatsCard } from '@/frontend/components/StatsCard'
|
|
import { useSession } from '@/frontend/hooks/useAuth'
|
|
import {
|
|
Badge,
|
|
Button,
|
|
Container,
|
|
Group,
|
|
Loader,
|
|
Paper,
|
|
SimpleGrid,
|
|
Stack,
|
|
Table,
|
|
Text,
|
|
Title,
|
|
} from '@mantine/core'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { createFileRoute, Link, redirect } from '@tanstack/react-router'
|
|
import { TbApps, TbChevronRight, TbMessageReport, TbUsers } from 'react-icons/tb'
|
|
|
|
export const Route = createFileRoute('/dashboard')({
|
|
beforeLoad: async ({ context }) => {
|
|
try {
|
|
const data = await context.queryClient.ensureQueryData({
|
|
queryKey: ['auth', 'session'],
|
|
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
|
|
})
|
|
if (!data?.user) throw redirect({ to: '/login' })
|
|
} catch (e) {
|
|
if (e instanceof Error) throw redirect({ to: '/login' })
|
|
throw e
|
|
}
|
|
},
|
|
component: DashboardPage,
|
|
})
|
|
|
|
function DashboardPage() {
|
|
const { data: sessionData } = useSession()
|
|
const user = sessionData?.user
|
|
|
|
const { data: stats, isLoading: statsLoading } = useQuery({
|
|
queryKey: ['dashboard', 'stats'],
|
|
queryFn: () => fetch('/api/dashboard/stats').then((r) => r.json()),
|
|
})
|
|
|
|
const { data: apps, isLoading: appsLoading } = useQuery({
|
|
queryKey: ['apps'],
|
|
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 (
|
|
<DashboardLayout>
|
|
<Container size="xl" py="lg">
|
|
<Stack gap="xl">
|
|
<Group justify="space-between" align="center">
|
|
<Stack gap={0}>
|
|
<Title order={2} className="gradient-text">Overview Dashboard</Title>
|
|
<Text size="sm" c="dimmed">Welcome back, {user?.name}. Here is what's happening today.</Text>
|
|
</Stack>
|
|
{/* <Button
|
|
variant="gradient"
|
|
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
|
|
leftSection={<TbApps size={18} />}
|
|
radius="md"
|
|
component={Link}
|
|
to="/apps"
|
|
>
|
|
Manage All Apps
|
|
</Button> */}
|
|
</Group>
|
|
|
|
{statsLoading ? (
|
|
<Loader size="xl" type="dots" mx="auto" mt="xl" />
|
|
) : (
|
|
<SimpleGrid cols={{ base: 1, sm: 3 }}>
|
|
<StatsCard
|
|
title="Total Applications"
|
|
value={stats?.totalApps || 0}
|
|
icon={TbApps}
|
|
color="brand-blue"
|
|
// trend={{ value: stats?.trends?.totalApps.toString() || '0', positive: true }}
|
|
/>
|
|
<StatsCard
|
|
title="New Errors"
|
|
value={stats?.newErrors || 0}
|
|
icon={TbMessageReport}
|
|
color="brand-purple"
|
|
// trend={{ value: stats?.trends?.newErrors.toString() || '0', positive: false }}
|
|
/>
|
|
<StatsCard
|
|
title="Users"
|
|
value={stats?.activeUsers || 0}
|
|
icon={TbUsers}
|
|
color="teal"
|
|
// trend={{ value: stats?.trends?.activeUsers.toString() || '0', positive: true }}
|
|
/>
|
|
</SimpleGrid>
|
|
)}
|
|
|
|
<Group justify="space-between" mt="md">
|
|
<Title order={3}>Registered Applications</Title>
|
|
<Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />} component={Link} to="/apps">
|
|
View All Apps
|
|
</Button>
|
|
</Group>
|
|
|
|
{appsLoading ? (
|
|
<Loader size="xl" type="dots" mx="auto" />
|
|
) : (
|
|
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
|
{apps?.map((app: any) => (
|
|
<AppCard key={app.id} {...app} />
|
|
))}
|
|
</SimpleGrid>
|
|
)}
|
|
|
|
<Group justify="space-between" mt="md">
|
|
<Title order={3}>Recent Error Reports</Title>
|
|
<Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />} component={Link} to="/bug-reports">
|
|
View All Errors
|
|
</Button>
|
|
</Group>
|
|
|
|
<Paper withBorder radius="2xl" className="glass" p="md">
|
|
<Table className="data-table" verticalSpacing="md">
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Application</Table.Th>
|
|
<Table.Th>Error Message</Table.Th>
|
|
<Table.Th>Version</Table.Th>
|
|
<Table.Th>Time</Table.Th>
|
|
<Table.Th>Severity</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{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.Td>
|
|
<Text fw={600} size="sm" style={{ textTransform: 'uppercase' }}>{error.app}</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm" c="dimmed" lineClamp={1}>{error.message}</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Badge variant="light" color="gray">v{error.version}</Badge>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="xs" c="dimmed">{formatTimeAgo(error.time)}</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Badge
|
|
color={error.severity === 'OPEN' ? 'red' : error.severity === 'IN_PROGRESS' || error.severity === 'ON_HOLD' ? 'orange' : 'yellow'}
|
|
variant="dot"
|
|
>
|
|
{error.severity.toUpperCase()}
|
|
</Badge>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Paper>
|
|
</Stack>
|
|
</Container>
|
|
</DashboardLayout>
|
|
)
|
|
}
|