Grow a node with its ports, stop it flickering, draw what it reaches out to
- A node's height follows the ports on its busiest side. It is a function of the document, so `layoutGraph` reserves exactly what is drawn and nothing measured is fed back into the layout. - The three status controls now sit in slots that are there whether the control is or not. A node running many times a second mounted and unmounted the stop button on every execution, resizing the card each time. - A port bound to another flow's message is drawn as a label, naming the node at the far end and its type. Only the opposite direction was answered before. The scan behind both is now cached on the store's commit counter rather than reading every flow per request. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
This commit is contained in:
@@ -30,7 +30,7 @@ from fluksio.core.db import engine
|
||||
from fluksio.flow.controller import FlowController
|
||||
from fluksio.flow.dashboards import DashboardStore
|
||||
from fluksio.flow.events import event_bus
|
||||
from fluksio.flow.messages import qualify
|
||||
from fluksio.flow.messages import flow_of, qualify
|
||||
from fluksio.flow.panels import messages_for
|
||||
from fluksio.flow.pipeline import ValidationIssue
|
||||
from fluksio.flow.runs import RunRejected
|
||||
@@ -130,36 +130,65 @@ class TriggerRequest(BaseModel):
|
||||
values: dict[str, Any] = {}
|
||||
|
||||
|
||||
def _endpoints(controller: FlowController, flow: str) -> list[Endpoint]:
|
||||
"""Everything wired into ``flow`` from outside it."""
|
||||
found: list[Endpoint] = []
|
||||
def _endpoints(controller: FlowController, definition: FlowDef) -> list[Endpoint]:
|
||||
"""Everything wired into this flow from outside it."""
|
||||
flow = definition.name
|
||||
found: dict[str, Endpoint] = {}
|
||||
|
||||
dashboards: DashboardStore | None = getattr(controller, "dashboards", None)
|
||||
if dashboards is not None:
|
||||
for binding in dashboards.bindings_for(flow):
|
||||
found.append(
|
||||
Endpoint(
|
||||
kind="dashboard",
|
||||
id=f"dashboard:{binding['dashboard']}:{binding['widget']}",
|
||||
label=binding["title"],
|
||||
detail=binding["type"],
|
||||
provides=[binding["provides"]] if binding["provides"] else [],
|
||||
requires=binding["requires"],
|
||||
)
|
||||
found[f"dashboard:{binding['dashboard']}:{binding['widget']}"] = Endpoint(
|
||||
kind="dashboard",
|
||||
id=f"dashboard:{binding['dashboard']}:{binding['widget']}",
|
||||
label=binding["title"],
|
||||
detail=binding["type"],
|
||||
provides=[binding["provides"]] if binding["provides"] else [],
|
||||
requires=binding["requires"],
|
||||
)
|
||||
|
||||
for other, node_id, provides, requires in controller.cross_flow_nodes(flow):
|
||||
found.append(
|
||||
Endpoint(
|
||||
kind="flow",
|
||||
id=f"flow:{other}.{node_id}",
|
||||
label=f"{other}.{node_id}",
|
||||
detail="flow",
|
||||
provides=provides,
|
||||
requires=requires,
|
||||
)
|
||||
def _flow_endpoint(key: str, detail: str) -> Endpoint:
|
||||
"""One label per node on the far side, however many names reach it."""
|
||||
return found.setdefault(
|
||||
f"flow:{key}",
|
||||
Endpoint(kind="flow", id=f"flow:{key}", label=key, detail=detail),
|
||||
)
|
||||
return found
|
||||
|
||||
# An outsider reaching into this flow.
|
||||
for other, node_id, provides, requires in controller.cross_flow_nodes(flow):
|
||||
endpoint = _flow_endpoint(f"{other}.{node_id}", "flow")
|
||||
endpoint.provides += provides
|
||||
endpoint.requires += requires
|
||||
|
||||
# And this flow reaching out: its own ports bound to a message of another
|
||||
# flow. Read from the working document rather than from the store, so a
|
||||
# name just typed is drawn before it has been published — the same reason
|
||||
# the canvas draws its own boundary from the document.
|
||||
for node in definition.nodes:
|
||||
for spec, ours_publishes in [
|
||||
*((spec, False) for spec in node.requires),
|
||||
*((spec, True) for spec in node.provides),
|
||||
]:
|
||||
message = qualify(flow, spec.name or "")
|
||||
if not message or flow_of(message) == flow:
|
||||
continue
|
||||
# The node at the other end, so the label reads like a dashboard's:
|
||||
# what it is on the first line, what sort of thing it is on the
|
||||
# second. A message no published flow declares yet has no other end
|
||||
# to name, so it is drawn as the message it is.
|
||||
far = controller.message_node(
|
||||
message, published=not ours_publishes, exclude=flow
|
||||
)
|
||||
endpoint = _flow_endpoint(
|
||||
f"{far.flow}.{far.node}" if far else message,
|
||||
far.type if far else "flow",
|
||||
)
|
||||
# An endpoint publishing into this flow is what this flow reads.
|
||||
side = endpoint.requires if ours_publishes else endpoint.provides
|
||||
if message not in side:
|
||||
side.append(message)
|
||||
|
||||
return list(found.values())
|
||||
|
||||
|
||||
def _detail(controller: FlowController, definition: FlowDef) -> FlowDetail:
|
||||
@@ -168,7 +197,7 @@ def _detail(controller: FlowController, definition: FlowDef) -> FlowDetail:
|
||||
"enabled": controller.is_enabled(name),
|
||||
"paused": controller.is_paused(name),
|
||||
}
|
||||
endpoints = _endpoints(controller, name)
|
||||
endpoints = _endpoints(controller, definition)
|
||||
if controller.store.has_draft(name):
|
||||
# Report the draft the editor is showing, not the version running
|
||||
# underneath it — otherwise a node the author just broke looks fine.
|
||||
|
||||
Reference in New Issue
Block a user