Files
cameleer-server/ui/src/pages/Admin/AppConfigPage.tsx

173 lines
5.5 KiB
TypeScript
Raw Normal View History

import { useMemo, useCallback } from 'react';
import { useNavigate } from 'react-router';
import { DataTable, Badge, MonoText, useToast } from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system';
import { useAllApplicationConfigs, useUpdateApplicationConfig } from '../../api/queries/commands';
import type { ApplicationConfig } from '../../api/queries/commands';
import styles from './AppConfigPage.module.css';
function timeAgo(iso?: string): string {
if (!iso) return '\u2014';
const diff = Date.now() - new Date(iso).getTime();
const secs = Math.floor(diff / 1000);
if (secs < 60) return `${secs}s ago`;
const mins = Math.floor(secs / 60);
if (mins < 60) return `${mins}m ago`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h ago`;
return `${Math.floor(hours / 24)}d ago`;
}
function logLevelColor(level?: string): string {
switch (level?.toUpperCase()) {
case 'ERROR': return 'error';
case 'WARN': return 'warning';
case 'DEBUG': return 'running';
default: return 'success';
}
}
export default function AppConfigPage() {
const navigate = useNavigate();
const { toast } = useToast();
const { data: configs } = useAllApplicationConfigs();
const updateConfig = useUpdateApplicationConfig();
const handleChange = useCallback((config: ApplicationConfig, field: string, value: string | boolean) => {
const updated = { ...config, [field]: value };
updateConfig.mutate(updated, {
onSuccess: (saved) => {
toast({ title: 'Config updated', description: `${config.application}: ${field} \u2192 ${value} (v${saved.version})`, variant: 'success' });
},
onError: () => {
toast({ title: 'Config update failed', description: config.application, variant: 'error' });
},
});
}, [updateConfig, toast]);
const columns: Column<ApplicationConfig>[] = useMemo(() => [
{
key: '_inspect',
header: '',
width: '36px',
render: (_val, row) => (
<button
className={styles.inspectLink}
title="Open agent page"
onClick={(e) => {
e.stopPropagation();
navigate(`/agents/${row.application}`);
}}
>
&#x2197;
</button>
),
},
{
key: 'application',
header: 'Application',
sortable: true,
render: (_val, row) => <MonoText size="sm">{row.application}</MonoText>,
},
{
key: 'logForwardingLevel',
header: 'Log Level',
render: (_val, row) => (
<select
className={styles.inlineSelect}
value={row.logForwardingLevel ?? 'INFO'}
onChange={(e) => { e.stopPropagation(); handleChange(row, 'logForwardingLevel', e.target.value); }}
disabled={updateConfig.isPending}
>
<option value="ERROR">ERROR</option>
<option value="WARN">WARN</option>
<option value="INFO">INFO</option>
<option value="DEBUG">DEBUG</option>
</select>
),
},
{
key: 'engineLevel',
header: 'Engine Level',
render: (_val, row) => (
<select
className={styles.inlineSelect}
value={row.engineLevel ?? 'REGULAR'}
onChange={(e) => { e.stopPropagation(); handleChange(row, 'engineLevel', e.target.value); }}
disabled={updateConfig.isPending}
>
<option value="NONE">None</option>
<option value="MINIMAL">Minimal</option>
<option value="REGULAR">Regular</option>
<option value="COMPLETE">Complete</option>
</select>
),
},
{
key: 'payloadCaptureMode',
header: 'Payload Capture',
render: (_val, row) => (
<select
className={styles.inlineSelect}
value={row.payloadCaptureMode ?? 'NONE'}
onChange={(e) => { e.stopPropagation(); handleChange(row, 'payloadCaptureMode', e.target.value); }}
disabled={updateConfig.isPending}
>
<option value="NONE">None</option>
<option value="INPUT">Input</option>
<option value="OUTPUT">Output</option>
<option value="BOTH">Both</option>
</select>
),
},
{
key: 'metricsEnabled',
header: 'Metrics',
width: '80px',
render: (_val, row) => (
<label className={styles.inlineToggle} onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={row.metricsEnabled}
onChange={(e) => handleChange(row, 'metricsEnabled', e.target.checked)}
disabled={updateConfig.isPending}
/>
<span>{row.metricsEnabled ? 'On' : 'Off'}</span>
</label>
),
},
{
key: 'tracedProcessors',
header: 'Traced',
width: '70px',
render: (_val, row) => {
const count = row.tracedProcessors ? Object.keys(row.tracedProcessors).length : 0;
return count > 0
? <Badge label={`${count}`} color="running" variant="filled" />
: <MonoText size="xs">0</MonoText>;
},
},
{
key: 'version',
header: 'v',
width: '40px',
render: (_val, row) => <MonoText size="xs">{row.version}</MonoText>,
},
{
key: 'updatedAt',
header: 'Updated',
render: (_val, row) => <MonoText size="xs">{timeAgo(row.updatedAt)}</MonoText>,
},
], [navigate, handleChange, updateConfig.isPending]);
return (
<div>
<DataTable<ApplicationConfig>
columns={columns}
data={configs ?? []}
pageSize={50}
/>
</div>
);
}