Powered by AppSignal & Oban Pro

Graphviz Observer - fork

livebooks/sigmajs_refactoring.livemd

Graphviz Observer - fork

Mix.install([
  {:kino, "~> 0.15.3"},
  # {:iroh_ex, "~> 0.0.4"},
   {:iroh_ex,
   path: "/Users/adrianibanez/Documents/projects/2024_sensor-platform/checkouts/iroh_ex"},
  {:rustler, "~> 0.36.1", override: true},
  {:rustler_precompiled, "~> 0.7"}
])

Livebook Sigma

defmodule SigmaGraph do
  use Kino.JS
  use Kino.JS.Live

  def new do
    Kino.JS.Live.new(__MODULE__, %{})
  end

  # Add a node
  def add_node(kino, node, replace \\ false) do
    Kino.JS.Live.cast(kino, {:add_node, node, replace})
  end

  def merge_node(kino, node) do
    Kino.JS.Live.cast(kino, {:merge_node, node})
  end

  # Remove a node
  def remove_node(kino, node_id) do
    Kino.JS.Live.cast(kino, {:remove_node, node_id})
  end

  # Add an edge
  def add_edge(kino, edge) do
    Kino.JS.Live.cast(kino, {:add_edge, edge})
  end

  # Add an edge without rendering
  def add_edge_batched(kino, edge) do
    Kino.JS.Live.cast(kino, {:add_edge_batched, edge})
  end

  # Remove an edge
  def remove_edge(kino, edge) do
    Kino.JS.Live.cast(kino, {:remove_edge, edge})
  end

  def render(kino) do
    Kino.JS.Live.cast(kino, :render)
  end

  def clear(kino) do
    Kino.JS.Live.cast(kino, :clear)
  end

  def test(kino, payload) do
    Kino.JS.Live.cast(kino, {:test, payload})
  end

  @impl true
  def init(_, ctx) do
    {:ok, assign(ctx, nodes: %{}, edges: [])}
  end

  @impl true
  def handle_connect(ctx) do
    {:ok, ctx.assigns, ctx}
  end

  @impl true
  def handle_cast({:test, payload}, ctx) do
    # IO.inspect(payload, label: "Test")
    broadcast_event(ctx, "test", payload)
    {:noreply, ctx}
  end

  @impl true
  def handle_cast(:clear, ctx) do
    # IO.inspect(payload, label: "Test")
    broadcast_event(ctx, "clear", [])
    {:noreply, ctx}
  end

  @impl true
  def handle_cast({:add_node, node, replace}, ctx) do
    default_values = %{
      x: :rand.uniform(10),
      y: :rand.uniform(10),
      size: 10,
      color: "#1f77b4"
    }

    node = Map.merge(default_values, node)

    # IO.inspect(node, label: "Node debug")

    ctx = update_in(ctx.assigns.nodes, &Map.put(&1, node.id, node))
    # IO.inspect(node, label: "Add Node")
    # IO.inspect(ctx, label: "Add Node ctx")
    broadcast_event(
      ctx,
      "add_node",
      node |> Map.put(:replace, replace)
    )

    {:noreply, ctx}
  end

  @impl true
  def handle_cast({:merge_node, node}, ctx) do
    ctx =
      update_in(ctx.assigns.nodes, fn nodes ->
        Map.update(nodes, node.id, node, &Map.merge(&1, node))
      end)

    broadcast_event(
      ctx,
      "merge_node",
      node
    )

    {:noreply, ctx}
  end

  @impl true
  def handle_cast({:add_edge, edge}, ctx) do
    ctx = update_in(ctx.assigns.edges, &[edge | &1])
    broadcast_event(ctx, "add_edge", edge)
    {:noreply, ctx}
  end

  @impl true
  def handle_cast({:add_edge_batched, edge}, ctx) do
    ctx = update_in(ctx.assigns.edges, &[edge | &1])
    broadcast_event(ctx, "add_edge_batched", edge)
    {:noreply, ctx}
  end

  @impl true
  def handle_cast(:render, ctx) do
    broadcast_event(ctx, "render", [])
    {:noreply, ctx}
  end

  @impl true
  def handle_cast({:remove_node, node_id}, ctx) do
    ctx = update_in(ctx.assigns.nodes, &Map.delete(&1, node_id))

    # Correctly filter out edges that involve the removed node
    ctx =
      update_in(
        ctx.assigns.edges,
        &Enum.reject(&1, fn edge ->
          edge.source == node_id || edge.target == node_id
        end)
      )

    broadcast_event(ctx, "remove_node", node_id)
    {:noreply, ctx}
  end

  @impl true
  def handle_cast({:remove_edge, edge}, ctx) do
    ctx =
      update_in(
        ctx.assigns.edges,
        &Enum.reject(&1, fn e ->
          edge.source == e.source && edge.target == e.target
        end)
      )

    broadcast_event(ctx, "remove_edge", edge)
    {:noreply, ctx}
  end

  @impl true
  def handle_info(msg, ctx) do
    IO.puts("#{__MODULE__} Catchall #{inspect(msg)}")
    {:noreply, ctx}
  end

  asset "main.js" do
    """
    import Sigma from 'https://cdn.jsdelivr.net/npm/sigma@3.0.1/+esm';
    import Graph from 'https://cdn.jsdelivr.net/npm/graphology@0.26.0/+esm';
    import graphologyLayout from 'https://cdn.jsdelivr.net/npm/graphology-layout@0.6.1/+esm';
    import FA2Layout from 'https://cdn.jsdelivr.net/npm/graphology-layout-forceatlas2@0.10.1/+esm';
    import graphOlogyLayoutForce from 'https://cdn.jsdelivr.net/npm/graphology-layout-force@0.2.4/+esm';
    import sigmaEdgeCurve from 'https://cdn.jsdelivr.net/npm/@sigma/edge-curve@3.1.0/+esm';
    import debounce from 'https://cdn.jsdelivr.net/npm/debounce@2.2.0/+esm';

    const { circular } = graphologyLayout;

    // --- Configuration ---
    const DIMMED_COLOR = "rgba(200, 200, 200, 0.3)";
      

    export function init(ctx, payload) {
      const container = document.createElement("div");
      container.style.width = "100%";
      container.style.height = "500px";
      ctx.root.appendChild(container);



      // --- Reducers ---
      
      /**
       * Decides the final attributes for each node based on the hover state.
       * This function is passed to the Sigma constructor.
       */
      const nodeReducer = (nodeId, attributes) => {
        // Base attributes to potentially modify
        const newAttributes = { ...attributes };
      
        if (hoveredNodeId) {
          // A node is hovered
          if (nodeId === hoveredNodeId || hoveredNeighbors.has(nodeId)) {
            // This node IS the hovered one or a direct neighbor: HIGHLIGHT
            newAttributes.zIndex = 1;
            // ** Crucially: DO NOT set 'color' here **
            // By not setting it, Sigma will use the color from the
            // original graph data (your peer count color) or the default.
            // We only remove the potential 'color' override below if needed.
            if (newAttributes.color === DIMMED_COLOR) {
               // If it was previously dimmed, remove the override
               delete newAttributes.color;
            }
          } else {
            // This node is NOT hovered or a neighbor: DIM
            newAttributes.color = DIMMED_COLOR;
            newAttributes.zIndex = 0;
          }
        } else {
          // No node is hovered: RESET state for all nodes
          newAttributes.zIndex = 0;
          // If the node currently has the dimmed color, remove the override
          // to revert to its original/default color.
          if (newAttributes.color === DIMMED_COLOR) {
             delete newAttributes.color;
          }
        }
        return newAttributes;
      };
      
      /**
       * Decides the final attributes for each edge based on the hover state.
       * This function is passed to the Sigma constructor.
       */
      const edgeReducer = (edgeId, attributes, sourceId, targetId) => {
          const newAttributes = { ...attributes };
      
          if (hoveredNodeId) {
              // An edge is highlighted if it connects the hovered node to one of its neighbors
              // (or connects the hovered node to itself, though less common)
              const isSourceHovered = sourceId === hoveredNodeId;
              const isTargetHovered = targetId === hoveredNodeId;
              const isSourceNeighbor = hoveredNeighbors.has(sourceId);
              const isTargetNeighbor = hoveredNeighbors.has(targetId);
      
              // Highlight if connected *directly* to the hovered node
              // (Adjust logic slightly if you ONLY want edges *between* hovered and neighbors)
              if (isSourceHovered || isTargetHovered) {
                 // Highlight Edge
                  newAttributes.zIndex = 1;
                  // ** Crucially: DO NOT set 'color' here **
                  if (newAttributes.color === DIMMED_COLOR) {
                      delete newAttributes.color;
                  }
              } else {
                  // Dim Edge
                  newAttributes.color = DIMMED_COLOR;
                  newAttributes.zIndex = 0;
              }
          } else {
              // No node hovered: Reset edge state
              newAttributes.zIndex = 0;
              if (newAttributes.color === DIMMED_COLOR) {
                  delete newAttributes.color;
              }
          }
          return newAttributes;
      };

    

      const graph = new Graph();
      const renderer = new Sigma(graph, container, {
        labelDensity: 0.2,
        labelRenderedSizeThreshold: 10,
        defaultLabelSize: 14,
        nodeReducer: nodeReducer,
        edgeReducer: edgeReducer,
        edgeProgramClasses: {
          curved: sigmaEdgeCurve,
        },
      });

      // Store full labels separately
      const fullLabels = {};

      function placeNodesInMultiRings(centerX, centerY, baseRadius, minSpacing) {
          const ringNodes = graph.nodes().filter(n => graph.getNodeAttribute(n, "layout") === "ring");
          if (ringNodes.length === 0) return;
      
          let rings = [];
          let nodesPlaced = 0;
          let currentRadius = baseRadius;
      
          function maxNodesForRadius(radius) {
              let avgNodeSize = ringNodes.reduce((sum, node) => sum + graph.getNodeAttribute(node, "size"), 0) / ringNodes.length;
              let spacingFactor = avgNodeSize + minSpacing;
              return Math.max(6, Math.floor((2 * Math.PI * radius) / spacingFactor)); // Ensure at least 6 nodes per ring
          }
      
          // **Distribute Nodes into Multiple Rings Correctly**
          while (nodesPlaced < ringNodes.length) {
              let maxNodesInRing = maxNodesForRadius(currentRadius);
              let nodesInThisRing = Math.min(maxNodesInRing, ringNodes.length - nodesPlaced);
      
              let currentRingNodes = ringNodes.slice(nodesPlaced, nodesPlaced + nodesInThisRing);
              rings.push({ nodes: currentRingNodes, radius: currentRadius });
      
              nodesPlaced += nodesInThisRing;
      
              // Increase radius for the next ring
              currentRadius += baseRadius + minSpacing * 4;
          }
      
          // **Place Nodes in Their Respective Rings**
          rings.forEach(({ nodes, radius }) => {
              console.log("ring ", radius, nodes.length);
              placeNodesInRing(centerX, centerY, radius, nodes);
          });
      }
      
      function placeNodesInRing(centerX, centerY, radius, nodes) {
          if (nodes.length === 0) return;
      
          const angleStep = (2 * Math.PI) / nodes.length;
      
          nodes.forEach((node, i) => {
              const angle = i * angleStep;
              graph.mergeNodeAttributes(node, {
                  x: centerX + radius * Math.cos(angle),
                  y: centerY + radius * Math.sin(angle),
              });
          });
      }


      function placeNodesInRing(centerX, centerY, radius) {
          const ringNodes = graph.nodes().filter(n => graph.getNodeAttribute(n, "layout") === "ring");
          const angleStep = (2 * Math.PI) / ringNodes.length; // Evenly distribute

          ringNodes.forEach((node, i) => {
              const angle = i * angleStep;
              graph.mergeNodeAttributes(node, {
                  x: centerX + radius * Math.cos(angle),
                  y: centerY + radius * Math.sin(angle),
              });
          });
      }

      function placeNodesInGrid(gridStartX, gridStartY, cols, spacing) {
          const gridNodes = graph.nodes().filter(n => graph.getNodeAttribute(n, "layout") === "grid");

          gridNodes.forEach((node, i) => {
              const row = Math.floor(i / cols);
              const col = i % cols;
              graph.mergeNodeAttributes(node, {
                  x: gridStartX + col * spacing, // Column placement
                  y: gridStartY + row * spacing, // Row placement
              });
          });
      }

      function render() {
        adjustLabelPositions();
        renderer.refresh();
      }

      function formatLabel(label) {
        return label.length > 20 ? label.substring(0, 20) + "..." : label;
      }

      function adjustLabelPositions() {
        graph.updateEachNodeAttributes((node, attr) => ({
          ...attr,
          labelY: attr.y + 0.2,  // Move label slightly outside the node
        }));
      }

      ctx.handleEvent("clear", (n) => {
        graph.clear();
        renderer.refresh();
      });

      ctx.handleEvent("add_node", (node) => {
        console.log("Add node", node);

        if (node.replace || !graph.hasNode(node.id)) {
          if (node.replace && graph.hasNode(node.id)) {
            graph.dropNode(node.id);
          }

          fullLabels[node.id] = node.label;

          graph.addNode(node.id, node);

          // placeNodesInRing(centerX, centerY, radius)
          if(node.layout == 'ring') {
            placeNodesInRing(0, 0, 20); // circular.assign(graph);
            //placeNodesInMultiRings(0, 0, 30, 5);
          }
          // placeNodesInGrid(gridStartX, gridStartY, cols, spacing)
          if(node.layout == 'grid') placeNodesInGrid(-50, -20, 4, 5);

        } else {
          console.log("Node " + node.id + " already exists, ignore");
        }

        debounce(render, 100);
      });

      ctx.handleEvent("merge_node", (node) => {
        console.log("Merge node", node);

        if (graph.hasNode(node.id)) {
          graph.mergeNode(node.id, node);
          fullLabels[node.id] = node.label;
          adjustLabelPositions();
          debounce(render, 100);
        }
      });

      ctx.handleEvent("remove_node", (nodeId) => {
        if (graph.hasNode(nodeId)) {
          graph.dropNode(nodeId);
          delete fullLabels[nodeId];
        }
        debounce(render, 100);
      });

      ctx.handleEvent("add_edge", ({ source, target, props }) => {
        if (graph.hasNode(source) && graph.hasNode(target)) {
          if( graph.hasEdge(source, target) ) graph.dropEdge(source, target);
          graph.addEdge(source, target, props);
        } 
        debounce(render, 100);
      });

      ctx.handleEvent("add_edge_batched", ({ source, target, props }) => {
        if (graph.hasNode(source) && graph.hasNode(target) && !graph.hasEdge(source, target)) {
          graph.addEdge(source, target, props);
        }
      });

      ctx.handleEvent("render", (ctx) => {
        renderer.refresh();
      });

      ctx.handleEvent("remove_edge", ({ source, target }) => {
        if (graph.hasEdge(source, target)) {
          graph.dropEdge(source, target);
          console.log("dropped edge with ", source, target);
        } else {
          console.log("no edge with ", source, target);
        }
        renderer.refresh();
      });

      
      // --- External State ---
      // Holds the ID of the node currently being hovered, or null if none.
      let hoveredNodeId = null;
      // Optional: Store neighbors temporarily for performance if graph is large
      let hoveredNeighbors = new Set();
      
      // --- Event Handlers (Now much simpler!) ---
      
      renderer.on("enterNode", ({ node }) => {
        // Set the hover state
        hoveredNodeId = node;
        hoveredNeighbors = new Set(graph.neighbors(node)); // Calculate neighbors on enter
      
        // Set the specific label for the hovered node (direct manipulation is fine here)
        if (fullLabels && fullLabels[node]) { // Check if fullLabels exists and has entry
            graph.setNodeAttribute(node, "label", fullLabels[node]);
        }
      
        // Trigger reducers to re-evaluate based on the new state
        renderer.refresh();
      });
      
      renderer.on("leaveNode", ({ node }) => {
        // Reset the hover state ONLY if leaving the currently hovered node
        // (Prevents issues if mouse moves quickly between nodes)
        if (hoveredNodeId === node) {
            hoveredNodeId = null;
            hoveredNeighbors = new Set(); // Clear neighbors
      
            // Reset the label (remove attribute to revert to default/data label)
            graph.removeNodeAttribute(node, "label");
      
            // Trigger reducers to re-evaluate (will now reset everything)
            renderer.refresh();
        }
      });
    };

    """
  end
