ui: Apply everywhere — power toggles, plan preview, no start/stop/install
Rework the dashboard around convergence:
- ServiceControls / ServiceCard / JobCard: replace start/restart/stop
with a Power toggle (set enabled → apply, i.e. activate/deactivate)
plus Restart (the one imperative bounce).
- ToolDetail: install/uninstall becomes an Enable/Disable toggle;
`installed` stays the live state, `enabled` the desired one.
- ConfigPanel Apply and CreateDeploymentForm now POST /apply (one
converge that renders + restarts only what changed) instead of
/deploy + a separate start/restart.
- New ConvergePanel on Overview: a terraform-style Preview (POST /apply
plan=true) showing would-activate/restart/deactivate, then Apply.
- useApply + useSetEnabled hooks; `enabled` added to Service/Job/
Deployment types; both handle the self-restart connection drop.
Backend: PUT /config/deployments/{name}/enabled sets desired on/off
(edit config; caller runs apply) — the declarative toggle the UI uses.
Dashboard builds clean (tsc + vite); 59 API tests pass; live /apply and
/services?enabled verified after an api restart.
This commit is contained in:
@@ -154,6 +154,65 @@ export function useServiceAction() {
|
||||
})
|
||||
}
|
||||
|
||||
export interface ApplyResult {
|
||||
status: string
|
||||
planned: boolean
|
||||
changed: boolean
|
||||
activated: string[]
|
||||
restarted: string[]
|
||||
deactivated: string[]
|
||||
unchanged: string[]
|
||||
messages: string[]
|
||||
}
|
||||
|
||||
// Converge the running system to config. Pass a name to converge one deployment,
|
||||
// or plan:true for a dry-run diff. Handles the API restarting itself mid-apply.
|
||||
export function useApply() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async ({ name, plan }: { name?: string; plan?: boolean } = {}) => {
|
||||
try {
|
||||
return await apiClient.post<ApplyResult>("/apply", { name, plan })
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
// Self-restart killed the connection — treat as accepted, wait + refresh.
|
||||
await waitForApi()
|
||||
return {
|
||||
status: "ok", planned: false, changed: true,
|
||||
activated: [], restarted: name ? [name] : [], deactivated: [],
|
||||
unchanged: [], messages: [],
|
||||
} as ApplyResult
|
||||
}
|
||||
throw err
|
||||
}
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (!data.planned) qc.invalidateQueries()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Set a deployment's desired on/off state, then converge it. One click = "make it
|
||||
// so": edit config (declarative), then apply that single deployment.
|
||||
export function useSetEnabled() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async ({ name, enabled }: { name: string; enabled: boolean }) => {
|
||||
await apiClient.put(`/config/deployments/${name}/enabled`, { enabled })
|
||||
try {
|
||||
return await apiClient.post<ApplyResult>("/apply", { name })
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
await waitForApi()
|
||||
return null
|
||||
}
|
||||
throw err
|
||||
}
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries(),
|
||||
})
|
||||
}
|
||||
|
||||
export function useProgramAction() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
|
||||
Reference in New Issue
Block a user