Skip to main content

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

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>
);
}

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',
});

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>
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