end
defmodule IrohLogger do
  use GenServer

  @node_size 5
  @impl true
  def init(config) do
    {:ok, %{graph: config.graph, datatable: config.datatable, nodes: [], relays: [], peers: %{}, msg_received_cnt: 0, messages: []}}
  end

  @impl true
  def handle_info({ :iroh_node_setup, node_addr }, %{ graph: graph, nodes: nodes } = state ) do
    
    IO.puts("Node setup: #{node_addr}")
    
    SigmaGraph.add_node(
        graph,
        %{
          id: "#{node_addr}",
          layout: "ring",
          size: @node_size,
          color: "grey",
          label: "#{node_addr}"
        },
        true
      )
    
    {:noreply, 
      state
      |> Map.put(:nodes, nodes ++ [node_addr])
    }
  end


  @impl true
  def handle_info({ :iroh_node_connected, node_addr, node_relay }, %{ graph: graph, nodes: nodes, relays: relays } = state ) do
    
    IO.puts("Node connected: #{node_addr} #{node_relay}")
    
    SigmaGraph.merge_node(
        graph,
        %{
          id: "#{node_addr}",
          layout: "ring",
          size: @node_size,
          color: "grey",
          label: "#{node_addr}"
        }
      )

    SigmaGraph.merge_node(
        graph,
        %{
          id: node_relay,
          size: @node_size,
          color: "blue",
          label: node_relay,
          x: -15,
          y: 0,
        }
      )

    SigmaGraph.add_edge(
      graph, 
      %{
        source: node_addr,
        target: node_relay,
        props: %{ type: "curved", color: "grey" }
      }
    )
    
    {:noreply, 
      state
      |> Map.put(:nodes, nodes ++ [node_addr])
      |> Map.put(:relays, relays ++ [node_relay])
      |> update_in([:peers, node_addr], fn 
        nil -> []
      end)
    }
  end

  @impl true
  def handle_info({:iroh_gossip_neighbor_up, source, target}, %{ graph: graph, peers: peers } = state) do
    IO.puts("Neighbor up: #{source} #{target}")

    new_peers = peers[source] ++ [target]

    peers_cnt = Enum.count(new_peers)

    color =
      cond do
        peers_cnt == 0 -> "red"
        peers_cnt == 1 -> "orange"
        peers_cnt < 3 -> "yellow"
        peers_cnt >= 3 -> "green"
      end

    SigmaGraph.merge_node(
      graph,
      %{
        id: source,
        layout: "ring",
        size: @node_size + peers_cnt * 2,
        color: color,
        label: "#{source} P:#{peers_cnt}"
      }
    )
    
    SigmaGraph.add_edge(
      graph, 
      %{
        source: source,
        target: target,
        props: %{ color: "grey", size: 1 }
      }
    )
    {:noreply, 
      state
      |> update_in([:peers, source], fn _ -> new_peers end)
    }
  end

  @impl true
  def handle_info({:iroh_gossip_neighbor_down, source, target}, %{ graph: graph } = state) do
    IO.puts("Neighbor down: #{source} #{target}")
    SigmaGraph.remove_edge(
      graph, 
      %{
        source: source,
        target: target,
      }
    )
    {:noreply, 
      state
      |> update_in([:peers, source], fn peers -> peers -- [target] end)
    }
  end

  @impl true
  def handle_info({:iroh_gossip_node_discovered_, source, target}, %{ graph: graph } = state) do
    SigmaGraph.add_edge(
      graph, 
      %{
        source: source,
        target: target,
        props: %{ color: "#ddd" }
      }
    )
    {:noreply, state}
  end

  @impl true
  def handle_info({:iroh_gossip_message_received, source, payload}, %{ msg_received_cnt: msg_received_cnt, graph: graph, messages: messages, datatable: datatable } = state) do
    
    edge = %{source: payload, target: source}
    IO.puts("#{__MODULE__} handle_info :iroh_gossip_message_received #{source} #{payload} #{inspect(edge)}")
    SigmaGraph.remove_edge(graph, edge)
    
    new_messages = messages ++ [%{source: source, payload: payload}]
    
    Kino.DataTable.update(datatable, new_messages)
  
    {:noreply, state
      |> Map.put(:msg_received_cnt, msg_received_cnt + 1)
      |> Map.put(:messages, new_messages)
    }
  end

  @impl true
  def handle_info(msg, state) do
    IO.puts("#{__MODULE__} Catchall #{inspect(msg)}")
    {:noreply, state}
  end
