Files
app/frontend/src/routes/_layout/modules.tsx
T
stroblmeandClaude Opus 5.5 7370105cd8 Make changing engine-run code superuser-only
Writing node source, installing modules and any flow save that adds,
retypes or repoints a code node (Function, Command), edits a Command
node's settings or changes a node's resources.env now answer 403 for
everyone but a superuser. MCP tools and the assistant reach the same
routes, so they inherit the gate. Node types report runs_code.

The editor shows code read-only with a note, disables a Command node's
settings, leaves code nodes and reusable nodes out of the palette and
paste, and the Modules page offers no install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
2026-10-06 11:36:30 +02:00

181 lines
6.7 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute } from "@tanstack/react-router"
import { Package } from "lucide-react"
import { useState } from "react"
import { type ApiError, ModulesService } from "@/client"
import { InfoTip } from "@/components/Flow/SidePanel"
import { Button } from "@/components/ui/button"
import useAuth from "@/hooks/useAuth"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
export const Route = createFileRoute("/_layout/modules")({
component: Modules,
head: () => ({
meta: [
{
title: "Modules - Fluksio",
},
],
}),
})
const modulesKey = ["modules"]
const SECTION =
"text-xs font-medium uppercase tracking-[0.5px] text-muted-foreground"
function Modules() {
const { data } = useQuery({
queryKey: modulesKey,
queryFn: () => ModulesService.readModules(),
})
const queryClient = useQueryClient()
const { showSuccessToast, showErrorToast } = useCustomToast()
// A package runs its own code on install, so installing is a superuser's.
const { user } = useAuth()
const mayInstall = Boolean(user?.is_superuser)
// Null until the field is touched, so what is stored shows through until
// someone actually starts editing it.
const [draft, setDraft] = useState<string | null>(null)
const [output, setOutput] = useState("")
const apply = useMutation({
mutationFn: (requirements: string) =>
ModulesService.applyModules({ requestBody: { requirements } }),
onSuccess: (result) => {
showSuccessToast("Modules installed")
setOutput(result.output ?? "")
setDraft(null)
queryClient.invalidateQueries({ queryKey: modulesKey })
},
onError: (error: ApiError) => {
// The 400 detail is uv's own output — several lines of resolver
// reasoning, which belongs in the pane rather than in a toast.
const detail = (error.body as { detail?: string } | undefined)?.detail
if (detail) {
setOutput(detail)
showErrorToast("Those requirements could not be installed")
} else {
handleError.call(showErrorToast, error)
}
},
})
const stored = data?.requirements ?? ""
const requirements = draft ?? stored
const packages = data?.packages ?? []
const dirty = requirements !== stored
// Fluksio was installed into an environment somebody else made, and node
// code runs on it. Installing from here would mean `uv pip sync`, which
// holds a venv to exactly one list — and would uninstall their work.
const adopted = data?.adopted ?? false
return (
<div className="grid gap-6">
<div className="grid gap-1">
<div className="flex items-center gap-1">
<h1 className="text-2xl">Modules</h1>
<InfoTip label="Modules">
{adopted
? "Fluksio was installed into an environment you already had, so that is the one your nodes run on. Change it with pip or uv rather than from here."
: "Packages install into an environment of their own, separate from the engine's, so a version pinned here is the one your code gets. The list is kept with your flows, so a rebuilt deployment installs the same set again."}
</InfoTip>
</div>
<p className="text-sm text-muted-foreground">
The Python packages your function nodes can import.
</p>
</div>
{adopted ? null : !mayInstall ? (
<p
className="text-sm text-muted-foreground"
data-testid="modules-locked"
>
Only a superuser can install packages.
</p>
) : (
<section className="grid gap-3">
<h2 className={SECTION}>Requirements</h2>
<textarea
className="min-h-[160px] w-full rounded-md border border-border bg-card p-3 font-mono text-sm shadow-e1 outline-none focus-visible:border-primary"
spellCheck={false}
aria-label="Requirements"
data-testid="requirements"
placeholder={"requests==2.32.3\npandas>=2.2"}
value={requirements}
onChange={(event) => setDraft(event.target.value)}
/>
<div className="flex items-center gap-3">
<Button
disabled={apply.isPending}
data-testid="apply-modules"
onClick={() => apply.mutate(requirements)}
>
<Package />
{apply.isPending ? "Installing" : "Apply"}
</Button>
<span className="text-sm text-muted-foreground">
{dirty
? "Not applied yet."
: data?.applied
? "Installed and in step."
: "What is installed does not match this list."}
</span>
</div>
{output ? (
<pre
className="max-h-64 overflow-auto rounded-md border border-border bg-card p-3 font-mono text-xs shadow-e1"
data-testid="modules-output"
>
{output}
</pre>
) : null}
</section>
)}
<section className="grid gap-3">
<h2 className={SECTION}>Installed</h2>
<p className="text-sm text-muted-foreground" data-testid="modules-venv">
{data
? `Python ${data.python_version || "unknown"} at ${data.venv_path}`
: "Reading the environment…"}
</p>
{adopted ? (
<p
className="text-sm text-muted-foreground"
data-testid="modules-adopted"
>
Adopted — this is the environment Fluksio itself is installed in.
Add a package the way you added the rest:{" "}
<span className="font-mono">pip install …</span>, then sync or
restart so the workers pick it up. For a venv the engine owns
instead, set <span className="font-mono">NODE_VENV=managed</span>.
</p>
) : null}
{packages.length === 0 ? (
<p className="text-sm text-muted-foreground">
Nothing installed yet. Node code has the standard library.
</p>
) : (
<div className="grid gap-2">
{packages.map((entry) => (
<div
key={entry.name}
className="flex items-center justify-between gap-3 rounded-lg border border-border bg-card p-3 shadow-e1"
data-testid="module-row"
>
<span className="font-mono text-sm">{entry.name}</span>
<span className="font-mono text-sm text-muted-foreground">
{entry.version}
</span>
</div>
))}
</div>
)}
</section>
</div>
)
}