upd: log global user
This commit is contained in:
@@ -46,7 +46,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
||||
</Avatar>
|
||||
<Stack gap={0}>
|
||||
<Text fw={700} size="lg" style={{ letterSpacing: '-0.3px' }}>{name}</Text>
|
||||
<Text size="xs" c="dimmed" fw={600}>BUILD v{version}</Text>
|
||||
<Text size="xs" c="dimmed" fw={600}>VERSION {version}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Badge color={statusColor} variant="dot" size="sm">
|
||||
@@ -54,7 +54,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Stack gap="md" mt="sm">
|
||||
{/* <Stack gap="md" mt="sm">
|
||||
<Box>
|
||||
<Group justify="space-between" mb={6}>
|
||||
<Group gap="xs">
|
||||
@@ -70,13 +70,13 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
||||
<Group justify="space-between" mb={6}>
|
||||
<Group gap="xs">
|
||||
<TbAlertTriangle size={16} color={errors > 0 ? '#ef4444' : '#64748b'} />
|
||||
<Text size="xs" fw={700} c="dimmed">HEALTH INCIDENTS</Text>
|
||||
<Text size="xs" fw={700} c="dimmed">ERROR</Text>
|
||||
</Group>
|
||||
<Text size="sm" fw={700} color={errors > 0 ? 'red' : 'dimmed'}>{errors}</Text>
|
||||
</Group>
|
||||
<Progress value={errors > 0 ? 30 : 0} size="sm" color="red" radius="xl" />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack> */}
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
@@ -96,7 +96,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
||||
}
|
||||
}}
|
||||
>
|
||||
Center Intelligence
|
||||
View
|
||||
</Button>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -27,7 +27,9 @@ import {
|
||||
TbSettings,
|
||||
TbUserCircle,
|
||||
TbSun,
|
||||
TbMoon
|
||||
TbMoon,
|
||||
TbUser,
|
||||
TbHistory
|
||||
} from 'react-icons/tb'
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
@@ -49,7 +51,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const globalNav = [
|
||||
{ label: 'Dashboard', icon: TbDashboard, to: '/dashboard' },
|
||||
{ label: 'Applications', icon: TbApps, to: '/apps' },
|
||||
{ label: 'Settings', icon: TbSettings, to: '/settings' },
|
||||
{ label: 'Log Activity', icon: TbHistory, to: '/logs' },
|
||||
{ label: 'Users', icon: TbUser, to: '/users' },
|
||||
]
|
||||
|
||||
const activeApp = appId ? APP_CONFIGS[appId] : null
|
||||
@@ -166,7 +169,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
mb={"md"}
|
||||
variant="filled"
|
||||
styles={(theme) => ({
|
||||
input: { border: '1px solid rgba(255,255,255,0.1)' }
|
||||
input: { border: computedColorScheme === 'dark' ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.1)' }
|
||||
})}
|
||||
/>
|
||||
}
|
||||
@@ -215,7 +218,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
<Box
|
||||
p="md"
|
||||
className="glass"
|
||||
style={{ borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}
|
||||
style={{ borderRadius: '12px', border: computedColorScheme === 'dark' ? '1px solid rgba(255,255,255,0.05)' : '1px solid rgba(0,0,0,0.05)' }}
|
||||
>
|
||||
<Text size="xs" c="dimmed" fw={600} mb="xs">SYSTEM STATUS</Text>
|
||||
<Group gap="xs">
|
||||
|
||||
214
src/frontend/routes/logs.tsx
Normal file
214
src/frontend/routes/logs.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
Badge,
|
||||
Container,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Paper,
|
||||
TextInput,
|
||||
Select,
|
||||
Avatar,
|
||||
Box,
|
||||
Divider,
|
||||
} from '@mantine/core'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { TbSearch, TbClock, TbCheck, TbX } from 'react-icons/tb'
|
||||
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
||||
|
||||
export const Route = createFileRoute('/logs')({
|
||||
component: GlobalLogsPage,
|
||||
})
|
||||
|
||||
const timelineData = [
|
||||
{
|
||||
date: 'TODAY',
|
||||
logs: [
|
||||
{ id: 1, time: '12:12 PM', operator: 'Budi Santoso', app: 'Desa+', color: 'blue', content: <>generated document <Badge variant="light" color="gray" radius="sm">Surat Domisili</Badge> for <Badge variant="light" color="blue" radius="sm">Sukatani</Badge></> },
|
||||
{ id: 2, time: '11:42 AM', operator: 'Siti Aminah', app: 'Desa+', color: 'teal', content: <>uploaded financial report <Badge variant="light" color="gray" radius="sm">Realisasi Q1</Badge> for <Badge variant="light" color="teal" radius="sm">Sukamaju</Badge></> },
|
||||
{ id: 3, time: '10:12 AM', operator: 'System', app: 'Desa+', color: 'red', icon: TbX, content: <>experienced failure in <Badge variant="light" color="violet" radius="sm">SIAK Sync</Badge> at <Badge variant="light" color="red" radius="sm" leftSection={<TbX size={12}/>}>Cikini</Badge></>, message: { title: 'Sync Operation Failed (NullPointerException)', text: 'NullPointerException at village_sync.dart:45. The server returned a timeout error while waiting for the master database replica connection. Auto-retry scheduled in 15 minutes.' } },
|
||||
{ id: 4, time: '09:42 AM', operator: 'Jane Smith', app: 'E-Commerce', color: 'orange', icon: TbCheck, content: <>resolved payment gateway issue for <Badge variant="light" color="orange" radius="sm">E-Commerce</Badge> checkout</> },
|
||||
]
|
||||
},
|
||||
{
|
||||
date: 'YESTERDAY',
|
||||
logs: [
|
||||
{ id: 5, time: '05:10 AM', operator: 'System', app: 'System', color: 'cyan', content: <>completed automated <Badge variant="light" color="cyan" radius="sm">Nightly Backup</Badge> for all 138 villages</> },
|
||||
{ id: 6, time: '04:50 AM', operator: 'Rahmat Hidayat', app: 'Desa+', color: 'green', content: <>granted Admin access to <Text component="span" fw={600}>Desa Bojong Gede</Text> operator</> },
|
||||
{ id: 7, time: '03:42 AM', operator: 'System', app: 'Fitness App', color: 'red', icon: TbX, content: <>detected SocketException across <Badge variant="light" color="violet" radius="sm">Fitness App</Badge> wearable sync operations.</> },
|
||||
{ id: 8, time: '02:33 AM', operator: 'Agus Setiawan', app: 'Desa+', color: 'blue', content: <>verified 145 <Badge variant="light" color="gray" radius="sm">Surat Kematian</Badge> entries in batch.</> },
|
||||
]
|
||||
},
|
||||
{
|
||||
date: '12 APRIL, 2026',
|
||||
logs: [
|
||||
{ id: 9, time: '03:42 AM', operator: 'Amel', app: 'Desa+', color: 'indigo', content: <>changed version configurations rolling out <Badge variant="light" color="gray" radius="sm">Desa+ v2.4.1</Badge></> },
|
||||
{ id: 10, time: '02:10 AM', operator: 'John Doe', app: 'E-Commerce', color: 'pink', content: <>updated App setting <Badge variant="light" color="gray" radius="sm">Require OTP on Login</Badge> <Text component="span" c="violet" fw={600} size="sm" style={{ cursor: 'pointer' }}>View Details</Text></> },
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
function GlobalLogsPage() {
|
||||
const [search, setSearch] = useState('')
|
||||
const [appFilter, setAppFilter] = useState<string | null>(null)
|
||||
const [operatorFilter, setOperatorFilter] = useState<string | null>(null)
|
||||
|
||||
const filteredTimeline = useMemo(() => {
|
||||
return timelineData
|
||||
.map(group => {
|
||||
const filteredLogs = group.logs.filter(log => {
|
||||
if (appFilter && log.app !== appFilter) return false;
|
||||
if (operatorFilter && log.operator !== operatorFilter) return false;
|
||||
if (search) {
|
||||
const lSearch = search.toLowerCase();
|
||||
if (!log.operator.toLowerCase().includes(lSearch) && !log.app.toLowerCase().includes(lSearch)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return { ...group, logs: filteredLogs };
|
||||
})
|
||||
.filter(group => group.logs.length > 0);
|
||||
}, [search, appFilter, operatorFilter]);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container size="xl" py="lg">
|
||||
|
||||
{/* Header Controls */}
|
||||
<Group mb="xl" gap="md">
|
||||
<TextInput
|
||||
placeholder="Search operator or app..."
|
||||
leftSection={<TbSearch size={16} />}
|
||||
radius="md"
|
||||
w={220}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<Select
|
||||
placeholder="All Applications"
|
||||
data={['Desa+', 'E-Commerce', 'Fitness App', 'System']}
|
||||
radius="md"
|
||||
w={160}
|
||||
clearable
|
||||
value={appFilter}
|
||||
onChange={setAppFilter}
|
||||
/>
|
||||
<Select
|
||||
placeholder="All Operators"
|
||||
data={['Agus Setiawan', 'Amel', 'Budi Santoso', 'Jane Smith', 'John Doe', 'Rahmat Hidayat', 'Siti Aminah', 'System']}
|
||||
radius="md"
|
||||
w={160}
|
||||
clearable
|
||||
value={operatorFilter}
|
||||
onChange={setOperatorFilter}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Timeline Content */}
|
||||
<Paper withBorder p="xl" radius="2xl" className="glass" style={{ background: 'var(--mantine-color-body)' }}>
|
||||
{filteredTimeline.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">No logs found matching your filters.</Text>
|
||||
) : filteredTimeline.map((group, groupIndex) => (
|
||||
<Box key={group.date}>
|
||||
<Text
|
||||
size="xs"
|
||||
fw={700}
|
||||
c="dimmed"
|
||||
mt={groupIndex > 0 ? "xl" : 0}
|
||||
mb="lg"
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
>
|
||||
{group.date}
|
||||
</Text>
|
||||
|
||||
<Stack gap={0} pl={4}>
|
||||
{group.logs.map((log, logIndex) => {
|
||||
const isLastLog = logIndex === group.logs.length - 1;
|
||||
|
||||
return (
|
||||
<Group
|
||||
key={log.id}
|
||||
wrap="nowrap"
|
||||
align="flex-start"
|
||||
gap="lg"
|
||||
style={{ position: 'relative', paddingBottom: isLastLog ? 0 : 32 }}
|
||||
>
|
||||
{/* Left: Time */}
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
w={70}
|
||||
style={{ flexShrink: 0, marginTop: 4, textAlign: 'left' }}
|
||||
>
|
||||
{log.time}
|
||||
</Text>
|
||||
|
||||
{/* Middle: Line & Avatar */}
|
||||
<Box style={{ position: 'relative', width: 20, flexShrink: 0, alignSelf: 'stretch' }}>
|
||||
{/* Vertical Line */}
|
||||
{!isLastLog && (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 24,
|
||||
bottom: -8,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: 1,
|
||||
backgroundColor: 'rgba(128,128,128,0.2)'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Avatar */}
|
||||
<Box style={{ position: 'relative', zIndex: 2 }}>
|
||||
{log.icon ? (
|
||||
<Avatar size={24} radius="xl" color={log.color} variant="light">
|
||||
<log.icon size={14} />
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar size={24} radius="xl" color={log.color}>
|
||||
{log.operator.charAt(0)}
|
||||
</Avatar>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Right: Content */}
|
||||
<Box style={{ flexGrow: 1, marginTop: 2 }}>
|
||||
<Text size="sm">
|
||||
<Text component="span" fw={600} mr={4}>{log.operator}</Text>
|
||||
{log.content}
|
||||
</Text>
|
||||
|
||||
{log.message && (
|
||||
<Paper
|
||||
withBorder
|
||||
p="md"
|
||||
radius="md"
|
||||
mt="sm"
|
||||
style={{ maxWidth: 800, backgroundColor: 'transparent' }}
|
||||
>
|
||||
<Text size="sm" fw={600} mb={4}>{log.message.title}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{log.message.text}
|
||||
</Text>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
{groupIndex < timelineData.length - 1 && (
|
||||
<Divider my="xl" color="rgba(128,128,128,0.1)" />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
@@ -35,8 +35,8 @@ import {
|
||||
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
||||
import { StatsCard } from '@/frontend/components/StatsCard'
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
component: SettingsPage,
|
||||
export const Route = createFileRoute('/users')({
|
||||
component: UsersPage,
|
||||
})
|
||||
|
||||
const mockUsers = [
|
||||
@@ -67,14 +67,14 @@ const roles = [
|
||||
},
|
||||
]
|
||||
|
||||
function SettingsPage() {
|
||||
function UsersPage() {
|
||||
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">Settings</Title>
|
||||
<Title order={2} className="gradient-text">Users</Title>
|
||||
<Text size="sm" c="dimmed">Manage system users, security roles, and application access control.</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
Reference in New Issue
Block a user