end




defmodule UIGenServer do
  use GenServer
  def init( init_state ) do

    graph = init_state.graph
    datatable = init_state.datatable

    {:ok, logger_pid} = GenServer.start_link(IrohLogger, %{graph: graph, datatable: datatable})
    
    # {:ok, observer_pid} = IrohObserver.start_link({graph, datatable})
  
    { :ok, manager_pid }  = IrohEx.NodeManager.start_link(logger_pid)
    
    {:ok, init_state
      |> Map.put(:graph, graph)
      |> Map.put(:datatable, datatable)
      |> Map.put(:logger_pid,  logger_pid)
      |> Map.put(:manager_pid, manager_pid)
    }
  end

  def handle_call({:config, key, value}, _from, state) do    
    IO.puts("#{__MODULE__} :config #{key} #{value}")
    {:reply, value, Map.put(state, key, value)}
  end

  def handle_call(:reset, _from, %{
    datatable: datatable,
    graph: graph,
    manager_pid: manager_pid,
  } = state) do
    GenServer.call(manager_pid, :reset)
  
    Kino.DataTable.update(datatable, [])
    SigmaGraph.clear(graph)
    
    :erlang.garbage_collect()
  
    {:reply, [], state}
  end

  def handle_call(:start_sim, _from, %{
    datatable: datatable,
    graph: graph,
    num_nodes: num_nodes,
    delay_ms: delay_ms,
    manager_pid: manager_pid,
    logger_pid: logger_pid,
    #delay_msg_ms: delay_msg_ms
  } = state) do
    Kino.DataTable.update(datatable, [])
    SigmaGraph.clear(graph)
  
    GenServer.call(manager_pid, {:create_nodes, num_nodes, logger_pid}, 30_000)
    # why? 
    Process.sleep(delay_ms)
    
    {:reply, [], state}
  end  

  def handle_call(:send_messages, _from, %{
    datatable: _datatable,
    graph: graph,
    num_messages: num_messages,
    use_random: use_random,
    delay_ms: delay_ms,
  } = state) do
    nodes = IrohEx.NodeSupervisor.get_children()
  
    # IO.inspect(nodes, label: "Nodes test")
    send_many_messages(graph, nodes, num_messages, use_random, 100 )
  
    Process.sleep(delay_ms)
  
    reports = Enum.map(nodes, fn n ->
      #IO.puts("Process alive? #{Process.alive?(n)}")
  
      #IO.puts("node report: #{inspect(n)}")
      report = GenServer.call(n, :report)
      #IO.puts("report: #{inspect(report)}")
      report
    end)
  
    IO.puts("Node reports #{inspect(reports)}" )

    {:reply, [], state}
  
  end


  def send_many_messages(graph, nodes, msg_cnt, use_random_node, rand_msg_delay \\ 100) do

    no_random_node = Enum.random(nodes)

    stream =
      Stream.map(1..msg_cnt, fn x ->
        # Return the *action* to be performed
        fn ->
          node = case use_random_node do 
            true -> Enum.random(nodes)
            false -> no_random_node
          end
          _node_id = GenServer.call(node, :get_node_addr)
          #Native.gen_node_addr(node)
          # Sleep *before* the task
          # Process.sleep(rand_msg_delay)

          msg = "MSG:#{x}"

          #Native.send_message(node, msg)
          #GenServer.cast(manager_pid, {:send_message, node, msg})
          # GenServer.cast(node, {:send_message, msg})
          Process.send_after(node, {:send_message, msg}, :rand.uniform(rand_msg_delay))

          SigmaGraph.add_node(
            graph,
            %{id: msg, layout: "grid", size: 5, label: msg, color: "grey"},
            true
          )

          recipient_nodes = nodes -- [node]

          Enum.each(recipient_nodes, fn n -> 
            node_id = GenServer.call(n, :get_node_addr)
            edge = %{source: msg, target: node_id, props: %{ type: "curved" }}
            IO.puts("#{__MODULE__} send_message #{msg} edge: #{inspect(edge)}")
            SigmaGraph.add_edge(graph, edge)  
          end)
        end
      end)

    stream
    |> Task.async_stream(fn action -> action.() end, max_concurrency: Enum.count(nodes))
    # |> Task.async_stream(fn action -> action.() end, max_concurrency: Enum.count(nodes))
    |> Enum.to_list()

    Process.sleep(1000)
    
  end
  
