app: tool page converge button for enabled-but-not-installed drift
The path-tool lifecycle offered only Enable/Disable (keyed on enabled), so an enabled-but-not-on-PATH tool showed just "Disable" with no way to converge it. Add an Install/Uninstall (apply) action whenever desired (enabled) and actual (installed) disagree.
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
import { useParams, Link } from "react-router-dom"
|
import { useParams, Link } from "react-router-dom"
|
||||||
import { Loader2, Package } from "lucide-react"
|
import { Loader2 } from "lucide-react"
|
||||||
import { useDeployment, useSetEnabled } from "@/services/api/hooks"
|
import { useApply, useDeployment, useSetEnabled } from "@/services/api/hooks"
|
||||||
|
import { kindIcon } from "@/lib/labels"
|
||||||
import { DetailHeader } from "@/components/detail/DetailHeader"
|
import { DetailHeader } from "@/components/detail/DetailHeader"
|
||||||
import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
||||||
import { RelatedDeployments } from "@/components/detail/RelatedDeployments"
|
import { RelatedDeployments } from "@/components/detail/RelatedDeployments"
|
||||||
|
|
||||||
|
const ProgramIcon = kindIcon("program")
|
||||||
|
|
||||||
export function ToolDetailPage() {
|
export function ToolDetailPage() {
|
||||||
const { name } = useParams<{ name: string }>()
|
const { name } = useParams<{ name: string }>()
|
||||||
const { data: deployment, isLoading, error, refetch } = useDeployment(name ?? "")
|
const { data: deployment, isLoading, error, refetch } = useDeployment(name ?? "")
|
||||||
@@ -60,7 +63,7 @@ export function ToolDetailPage() {
|
|||||||
to={`/programs/${program}`}
|
to={`/programs/${program}`}
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-[var(--primary)] hover:underline"
|
className="inline-flex items-center gap-1.5 text-sm text-[var(--primary)] hover:underline"
|
||||||
>
|
>
|
||||||
<Package size={14} /> {program}
|
<ProgramIcon size={14} /> {program}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,8 +86,15 @@ function PathLifecycle({
|
|||||||
installed: boolean | null
|
installed: boolean | null
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
}) {
|
}) {
|
||||||
const { mutate, isPending } = useSetEnabled()
|
const setEnabled = useSetEnabled()
|
||||||
|
const apply = useApply()
|
||||||
const installed = installedState === true
|
const installed = installedState === true
|
||||||
|
// Drift: the desired state (enabled) and the actual state (installed) disagree —
|
||||||
|
// config says it should be on PATH but isn't, or vice versa. The enable/disable
|
||||||
|
// toggle changes *intent*; converging is a separate act, so offer an Apply button
|
||||||
|
// that reconciles reality to the current intent without flipping it.
|
||||||
|
const drift = installedState !== null && installedState !== enabled
|
||||||
|
const busy = setEnabled.isPending || apply.isPending
|
||||||
const dot =
|
const dot =
|
||||||
installedState === true
|
installedState === true
|
||||||
? "bg-green-500"
|
? "bg-green-500"
|
||||||
@@ -97,20 +107,34 @@ function PathLifecycle({
|
|||||||
<span className={`h-2 w-2 rounded-full shrink-0 ${dot}`} />
|
<span className={`h-2 w-2 rounded-full shrink-0 ${dot}`} />
|
||||||
<span>{installed ? "Installed on PATH" : "Not installed"}</span>
|
<span>{installed ? "Installed on PATH" : "Not installed"}</span>
|
||||||
{!enabled && <span className="text-xs text-amber-400">disabled</span>}
|
{!enabled && <span className="text-xs text-amber-400">disabled</span>}
|
||||||
|
{drift && <span className="text-xs text-amber-400">needs apply</span>}
|
||||||
<span className="text-xs text-[var(--muted)]">manager: path</span>
|
<span className="text-xs text-[var(--muted)]">manager: path</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => mutate({ name, enabled: !enabled }, { onSuccess: onDone })}
|
{drift && (
|
||||||
disabled={isPending}
|
<button
|
||||||
className={`flex items-center gap-1.5 px-2.5 py-1 text-sm rounded border transition-colors disabled:opacity-40 ${
|
onClick={() => apply.mutate({ name }, { onSuccess: onDone })}
|
||||||
enabled
|
disabled={busy}
|
||||||
? "border-red-800 text-red-400 hover:bg-red-800/30"
|
title={enabled ? "Converge onto PATH" : "Remove from PATH"}
|
||||||
: "border-green-800 text-green-400 hover:bg-green-800/30"
|
className="flex items-center gap-1.5 px-2.5 py-1 text-sm rounded border border-green-800 text-green-400 hover:bg-green-800/30 transition-colors disabled:opacity-40"
|
||||||
}`}
|
>
|
||||||
>
|
{apply.isPending && <Loader2 size={14} className="animate-spin" />}
|
||||||
{isPending && <Loader2 size={14} className="animate-spin" />}
|
{enabled ? "Install" : "Uninstall"}
|
||||||
{enabled ? "Disable" : "Enable"}
|
</button>
|
||||||
</button>
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setEnabled.mutate({ name, enabled: !enabled }, { onSuccess: onDone })}
|
||||||
|
disabled={busy}
|
||||||
|
className={`flex items-center gap-1.5 px-2.5 py-1 text-sm rounded border transition-colors disabled:opacity-40 ${
|
||||||
|
enabled
|
||||||
|
? "border-red-800 text-red-400 hover:bg-red-800/30"
|
||||||
|
: "border-green-800 text-green-400 hover:bg-green-800/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{setEnabled.isPending && <Loader2 size={14} className="animate-spin" />}
|
||||||
|
{enabled ? "Disable" : "Enable"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user