feat: add app configuration UI to settings page
All checks were successful
CI / ci (pull_request) Successful in 17s
CI / e2e (pull_request) Successful in 47s

Log level dropdown on the settings page lets admins change the application
log verbosity at runtime without restarting. Uses the new /v1/config API
with the existing settings.view/settings.edit permissions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-04 19:04:31 +00:00
parent 772d5578ad
commit aa5b53920d

View File

@@ -13,8 +13,9 @@ import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Switch } from '@/components/ui/switch'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { moduleListOptions, moduleMutations, moduleKeys } from '@/api/modules'
import { Save, Plus, Trash2, MapPin, Building, ImageIcon, Blocks, Lock } from 'lucide-react'
import { Save, Plus, Trash2, MapPin, Building, ImageIcon, Blocks, Lock, Settings2 } from 'lucide-react'
import { toast } from 'sonner'
interface StoreSettings {
@@ -236,6 +237,9 @@ function SettingsPage() {
{/* Modules */}
<ModulesCard />
{/* App Configuration */}
<AppConfigCard />
</div>
)
}
@@ -296,6 +300,82 @@ function ModulesCard() {
)
}
interface AppConfigEntry {
key: string
value: string | null
description: string | null
updatedAt: string
}
const LOG_LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'] as const
function configOptions() {
return queryOptions({
queryKey: ['config'],
queryFn: () => api.get<{ data: AppConfigEntry[] }>('/v1/config'),
})
}
function AppConfigCard() {
const queryClient = useQueryClient()
const hasPermission = useAuthStore((s) => s.hasPermission)
const canEdit = hasPermission('settings.edit')
const { data: configData, isLoading } = useQuery(configOptions())
const configs = configData?.data ?? []
const logLevel = configs.find((c) => c.key === 'log_level')?.value ?? 'info'
const updateMutation = useMutation({
mutationFn: ({ key, value }: { key: string; value: string }) =>
api.patch<AppConfigEntry>(`/v1/config/${key}`, { value }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['config'] })
toast.success('Configuration updated')
},
onError: (err) => toast.error(err.message),
})
return (
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Settings2 className="h-5 w-5" />App Configuration
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-12 w-full" />
) : (
<div className="space-y-4">
<div className="flex items-center justify-between p-3 rounded-md border">
<div className="min-w-0">
<span className="font-medium text-sm">Log Level</span>
<p className="text-xs text-muted-foreground mt-0.5">Controls the verbosity of application logs</p>
</div>
<Select
value={logLevel}
onValueChange={(value) => updateMutation.mutate({ key: 'log_level', value })}
disabled={!canEdit || updateMutation.isPending}
>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LOG_LEVELS.map((level) => (
<SelectItem key={level} value={level}>
{level}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
</CardContent>
</Card>
)
}
function LocationCard({ location }: { location: Location }) {
const queryClient = useQueryClient()
const [editing, setEditing] = useState(false)