end
graph = SigmaGraph.new()
# do not show graph yet
nil

Run stuff


# UI Controls
num_nodes = Kino.Input.number("Number of Nodes", default: 30, min: 3)
delay_ms = Kino.Input.number("Delay after connect, before sending messages", default: 1000, min: 0)
delay_msg_ms = Kino.Input.number("Delay msg random", default: 1000, min: 0)
num_messages = Kino.Input.number("Number of Messages", default: 3, min: 1)
use_random = Kino.Input.checkbox("Use Random Sender", default: true)
setup_button = Kino.Control.button("Setup nodes")
send_button = Kino.Control.button("Send messages")
reset_button = Kino.Control.button("Reset")

datatable =
  Kino.DataTable.new(
    [],
    keys: [:source, :payload]
  )

buttons = Kino.Layout.grid([setup_button,
    send_button,
    reset_button], columns: 3)

# Display Controls
grid =
  Kino.Layout.grid([
    graph,
    num_nodes,
    delay_ms,
    delay_msg_ms,
    num_messages,
    use_random,
    buttons,
    datatable
  ])

grid

num_nodes_value = Kino.Input.read(num_nodes)
num_messages_value = Kino.Input.read(num_messages) #Kino.Input.read(num_messages)
use_random_value = Kino.Input.read(use_random)
delay_ms_value = Kino.Input.read(delay_ms)
delay_msg_ms_value = Kino.Input.read(delay_msg_ms)

