diff --git a/frontends/nextjs/src/components/misc/viewers/audit-log/Filters.tsx b/frontends/nextjs/src/components/misc/viewers/audit-log/Filters.tsx
new file mode 100644
index 000000000..7dc47373d
--- /dev/null
+++ b/frontends/nextjs/src/components/misc/viewers/audit-log/Filters.tsx
@@ -0,0 +1,188 @@
+import { useMemo } from 'react'
+import { Badge, Button, Input, Label, Switch, ToggleGroup, ToggleGroupItem } from '@/components/ui'
+import type { OperationType, ResourceType } from '@/lib/security/secure-db/types'
+import { FunnelSimple, MagnifyingGlass, X } from '@phosphor-icons/react'
+
+interface AuditLogFiltersProps {
+ searchTerm: string
+ onSearchChange: (value: string) => void
+ selectedOperations: OperationType[]
+ onOperationsChange: (operations: OperationType[]) => void
+ selectedResources: ResourceType[]
+ onResourcesChange: (resources: ResourceType[]) => void
+ showFailuresOnly: boolean
+ onShowFailuresChange: (value: boolean) => void
+ availableOperations?: OperationType[]
+ availableResources?: ResourceType[]
+ onReset?: () => void
+}
+
+const DEFAULT_OPERATIONS: OperationType[] = ['CREATE', 'READ', 'UPDATE', 'DELETE']
+const DEFAULT_RESOURCES: ResourceType[] = [
+ 'user',
+ 'workflow',
+ 'luaScript',
+ 'pageConfig',
+ 'modelSchema',
+ 'comment',
+ 'componentNode',
+ 'componentConfig',
+ 'cssCategory',
+ 'dropdownConfig',
+ 'tenant',
+ 'powerTransfer',
+ 'smtpConfig',
+ 'credential'
+]
+
+export function AuditLogFilters({
+ searchTerm,
+ onSearchChange,
+ selectedOperations,
+ onOperationsChange,
+ selectedResources,
+ onResourcesChange,
+ showFailuresOnly,
+ onShowFailuresChange,
+ availableOperations,
+ availableResources,
+ onReset
+}: AuditLogFiltersProps) {
+ const operationOptions = availableOperations || DEFAULT_OPERATIONS
+ const resourceOptions = useMemo(
+ () => availableResources || DEFAULT_RESOURCES,
+ [availableResources]
+ )
+
+ return (
+
+
+
+ Filters
+
+
+
+
+
+
+
+ onSearchChange(event.target.value)}
+ className="pl-9"
+ />
+
+
+
+
+
+
+
Show only unsuccessful operations
+
+
+
+
+
+
+
+
+ onOperationsChange(value as OperationType[])}
+ className="flex flex-wrap gap-2"
+ >
+ {operationOptions.map((operation) => (
+
+ {operation}
+
+ ))}
+
+
+
+
+
+
+ Select one or more
+
+
+ {resourceOptions.map((resource) => {
+ const isSelected = selectedResources.includes(resource)
+ return (
+
+ )
+ })}
+
+
+
+
+
+
+ {selectedOperations.length > 0 && (
+
+ Operations:
+ {selectedOperations.join(', ')}
+
+ )}
+ {selectedResources.length > 0 && (
+
+ Resources:
+ {selectedResources.join(', ')}
+
+ )}
+ {showFailuresOnly && (
+
+
+ Failures
+
+ )}
+ {selectedOperations.length === 0 &&
+ selectedResources.length === 0 &&
+ !showFailuresOnly && (
+ No filters applied
+ )}
+
+
+ {onReset && (
+
+ )}
+
+
+ )
+}
+
+function WarningIcon() {
+ return
+}
diff --git a/frontends/nextjs/src/components/misc/viewers/audit-log/LogTable.tsx b/frontends/nextjs/src/components/misc/viewers/audit-log/LogTable.tsx
new file mode 100644
index 000000000..21cbbdae7
--- /dev/null
+++ b/frontends/nextjs/src/components/misc/viewers/audit-log/LogTable.tsx
@@ -0,0 +1,124 @@
+import { Badge, Card, CardContent, CardHeader, CardTitle, ScrollArea, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui'
+import type { AuditLog, OperationType, ResourceType } from '@/lib/security/secure-db/types'
+import { ArrowDown, ArrowUp, ShieldCheck, User as UserIcon, WarningCircle } from '@phosphor-icons/react'
+
+interface LogTableProps {
+ logs: AuditLog[]
+ sortField?: keyof AuditLog | null
+ sortDirection?: 'asc' | 'desc'
+ onSortChange?: (field: keyof AuditLog) => void
+}
+
+const OPERATION_COLORS: Record = {
+ CREATE: 'bg-green-100 text-green-800',
+ READ: 'bg-blue-100 text-blue-800',
+ UPDATE: 'bg-yellow-100 text-yellow-800',
+ DELETE: 'bg-red-100 text-red-800'
+}
+
+const RESOURCE_ICONS: Partial> = {
+ user: ,
+ credential:
+}
+
+export function LogTable({ logs, sortField, sortDirection = 'asc', onSortChange }: LogTableProps) {
+ const handleSort = (field: keyof AuditLog) => {
+ onSortChange?.(field)
+ }
+
+ return (
+
+
+ Audit Log
+
+
+
+
+
+
+
+ User
+ Operation
+ Resource
+ Status
+ Details
+
+
+
+ {logs.length === 0 ? (
+
+
+ No audit events to display
+
+
+ ) : (
+ logs.map((log) => (
+
+
+ {new Date(log.timestamp).toLocaleString()}
+
+ {log.username}
+
+ {log.operation}
+
+
+ {RESOURCE_ICONS[log.resource] || }
+ {log.resource}
+
+
+ {log.success ? (
+
+
+ Success
+
+ ) : (
+
+
+ Failed
+
+ )}
+
+
+ {log.errorMessage || '—'}
+
+
+ ))
+ )}
+
+
+
+
+
+ )
+}
+
+interface SortableHeaderProps {
+ field: keyof AuditLog
+ label: string
+ sortField?: keyof AuditLog | null
+ sortDirection?: 'asc' | 'desc'
+ onSort?: (field: keyof AuditLog) => void
+}
+
+function SortableHeader({ field, label, sortField, sortDirection = 'asc', onSort }: SortableHeaderProps) {
+ const isActive = sortField === field
+ const Icon = sortDirection === 'asc' ? ArrowUp : ArrowDown
+
+ return (
+ onSort?.(field)}
+ >
+
+ {label}
+ {isActive && }
+
+
+ )
+}
diff --git a/frontends/nextjs/src/components/misc/viewers/model-list/DetailsDrawer.tsx b/frontends/nextjs/src/components/misc/viewers/model-list/DetailsDrawer.tsx
new file mode 100644
index 000000000..07bc37865
--- /dev/null
+++ b/frontends/nextjs/src/components/misc/viewers/model-list/DetailsDrawer.tsx
@@ -0,0 +1,73 @@
+import { Badge, Button, ScrollArea, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui'
+import type { ModelSchema } from '@/lib/schema-types'
+import { getFieldLabel } from '@/lib/schema-utils'
+
+interface DetailsDrawerProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ record: any | null
+ model: ModelSchema
+}
+
+export function DetailsDrawer({ open, onOpenChange, record, model }: DetailsDrawerProps) {
+ const renderValue = (fieldName: string) => {
+ const field = model.fields.find((item) => item.name === fieldName)
+ if (!field) return null
+ const value = record?.[fieldName]
+
+ if (value === null || value === undefined || value === '') {
+ return Not provided
+ }
+
+ switch (field.type) {
+ case 'boolean':
+ return value ? (
+
+ Yes
+
+ ) : (
+ No
+ )
+ case 'date':
+ case 'datetime':
+ return new Date(value).toLocaleString()
+ case 'json':
+ return {JSON.stringify(value, null, 2)}
+ default:
+ return {String(value)}
+ }
+ }
+
+ return (
+
+
+
+ {model.label || model.name} details
+ Review the full record and its attributes.
+
+
+
+
+
+
+ {model.fields.map((field) => (
+
+
{getFieldLabel(field)}
+
{renderValue(field.name)}
+ {field.helpText && (
+
{field.helpText}
+ )}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontends/nextjs/src/components/misc/viewers/model-list/ModelFilters.tsx b/frontends/nextjs/src/components/misc/viewers/model-list/ModelFilters.tsx
new file mode 100644
index 000000000..465f5605d
--- /dev/null
+++ b/frontends/nextjs/src/components/misc/viewers/model-list/ModelFilters.tsx
@@ -0,0 +1,82 @@
+import { Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
+import type { FieldSchema, ModelSchema } from '@/lib/schema-types'
+import { MagnifyingGlass } from '@phosphor-icons/react'
+
+interface ModelFiltersProps {
+ model: ModelSchema
+ filters: Record
+ searchTerm: string
+ onSearchChange: (value: string) => void
+ onFilterChange: (field: string, value: any) => void
+}
+
+function getFilterableFields(model: ModelSchema): FieldSchema[] {
+ if (model.listFilter) {
+ return model.fields.filter((field) => model.listFilter?.includes(field.name))
+ }
+ return model.fields.filter((field) => field.type === 'select' || field.type === 'boolean')
+}
+
+export function ModelFilters({ model, filters, searchTerm, onSearchChange, onFilterChange }: ModelFiltersProps) {
+ const filterFields = getFilterableFields(model)
+
+ return (
+
+
+
+
+
+ onSearchChange(event.target.value)}
+ className="pl-9"
+ />
+
+
+
+ {filterFields.length > 0 && (
+
+ {filterFields.map((field) => (
+
+
+ {field.type === 'select' ? (
+
+ ) : (
+
+ )}
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/frontends/nextjs/src/components/misc/viewers/model-list/ModelTable.tsx b/frontends/nextjs/src/components/misc/viewers/model-list/ModelTable.tsx
new file mode 100644
index 000000000..8ecf94a36
--- /dev/null
+++ b/frontends/nextjs/src/components/misc/viewers/model-list/ModelTable.tsx
@@ -0,0 +1,148 @@
+import { Badge, Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui'
+import type { FieldSchema, ModelSchema } from '@/lib/schema-types'
+import { getFieldLabel } from '@/lib/schema-utils'
+import { ArrowDown, ArrowUp, Pencil, Trash } from '@phosphor-icons/react'
+import { ReactNode } from 'react'
+
+interface ModelTableProps {
+ model: ModelSchema
+ records: any[]
+ displayFields: string[]
+ sortField?: string | null
+ sortDirection?: 'asc' | 'desc'
+ onSortChange?: (field: string) => void
+ onEdit?: (record: any) => void
+ onDelete?: (id: string) => void
+ onRowClick?: (record: any) => void
+ renderRelationValue?: (value: string, field: FieldSchema) => ReactNode
+}
+
+export function ModelTable({
+ model,
+ records,
+ displayFields,
+ sortField,
+ sortDirection = 'asc',
+ onSortChange,
+ onEdit,
+ onDelete,
+ onRowClick,
+ renderRelationValue
+}: ModelTableProps) {
+ const actionColumns = onEdit || onDelete ? 1 : 0
+
+ const renderCellValue = (record: any, fieldName: string) => {
+ const field = model.fields.find((item) => item.name === fieldName)
+ if (!field) return null
+
+ const value = record[fieldName]
+
+ if (value === null || value === undefined) {
+ return —
+ }
+
+ if (field.type === 'relation' && typeof value === 'string' && renderRelationValue) {
+ return renderRelationValue(value, field)
+ }
+
+ switch (field.type) {
+ case 'boolean':
+ return value ? Yes : No
+ case 'date':
+ case 'datetime':
+ return new Date(value).toLocaleString()
+ case 'json':
+ return {JSON.stringify(value)}
+ default:
+ return typeof value === 'string' && value.length > 60 ? `${value.slice(0, 60)}…` : String(value)
+ }
+ }
+
+ return (
+
+
+
+
+ {displayFields.map((fieldName) => {
+ const field = model.fields.find((item) => item.name === fieldName)
+ if (!field) return null
+ const isSortable = field.sortable !== false
+ const isActive = sortField === fieldName
+ const Icon = sortDirection === 'asc' ? ArrowUp : ArrowDown
+
+ return (
+ isSortable && onSortChange?.(fieldName)}
+ >
+
+
+ {getFieldLabel(field)}
+
+ {isSortable && isActive && }
+
+
+ )
+ })}
+ {(onEdit || onDelete) && Actions}
+
+
+
+ {records.length === 0 ? (
+
+
+ No records to display
+
+
+ ) : (
+ records.map((record) => (
+ onRowClick?.(record)}
+ >
+ {displayFields.map((fieldName) => (
+
+ {renderCellValue(record, fieldName)}
+
+ ))}
+ {(onEdit || onDelete) && (
+
+
+ {onEdit && (
+
+ )}
+ {onDelete && (
+
+ )}
+
+
+ )}
+
+ ))
+ )}
+
+
+
+ )
+}