feat(settings): add log retention settings page
Sysadmin/admin-only settings page for configuring how long device log history is kept before pruning. GET/PUT /api/settings/log-retention, new LogRetentionSettings page, nav entry, and route. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
// frontend/src/pages/settings/LogRetentionSettings.jsx
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import api from '@/lib/api'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import PageHeader from '@/components/ui/PageHeader'
|
||||
import Card from '@/components/ui/Card'
|
||||
import Spinner from '@/components/ui/Spinner'
|
||||
import FormField from '@/components/ui/FormField'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
const PRESETS = [30, 90, 180, 365]
|
||||
|
||||
export default function LogRetentionSettings() {
|
||||
const { toast } = useToast()
|
||||
|
||||
const [days, setDays] = useState(180)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
api.get('/settings/log-retention')
|
||||
.then(data => setDays(data.days))
|
||||
.catch(e => toast.danger('Failed to load settings', e.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
const updated = await api.put('/settings/log-retention', { days: Number(days) })
|
||||
setDays(updated.days)
|
||||
toast.success('Settings saved', `Device logs are now retained for ${updated.days} days.`)
|
||||
} catch (e) {
|
||||
toast.danger('Failed to save', e.message)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-wrapper page-wrapper--centered" style={{ '--page-content-max-width': 'var(--content-max-width-md)' }}>
|
||||
<PageHeader
|
||||
title="Log Retention"
|
||||
subtitle="Controls how long device MQTT logs (Device Logs, Info, Heartbeats, Commands) are kept before automatic deletion."
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-12)' }}>
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Card variant="flat">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-5)' }}>
|
||||
<FormField
|
||||
label="Retention period (days)"
|
||||
name="retention_days"
|
||||
type="number"
|
||||
value={days}
|
||||
onChange={e => setDays(e.target.value)}
|
||||
hint="Applies globally to every device — there is no per-device override."
|
||||
inputProps={{ min: 1, max: 3650 }}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', gap: 'var(--space-2)', flexWrap: 'wrap' }}>
|
||||
{PRESETS.map(p => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
onClick={() => setDays(p)}
|
||||
style={{
|
||||
height: 28,
|
||||
paddingInline: 'var(--space-3)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
border: `1px solid ${Number(days) === p ? 'var(--color-primary)' : 'var(--color-border-strong)'}`,
|
||||
background: Number(days) === p ? 'var(--color-primary-subtle)' : 'var(--color-bg-elevated)',
|
||||
color: Number(days) === p ? 'var(--color-primary)' : 'var(--color-text-secondary)',
|
||||
fontSize: 'var(--font-size-xs)',
|
||||
fontWeight: 'var(--font-weight-semibold)',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-family-base)',
|
||||
}}
|
||||
>
|
||||
{p === 30 ? '1 month' : p === 90 ? '3 months' : p === 180 ? '6 months' : '1 year'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button variant="primary" loading={saving} onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user