Add flow settings, pulse emitting nodes, and simplify node state

- One dot per node now carries the whole story: primary while running, sage
  after a good run, red when anything is wrong, with the explanation on hover.
  The corner badge is gone, along with the second way of saying the same thing.
- A node that publishes something flashes a ring, so a running flow is legible
  without reading the edge values. Nodes that consume but publish nothing stay
  quiet, which is why the event carries an output count.
- Flow settings open in the same panel its nodes use, from a pencil in the
  dock: the title, the name, and deleting the flow. NodePanel and FlowPanel
  share the panel chrome rather than each drawing their own.
- Renaming is a server operation, because a flow's name is the namespace of its
  messages: the directory moves and every other flow reading `old.message` is
  repointed, instead of being left pointing at a flow that no longer exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
This commit is contained in:
Melvin Strobl
2026-08-15 19:46:23 +02:00
co-authored by Claude Fable 5
parent c254d487ba
commit fd666743d2
18 changed files with 756 additions and 197 deletions
+62
View File
@@ -144,3 +144,65 @@ def test_node_types_are_listed(
by_type = {entry["type"]: entry for entry in types}
assert by_type["python"]["has_source"] is True
assert "properties" in by_type["mqtt"]["params_schema"]
def test_rename_flow(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
client.put(f"{PREFIX}/demo", headers=superuser_token_headers, json=a_flow())
response = client.post(
f"{PREFIX}/demo/rename",
headers=superuser_token_headers,
json={"new_name": "demo_renamed"},
)
assert response.status_code == 200
assert response.json()["definition"]["name"] == "demo_renamed"
assert (
client.get(f"{PREFIX}/demo", headers=superuser_token_headers).status_code == 404
)
assert (
client.get(
f"{PREFIX}/demo_renamed", headers=superuser_token_headers
).status_code
== 200
)
# Put it back so the tests that follow find the flow they expect.
client.post(
f"{PREFIX}/demo_renamed/rename",
headers=superuser_token_headers,
json={"new_name": "demo"},
)
def test_rename_onto_a_taken_name_is_refused(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
client.put(f"{PREFIX}/demo", headers=superuser_token_headers, json=a_flow())
client.put(
f"{PREFIX}/occupied", headers=superuser_token_headers, json=a_flow("occupied")
)
response = client.post(
f"{PREFIX}/demo/rename",
headers=superuser_token_headers,
json={"new_name": "occupied"},
)
assert response.status_code == 409
client.delete(f"{PREFIX}/occupied", headers=superuser_token_headers)
def test_rename_rejects_an_invalid_name(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
client.put(f"{PREFIX}/demo", headers=superuser_token_headers, json=a_flow())
response = client.post(
f"{PREFIX}/demo/rename",
headers=superuser_token_headers,
json={"new_name": "Not A Flow Name"},
)
assert response.status_code == 400