{:ok, ui_pid} = GenServer.start_link(UIGenServer, %{
  graph: graph,
  datatable: datatable,
  num_nodes: num_nodes_value, #Kino.Input.read(num_nodes),
  num_messages: num_messages_value, #Kino.Input.read(num_messages),
  use_random: use_random_value, #Kino.Input.read(use_random),
  delay_ms: delay_ms_value, # Kino.Input.read(delay_ms),
  delay_msg_ms: delay_msg_ms_value, # Kino.Input.read(delay_msg_ms),
})

Kino.Control.tagged_stream(
  num_nodes: num_nodes,
  num_messages: num_messages,
  use_random: use_random,
  delay_ms: delay_ms,
  delay_msg_ms: delay_msg_ms,
  setup_button: setup_button,
  send_button: send_button,
  reset_button: reset_button
)
|> Kino.listen(fn {key, event} ->

  case event.type do
    :change ->
      if Map.has_key?(event, :type) and Map.has_key?(event, :value) do
        IO.puts(":config #{key} #{event.value}")
        GenServer.call(ui_pid, { :config, key, event.value })
      end
    :click ->
      case key do
        :setup_button -> GenServer.call(ui_pid, :start_sim, 30_000)
        :send_button -> GenServer.call(ui_pid, :send_messages, 30_000)
        :reset_button -> GenServer.call(ui_pid, :reset)
      end
  end

  IO.puts("#{key}, #{inspect(event)}")
  #IO.inspect(event, label: "event")
end)