Components
Component Architecture
Frontend CLM menggunakan arsitektur komponen yang terorganisasi:
src/components/
├── ui/ # shadcn/ui primitives (46 components)
├── sidebar.tsx # Navigation sidebar
├── header.tsx # Top header
└── charts/ # Chart components
Layout Components
Sidebar
Navigation sidebar dengan role-based menu visibility.
// src/components/sidebar.tsx
export function Sidebar() {
const { roleId } = useApp();
// Menu items filtered by role
const menuItems = [
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
{ path: '/search', label: 'Log Search', icon: Search, roles: ['admin', 'it-ops', 'security'] },
{ path: '/alerting', label: 'Alerting', icon: Bell, roles: ['admin', 'security', 'helpdesk'] },
{ path: '/security', label: 'Security', icon: Shield, roles: ['admin', 'security'] },
{ path: '/compliance', label: 'Compliance', icon: FileCheck, roles: ['admin', 'compliance'] },
{ path: '/master-data', label: 'Master Data', icon: Database, roles: ['admin'] },
// ... more items
];
return (
<aside className="w-64 bg-sidebar border-r">
<nav>
{menuItems
.filter(item => !item.roles || item.roles.includes(roleId))
.map(item => <NavLink key={item.path} {...item} />)}
</nav>
</aside>
);
}
Header
Top header dengan user info dan tenant selector.
// src/components/header.tsx
export function Header() {
const { userName, userRoles, tenantName, setTenantId } = useApp();
return (
<header className="h-14 border-b flex items-center justify-between px-4">
<TenantSelector onChange={setTenantId} />
<div className="flex items-center gap-2">
<UserRoleBadge roles={userRoles} />
<UserMenu name={userName} />
</div>
</header>
);
}
shadcn/ui Components
Forms
Button
import { Button } from '@/components/ui/button';
// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
// Sizes
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon">Icon</Button>
Input
import { Input } from '@/components/ui/input';
<Input type="text" placeholder="Search..." />
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
Select
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
<Select>
<SelectTrigger>
<SelectValue placeholder="Select..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="1h">Last 1 hour</SelectItem>
<SelectItem value="24h">Last 24 hours</SelectItem>
<SelectItem value="7d">Last 7 days</SelectItem>
</SelectContent>
</Select>
Data Display
Table
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
<Table>
<TableHeader>
<TableRow>
<TableHead>Timestamp</TableHead>
<TableHead>Level</TableHead>
<TableHead>Message</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.map(log => (
<TableRow key={log.id}>
<TableCell>{log.timestamp}</TableCell>
<TableCell>{log.level}</TableCell>
<TableCell>{log.message}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
Card
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
<Card>
<CardHeader>
<CardTitle>Dashboard Overview</CardTitle>
</CardHeader>
<CardContent>
{/* Card content */}
</CardContent>
</Card>
Feedback
Dialog
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
</DialogHeader>
{/* Dialog content */}
</DialogContent>
</Dialog>
Alert Dialog
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Toast (Sonner)
import { toast } from 'sonner';
// Success
toast.success('Operation completed');
// Error
toast.error('Something went wrong');
// Info
toast.info('New message received');
// Promise
toast.promise(saveData(), {
loading: 'Saving...',
success: 'Data saved',
error: 'Failed to save',
});
Navigation
Tabs
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
</TabsList>
<TabsContent value="overview">
{/* Overview content */}
</TabsContent>
<TabsContent value="details">
{/* Details content */}
</TabsContent>
</Tabs>
Dropdown Menu
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Chart Components
Log Volume Chart
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
export function LogVolumeChart({ data }: { data: LogVolumeData[] }) {
return (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="count" stroke="#8884d8" fill="#8884d8" />
</AreaChart>
</ResponsiveContainer>
);
}
Error Rate Chart
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
export function ErrorRateChart({ data }: { data: ErrorRateData[] }) {
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="errorRate" stroke="#ef4444" />
<Line type="monotone" dataKey="successRate" stroke="#22c55e" />
</LineChart>
</ResponsiveContainer>
);
}
Custom Hooks
useApp
import { useApp } from '@/lib/app-context';
const {
tenantId, setTenantId,
roleId, setRoleId,
isAuthed, setAuthed,
userName, setUserName,
userRoles, setUserRoles
} = useApp();
useQuery (TanStack Query)
import { useQuery } from '@tanstack/react-query';
import { fetchJson } from '@/api/http';
function useLogs(params: LogSearchParams) {
return useQuery({
queryKey: ['logs', params],
queryFn: () => fetchJson('/api/v1/logs/list/paginate', {
method: 'POST',
body: JSON.stringify(params),
}),
});
}
Next Steps
- Routing - Route configuration
- State Management - State management
- i18n - Internationalization