Fix the MQTT node, suggest message names, and drop Items
- MQTT nodes failed to build: the topic map was renamed to talk in ports, but __slots__ still declared the old name, so every MQTT node raised AttributeError. Building one of each node type is now a test, since __slots__ makes this failure invisible until someone places the node. - Port names offer the messages already in play: everything published is worth reading, and an input nobody provides yet is worth publishing. A message only connects when both ends spell it the same way, so choosing beats typing. - Adding a port focuses its name field. - Dragging onto an input that already reads something offers the extra port as well as the replacement — an MQTT or InfluxDB node usually wants both. - The template's Item model, its routes, screens and table are gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
This commit is contained in:
co-authored by
Claude Fable 5
parent
8c82549cf6
commit
01af7787c1
@@ -20,7 +20,12 @@ import { useNavigate } from "@tanstack/react-router"
|
||||
import { Workflow } from "lucide-react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { type FlowDef_Input, FlowsService, type NodeDef_Input } from "@/client"
|
||||
import {
|
||||
type FlowDef_Input,
|
||||
FlowsService,
|
||||
type MessageSpec,
|
||||
type NodeDef_Input,
|
||||
} from "@/client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -54,9 +59,11 @@ const edgeTypes = { live: LiveEdge }
|
||||
|
||||
type Rebind = {
|
||||
nodeId: string
|
||||
nodeLabel: string
|
||||
port: string
|
||||
from: string
|
||||
to: string
|
||||
dtype: MessageSpec["dtype"]
|
||||
}
|
||||
|
||||
/** Step a new node off any node already sitting at that spot. */
|
||||
@@ -167,8 +174,31 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
[canvasNodes, definitions, flowName, issuesByNode, selectedId, typeLabels],
|
||||
)
|
||||
|
||||
// Edges follow from the name bindings, so they are derived, never stored.
|
||||
// A cheap fingerprint of the wiring: it changes when a name does, but not
|
||||
// when a node merely moves.
|
||||
const key = bindingsKey(definitions)
|
||||
|
||||
// Offer the names already in play: everything published is worth reading,
|
||||
// and an input nobody provides yet is worth publishing.
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: the bindings key is what changes names.
|
||||
const suggestions = useMemo(() => {
|
||||
const provided = new Set<string>()
|
||||
const consumed = new Set<string>()
|
||||
for (const node of definitions) {
|
||||
for (const spec of node.provides ?? []) {
|
||||
if (spec.name) provided.add(spec.name)
|
||||
}
|
||||
for (const spec of node.requires ?? []) {
|
||||
if (spec.name) consumed.add(spec.name)
|
||||
}
|
||||
}
|
||||
return {
|
||||
consumes: [...provided].sort(),
|
||||
provides: [...consumed].filter((name) => !provided.has(name)).sort(),
|
||||
}
|
||||
}, [key])
|
||||
|
||||
// Edges follow from the name bindings, so they are derived, never stored.
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: the key is the dependency; the array identity changes on every drag frame.
|
||||
const edges = useMemo(
|
||||
() => deriveEdges(definitions, flowName),
|
||||
@@ -297,12 +327,16 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
)
|
||||
if (!outSpec?.name || !inSpec) return
|
||||
|
||||
// Already reading something else: the user may want either message, so
|
||||
// offer the extra port rather than assuming a replacement.
|
||||
if (inSpec.name && inSpec.name !== outSpec.name) {
|
||||
setRebind({
|
||||
nodeId: consumer.id,
|
||||
nodeLabel: consumer.title || consumer.id,
|
||||
port: portOf(inSpec),
|
||||
from: inSpec.name,
|
||||
to: outSpec.name,
|
||||
dtype: outSpec.dtype,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -312,6 +346,26 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
[definitions, applyBinding],
|
||||
)
|
||||
|
||||
/** Give the consumer a second input, bound to the producer's message. */
|
||||
const addInputPort = useCallback(
|
||||
(nodeId: string, message: string, dtype: MessageSpec["dtype"]) => {
|
||||
commit(
|
||||
definitions.map((node) =>
|
||||
node.id === nodeId
|
||||
? {
|
||||
...node,
|
||||
requires: [
|
||||
...(node.requires ?? []),
|
||||
{ name: message, port: "", dtype },
|
||||
],
|
||||
}
|
||||
: node,
|
||||
),
|
||||
)
|
||||
},
|
||||
[commit, definitions],
|
||||
)
|
||||
|
||||
const unbind = useCallback(
|
||||
(message: string) => {
|
||||
const qualified = qualify(flowName, message)
|
||||
@@ -420,6 +474,7 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
node={selected}
|
||||
flow={flowName}
|
||||
nodeTypes={nodeTypeInfo ?? []}
|
||||
suggestions={suggestions}
|
||||
onChange={updateNode}
|
||||
onSaveSource={(code) => {
|
||||
if (selected) sourceMutation.mutate({ nodeId: selected.id, code })
|
||||
@@ -453,24 +508,41 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Change what this input reads?</DialogTitle>
|
||||
<DialogTitle>How should {rebind?.nodeLabel} read this?</DialogTitle>
|
||||
<DialogDescription>
|
||||
"{rebind?.port}" currently reads {rebind?.from}. Point it at{" "}
|
||||
{rebind?.to} instead?
|
||||
Its "{rebind?.port}" input already reads{" "}
|
||||
<span className="font-mono">{rebind?.from}</span>. It can take{" "}
|
||||
<span className="font-mono">{rebind?.to}</span> as well, or
|
||||
instead.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setRebind(null)}>
|
||||
Keep {rebind?.from}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (rebind) applyBinding(rebind.nodeId, rebind.port, rebind.to)
|
||||
setRebind(null)
|
||||
}}
|
||||
>
|
||||
Read {rebind?.to}
|
||||
<DialogFooter className="sm:justify-between">
|
||||
<Button variant="ghost" onClick={() => setRebind(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (rebind) {
|
||||
applyBinding(rebind.nodeId, rebind.port, rebind.to)
|
||||
}
|
||||
setRebind(null)
|
||||
}}
|
||||
>
|
||||
Replace
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (rebind) {
|
||||
addInputPort(rebind.nodeId, rebind.to, rebind.dtype)
|
||||
}
|
||||
setRebind(null)
|
||||
}}
|
||||
>
|
||||
Add as another input
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user