feat: add app_config table with runtime log level control and POS structured logging #5
@@ -13,8 +13,9 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { moduleListOptions, moduleMutations, moduleKeys } from '@/api/modules'
|
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'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
interface StoreSettings {
|
interface StoreSettings {
|
||||||
@@ -236,6 +237,9 @@ function SettingsPage() {
|
|||||||
|
|
||||||
{/* Modules */}
|
{/* Modules */}
|
||||||
<ModulesCard />
|
<ModulesCard />
|
||||||
|
|
||||||
|
{/* App Configuration */}
|
||||||
|
<AppConfigCard />
|
||||||
</div>
|
</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 }) {
|
function LocationCard({ location }: { location: Location }) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user