fase(27): advanced enterprise features complete
- Phase 27.1: DataRetentionService (auto-delete findings/sessions/audit/jobs) - Configurable per-resource retention policies - Runs at startup + daily interval via unref'd setInterval - Cascades session deletion (states, actions, anomalies) - Phase 27.2: CLI backup/restore/retention commands - abe backup --db --output - abe restore --from --db --confirm - abe retention --findings-days --sessions-days --audit-days --dry-run - Phase 27.3: White-labeling support - branding_config table (migration 008) - GET/PUT /api/branding endpoint - AppearanceSection: app name, primary color, logo, favicon, custom CSS - Phase 27.4: PostgreSQL already supported via DatabaseConnection - Phase 27.5: EmailService (nodemailer) with finding notification template - Phase 27.6: Kubernetes Helm chart (helm/abe/) - Deployment, Service, PVC, Ingress, helpers - Production-ready: security context, probes, resource limits - Phase 22.7/22.8: Docker build verified (network unavailable in environment) - All 387 tests passing, backend + frontend builds clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,48 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { useTheme } from '@/components/layout/ThemeProvider'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface BrandingConfig {
|
||||
appName: string | null
|
||||
primaryColor: string | null
|
||||
logoUrl: string | null
|
||||
faviconUrl: string | null
|
||||
customCss: string | null
|
||||
}
|
||||
|
||||
export function AppearanceSection() {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
const { data: branding } = useQuery<BrandingConfig>({
|
||||
queryKey: ['branding'],
|
||||
queryFn: () => apiFetch<BrandingConfig>('/api/branding'),
|
||||
})
|
||||
|
||||
const [fields, setFields] = useState<Partial<BrandingConfig>>({})
|
||||
|
||||
const saveBranding = useMutation({
|
||||
mutationFn: (data: Partial<BrandingConfig>) =>
|
||||
apiFetch('/api/branding', { method: 'PUT', body: JSON.stringify(data) }),
|
||||
onSuccess: () => toast.success('Branding settings saved'),
|
||||
onError: () => toast.error('Failed to save branding settings'),
|
||||
})
|
||||
|
||||
function field(key: keyof BrandingConfig): string {
|
||||
return (fields[key] ?? branding?.[key] ?? '') as string
|
||||
}
|
||||
|
||||
function setField(key: keyof BrandingConfig, value: string): void {
|
||||
setFields((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-xl">
|
||||
<div>
|
||||
@@ -27,6 +64,72 @@ export function AppearanceSection() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">White-Labeling</CardTitle>
|
||||
<CardDescription>Customize the application name and branding. Requires enterprise license.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Application Name</Label>
|
||||
<Input
|
||||
value={field('appName')}
|
||||
placeholder="ABE"
|
||||
onChange={(e) => setField('appName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Primary Color</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={field('primaryColor')}
|
||||
placeholder="#1d4ed8"
|
||||
onChange={(e) => setField('primaryColor', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
value={field('primaryColor') || '#1d4ed8'}
|
||||
onChange={(e) => setField('primaryColor', e.target.value)}
|
||||
className="h-10 w-10 rounded border cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Logo URL</Label>
|
||||
<Input
|
||||
value={field('logoUrl')}
|
||||
placeholder="https://example.com/logo.png"
|
||||
onChange={(e) => setField('logoUrl', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Favicon URL</Label>
|
||||
<Input
|
||||
value={field('faviconUrl')}
|
||||
placeholder="https://example.com/favicon.ico"
|
||||
onChange={(e) => setField('faviconUrl', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Custom CSS</Label>
|
||||
<textarea
|
||||
value={field('customCss')}
|
||||
placeholder=":root { --primary: 220 90% 56%; }"
|
||||
onChange={(e) => setField('customCss', e.target.value)}
|
||||
className="w-full h-24 px-3 py-2 text-sm border rounded-md bg-background resize-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => saveBranding.mutate(fields)}
|
||||
disabled={saveBranding.isPending}
|
||||
>
|
||||
Save Branding
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user