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:
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/services/api/hooks"
|
||||
import { NodeBar } from "@/components/NodeBar"
|
||||
import { PageHeader } from "@/components/PageHeader"
|
||||
import { ConvergePanel } from "@/components/ConvergePanel"
|
||||
|
||||
export function Overview() {
|
||||
const { data: services } = useServices()
|
||||
@@ -96,6 +97,8 @@ export function Overview() {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ConvergePanel />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
import { Clock } from "lucide-react"
|
||||
import { useJob, useStatus } from "@/services/api/hooks"
|
||||
import { useJob } from "@/services/api/hooks"
|
||||
import { LogViewer } from "@/components/LogViewer"
|
||||
import { DetailHeader } from "@/components/detail/DetailHeader"
|
||||
import { ServiceControls } from "@/components/detail/ServiceControls"
|
||||
@@ -10,8 +10,6 @@ import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
||||
export function ScheduledDetailPage() {
|
||||
const { name } = useParams<{ name: string }>()
|
||||
const { data: deployment, isLoading, error, refetch } = useJob(name ?? "")
|
||||
const { data: statusResp } = useStatus()
|
||||
const health = statusResp?.statuses.find((s) => s.id === name)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -38,7 +36,7 @@ export function ScheduledDetailPage() {
|
||||
stack={deployment.stack}
|
||||
source={deployment.source}
|
||||
>
|
||||
<ServiceControls name={deployment.id} health={health} />
|
||||
<ServiceControls name={deployment.id} enabled={deployment.enabled} />
|
||||
</DetailHeader>
|
||||
|
||||
{deployment.schedule && (
|
||||
|
||||
@@ -51,7 +51,7 @@ export function ServiceDetailPage() {
|
||||
{!isStatic && (
|
||||
<div className="flex items-center gap-2">
|
||||
{health && <HealthBadge status={health.status} latency={health.latency_ms} />}
|
||||
<ServiceControls name={deployment.id} health={health} />
|
||||
<ServiceControls name={deployment.id} enabled={deployment.enabled} />
|
||||
</div>
|
||||
)}
|
||||
</DetailHeader>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useParams, Link } from "react-router-dom"
|
||||
import { Loader2, Package } from "lucide-react"
|
||||
import { useDeployment, useProgramAction } from "@/services/api/hooks"
|
||||
import { useDeployment, useSetEnabled } from "@/services/api/hooks"
|
||||
import { DetailHeader } from "@/components/detail/DetailHeader"
|
||||
import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
||||
|
||||
@@ -38,9 +38,14 @@ export function ToolDetailPage() {
|
||||
<p className="text-sm text-[var(--muted)] -mt-4 mb-6">{deployment.description}</p>
|
||||
)}
|
||||
|
||||
{/* A tool's PATH deployment: install/uninstall is its start/stop. Its
|
||||
live state is `installed` (on PATH), which the endpoint sets directly. */}
|
||||
<PathLifecycle name={deployment.id} installed={deployment.installed} onDone={refetch} />
|
||||
{/* A tool's PATH deployment: enabling converges it onto PATH, disabling
|
||||
removes it. `installed` is the live state; `enabled` the desired one. */}
|
||||
<PathLifecycle
|
||||
name={deployment.id}
|
||||
enabled={deployment.enabled}
|
||||
installed={deployment.installed}
|
||||
onDone={refetch}
|
||||
/>
|
||||
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
|
||||
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-3">
|
||||
@@ -63,17 +68,19 @@ export function ToolDetailPage() {
|
||||
)
|
||||
}
|
||||
|
||||
/** Install/uninstall a tool on PATH — the path deployment's lifecycle. */
|
||||
/** Enable/disable a tool — convergence installs it onto PATH or removes it. */
|
||||
function PathLifecycle({
|
||||
name,
|
||||
enabled,
|
||||
installed: installedState,
|
||||
onDone,
|
||||
}: {
|
||||
name: string
|
||||
enabled: boolean
|
||||
installed: boolean | null
|
||||
onDone: () => void
|
||||
}) {
|
||||
const { mutate, isPending } = useProgramAction()
|
||||
const { mutate, isPending } = useSetEnabled()
|
||||
const installed = installedState === true
|
||||
const dot =
|
||||
installedState === true
|
||||
@@ -86,21 +93,20 @@ function PathLifecycle({
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className={`h-2 w-2 rounded-full shrink-0 ${dot}`} />
|
||||
<span>{installed ? "Installed on PATH" : "Not installed"}</span>
|
||||
{!enabled && <span className="text-xs text-amber-400">disabled</span>}
|
||||
<span className="text-xs text-[var(--muted)]">manager: path</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
mutate({ name, action: installed ? "uninstall" : "install" }, { onSuccess: onDone })
|
||||
}
|
||||
onClick={() => mutate({ name, enabled: !enabled }, { onSuccess: onDone })}
|
||||
disabled={isPending}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1 text-sm rounded border transition-colors disabled:opacity-40 ${
|
||||
installed
|
||||
enabled
|
||||
? "border-red-800 text-red-400 hover:bg-red-800/30"
|
||||
: "border-green-800 text-green-400 hover:bg-green-800/30"
|
||||
}`}
|
||||
>
|
||||
{isPending && <Loader2 size={14} className="animate-spin" />}
|
||||
{installed ? "Uninstall" : "Install"}
|
||||
{enabled ? "Disable" : "Enable"}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user