upd: gabungkan Settings menjadi 1 form dengan tombol Simpan Semua
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1482,7 +1482,7 @@ const CONFIG_DEFINITIONS: { key: string; label: string; description: string; pla
|
|||||||
function SettingsPanel() {
|
function SettingsPanel() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [values, setValues] = useState<Record<string, string>>({})
|
const [values, setValues] = useState<Record<string, string>>({})
|
||||||
const [saved, setSaved] = useState<Record<string, boolean>>({})
|
const [saved, setSaved] = useState(false)
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'config'],
|
queryKey: ['admin', 'config'],
|
||||||
@@ -1500,81 +1500,86 @@ function SettingsPanel() {
|
|||||||
setValues(initial)
|
setValues(initial)
|
||||||
}, [configs])
|
}, [configs])
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveAllMutation = useMutation({
|
||||||
mutationFn: ({ key, value }: { key: string; value: string }) =>
|
mutationFn: async (vals: Record<string, string>) => {
|
||||||
fetch('/api/admin/config', {
|
await Promise.all(
|
||||||
method: 'PUT',
|
CONFIG_DEFINITIONS.map((def) =>
|
||||||
credentials: 'include',
|
fetch('/api/admin/config', {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
method: 'PUT',
|
||||||
body: JSON.stringify({ key, value }),
|
credentials: 'include',
|
||||||
}).then((r) => r.json()),
|
headers: { 'Content-Type': 'application/json' },
|
||||||
onSuccess: (_, { key }) => {
|
body: JSON.stringify({ key: def.key, value: vals[def.key] ?? '' }),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: ['admin', 'config'] })
|
qc.invalidateQueries({ queryKey: ['admin', 'config'] })
|
||||||
setSaved((prev) => ({ ...prev, [key]: true }))
|
setSaved(true)
|
||||||
setTimeout(() => setSaved((prev) => ({ ...prev, [key]: false })), 2000)
|
setTimeout(() => setSaved(false), 2000)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hasUnconfigured = CONFIG_DEFINITIONS.some((def) => !configs.find((c) => c.key === def.key))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Title order={3}>Settings</Title>
|
<Title order={3}>Settings</Title>
|
||||||
<Text size="sm" c="dimmed">Konfigurasi runtime — perubahan langsung berlaku tanpa rebuild atau redeploy.</Text>
|
<Text size="sm" c="dimmed">Konfigurasi runtime — perubahan langsung berlaku tanpa rebuild atau redeploy.</Text>
|
||||||
|
|
||||||
{isLoading ? <Center><Loader /></Center> : (
|
{isLoading ? <Center><Loader /></Center> : (
|
||||||
<Stack gap="md">
|
<Paper withBorder p="lg" radius="md">
|
||||||
{CONFIG_DEFINITIONS.map((def) => {
|
<Stack gap="md">
|
||||||
const existing = configs.find((c) => c.key === def.key)
|
{CONFIG_DEFINITIONS.map((def) => {
|
||||||
return (
|
const existing = configs.find((c) => c.key === def.key)
|
||||||
<Paper key={def.key} withBorder p="lg" radius="md">
|
return (
|
||||||
<Stack gap="xs">
|
<Stack key={def.key} gap={4}>
|
||||||
<Group justify="space-between" align="flex-start">
|
<Group justify="space-between" align="flex-end">
|
||||||
<div>
|
<div>
|
||||||
<Text fw={600} size="sm">{def.label}</Text>
|
<Text fw={600} size="sm">{def.label}</Text>
|
||||||
<Text size="xs" c="dimmed" ff="monospace">{def.key}</Text>
|
<Text size="xs" c="dimmed" ff="monospace">{def.key}</Text>
|
||||||
</div>
|
</div>
|
||||||
{existing && (
|
{existing
|
||||||
<Text size="xs" c="dimmed">
|
? <Text size="xs" c="dimmed">Diupdate {new Date(existing.updatedAt).toLocaleString('id-ID')}</Text>
|
||||||
Diupdate {new Date(existing.updatedAt).toLocaleString('id-ID')}
|
: <Badge color="orange" variant="light" size="xs">Belum dikonfigurasi</Badge>
|
||||||
</Text>
|
}
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="xs" c="dimmed">{def.description}</Text>
|
<Text size="xs" c="dimmed">{def.description}</Text>
|
||||||
<Group gap="xs" align="flex-end">
|
<input
|
||||||
<Box style={{ flex: 1 }}>
|
type={def.secret ? 'password' : 'text'}
|
||||||
<input
|
style={{
|
||||||
type={def.secret ? 'password' : 'text'}
|
width: '100%',
|
||||||
style={{
|
padding: '8px 12px',
|
||||||
width: '100%',
|
borderRadius: 6,
|
||||||
padding: '8px 12px',
|
border: '1px solid var(--mantine-color-default-border)',
|
||||||
borderRadius: 6,
|
background: 'var(--mantine-color-body)',
|
||||||
border: '1px solid var(--mantine-color-default-border)',
|
color: 'var(--mantine-color-text)',
|
||||||
background: 'var(--mantine-color-body)',
|
fontSize: 13,
|
||||||
color: 'var(--mantine-color-text)',
|
fontFamily: 'monospace',
|
||||||
fontSize: 13,
|
}}
|
||||||
fontFamily: 'monospace',
|
value={values[def.key] ?? ''}
|
||||||
}}
|
onChange={(e) => setValues((prev) => ({ ...prev, [def.key]: e.target.value }))}
|
||||||
value={values[def.key] ?? ''}
|
placeholder={def.placeholder}
|
||||||
onChange={(e) => setValues((prev) => ({ ...prev, [def.key]: e.target.value }))}
|
/>
|
||||||
placeholder={def.placeholder}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
color={saved[def.key] ? 'green' : 'blue'}
|
|
||||||
loading={saveMutation.isPending && saveMutation.variables?.key === def.key}
|
|
||||||
onClick={() => saveMutation.mutate({ key: def.key, value: values[def.key] ?? '' })}
|
|
||||||
>
|
|
||||||
{saved[def.key] ? 'Tersimpan' : 'Simpan'}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
{!existing && (
|
|
||||||
<Badge color="orange" variant="light" size="xs">Belum dikonfigurasi — data tidak akan ter-load</Badge>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
)
|
||||||
)
|
})}
|
||||||
})}
|
|
||||||
</Stack>
|
{hasUnconfigured && (
|
||||||
|
<Text size="xs" c="orange">Beberapa konfigurasi belum diisi — data tidak akan ter-load sampai disimpan.</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button
|
||||||
|
color={saved ? 'green' : 'blue'}
|
||||||
|
loading={saveAllMutation.isPending}
|
||||||
|
onClick={() => saveAllMutation.mutate(values)}
|
||||||
|
>
|
||||||
|
{saved ? 'Tersimpan' : 'Simpan Semua'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user