feat(services): initial config on ADD SERVICE — schema modal in DeckyCard, MazeNET drag, and Inspector
- DeckyServiceAddRequest gains an optional `config: dict` field, validated
against the service's config_schema before any state mutation (400 on
bad type, no half-written rows).
- Engine: add_service threads `config` into _add_topology_service /
_add_fleet_service, persisting validated cfg to decky_config.service_config
BEFORE compose regen so the first `up -d --build` materialises the env on
the new container. No follow-up apply needed.
- Frontend: shared AddServiceConfigModal — same wizard accordion shape, used by:
* DeckyCard's ADD SERVICE picker (Fleet & MazeNET inspectors via shared component)
* MazeNET Inspector's ADD SERVICE picker
* MazeNET palette drag-drop onto a deployed decky
Empty-schema services short-circuit to a one-click add (no modal flash).
Operator can cancel; errors surface in the modal.
- Tests: add_service config plumbing — persist, drop unknown keys, 400-equivalent
on bad types, back-compat empty-config.
- Drive-by: fix stale repo-method names in test_services_live.py
(create_topology_decky → add_topology_decky, get_topology_decky → list+pick helper,
service.added → service_added topic).
This commit is contained in:
@@ -9,6 +9,7 @@ import { useToast } from './Toasts/useToast';
|
||||
import Modal from './Modal/Modal';
|
||||
import { useServiceRegistry } from '../hooks/useServiceRegistry';
|
||||
import ServiceConfigForm from './ServiceConfigForm';
|
||||
import AddServiceConfigModal from './AddServiceConfigModal';
|
||||
import ServiceConfigFields, {
|
||||
type FormState as SvcFormState,
|
||||
type ServiceConfigFieldDTO as SvcFieldDTO,
|
||||
@@ -164,6 +165,9 @@ const DeckyCard: React.FC<DeckyCardProps> = ({
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [opError, setOpError] = useState<string | null>(null);
|
||||
const [openCfgSvc, setOpenCfgSvc] = useState<string | null>(null);
|
||||
// Pending add — when non-null, AddServiceConfigModal is mounted and
|
||||
// will either auto-fire onConfirm (no schema fields) or show the form.
|
||||
const [pendingAdd, setPendingAdd] = useState<{ deckyName: string; slug: string } | null>(null);
|
||||
|
||||
const removeService = async (slug: string) => {
|
||||
setOpError(null);
|
||||
@@ -182,22 +186,30 @@ const DeckyCard: React.FC<DeckyCardProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const addService = async () => {
|
||||
const beginAdd = () => {
|
||||
if (!addSlug) return;
|
||||
setOpError(null);
|
||||
setBusy(addSlug);
|
||||
setPendingAdd({ deckyName: decky.name, slug: addSlug });
|
||||
};
|
||||
|
||||
const confirmAdd = async (deckyName: string, slug: string, cfg: Record<string, unknown>) => {
|
||||
setBusy(slug);
|
||||
try {
|
||||
const { data } = await api.post<{ services: string[] }>(
|
||||
`/deckies/${encodeURIComponent(decky.name)}/services`,
|
||||
{ name: addSlug },
|
||||
`/deckies/${encodeURIComponent(deckyName)}/services`,
|
||||
{ name: slug, config: cfg },
|
||||
);
|
||||
onServicesChanged(decky.name, data.services);
|
||||
onServicesChanged(deckyName, data.services);
|
||||
setPendingAdd(null);
|
||||
setAddOpen(false);
|
||||
setAddSlug('');
|
||||
} catch (err) {
|
||||
// Re-raise so the modal can surface the error in its own status row.
|
||||
// Also mirror onto opError for the inline picker case.
|
||||
const msg = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail
|
||||
?? 'Add failed.';
|
||||
setOpError(msg);
|
||||
throw err;
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
@@ -343,7 +355,7 @@ const DeckyCard: React.FC<DeckyCardProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
disabled={!addSlug || busy === addSlug}
|
||||
onClick={addService}
|
||||
onClick={beginAdd}
|
||||
className="btn violet small"
|
||||
>
|
||||
{busy === addSlug ? 'ADDING' : 'ADD'}
|
||||
@@ -409,6 +421,11 @@ const DeckyCard: React.FC<DeckyCardProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AddServiceConfigModal
|
||||
pending={pendingAdd}
|
||||
onCancel={() => setPendingAdd(null)}
|
||||
onConfirm={confirmAdd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user