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:
co-authored by
Claude Fable 5
parent
c254d487ba
commit
fd666743d2
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
|
||||
from app.flow.messages import MessageSpec
|
||||
from app.flow.schemas import FlowDef, NodeDef
|
||||
from app.flow.store import FlowNotFound, FlowStore
|
||||
from app.flow.store import FlowExists, FlowNotFound, FlowStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -73,3 +73,47 @@ def test_deleting_removes_flow_and_its_nodes(store: FlowStore):
|
||||
|
||||
assert store.list_flows() == []
|
||||
assert not (store.root / "heating").exists()
|
||||
|
||||
|
||||
def test_renaming_a_flow_carries_its_nodes(store: FlowStore):
|
||||
store.write_flow(a_flow())
|
||||
store.write_node_source(
|
||||
"heating", "sensor", "def process(params):\n return {}\n"
|
||||
)
|
||||
|
||||
renamed = store.rename_flow("heating", "warmth")
|
||||
|
||||
assert renamed.name == "warmth"
|
||||
assert store.list_flows() == ["warmth"]
|
||||
assert "def process" in store.read_node_source("warmth", "sensor")
|
||||
|
||||
|
||||
def test_renaming_a_flow_repoints_the_flows_reading_it(store: FlowStore):
|
||||
store.write_flow(a_flow())
|
||||
store.write_flow(
|
||||
FlowDef(
|
||||
name="display",
|
||||
nodes=[
|
||||
NodeDef(
|
||||
id="gauge",
|
||||
# Reads across the flow boundary, so the name must follow.
|
||||
requires=[MessageSpec(name="heating.temp")],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
store.rename_flow("heating", "warmth")
|
||||
|
||||
display = store.read_flow("display")
|
||||
assert display.nodes[0].requires[0].name == "warmth.temp"
|
||||
|
||||
|
||||
def test_renaming_onto_an_existing_name_is_refused(store: FlowStore):
|
||||
store.write_flow(a_flow())
|
||||
store.write_flow(FlowDef(name="warmth"))
|
||||
|
||||
with pytest.raises(FlowExists):
|
||||
store.rename_flow("heating", "warmth")
|
||||
|
||||
assert store.list_flows() == ["heating", "warmth"]
|
||||
|
||||
Reference in New Issue
Block a user