feat(HAI-004): create SettingsModal component
This commit is contained in:
104
packages/dashboard/app/components/SettingsModal.tsx
Normal file
104
packages/dashboard/app/components/SettingsModal.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { Settings } from "@hai/core";
|
||||
import { fetchSettings, updateSettings } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
interface SettingsModalProps {
|
||||
onClose: () => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
}
|
||||
|
||||
export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
|
||||
const [form, setForm] = useState<Settings>({ maxConcurrent: 2, pollIntervalMs: 15000 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings()
|
||||
.then((s) => {
|
||||
setForm(s);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
addToast(err.message, "error");
|
||||
setLoading(false);
|
||||
});
|
||||
}, [addToast]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
document.addEventListener("keydown", handleKey);
|
||||
return () => document.removeEventListener("keydown", handleKey);
|
||||
}, [onClose]);
|
||||
|
||||
const handleOverlayClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
try {
|
||||
await updateSettings(form);
|
||||
addToast("Settings saved", "success");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
}
|
||||
}, [form, onClose, addToast]);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open" onClick={handleOverlayClick}>
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h3>Settings</h3>
|
||||
<button className="modal-close" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div style={{ padding: "20px", textAlign: "center" }}>Loading…</div>
|
||||
) : (
|
||||
<div className="settings-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="maxConcurrent">Max Concurrent Tasks</label>
|
||||
<input
|
||||
id="maxConcurrent"
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
value={form.maxConcurrent}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, maxConcurrent: Number(e.target.value) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="pollIntervalMs">Poll Interval (ms)</label>
|
||||
<input
|
||||
id="pollIntervalMs"
|
||||
type="number"
|
||||
min={5000}
|
||||
step={1000}
|
||||
value={form.pollIntervalMs}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, pollIntervalMs: Number(e.target.value) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="modal-actions">
|
||||
<button className="btn btn-sm" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-primary btn-sm" onClick={handleSave} disabled={loading}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user