diff --git a/assets/css/app.scss b/assets/css/app.scss index 2df64723..d778180a 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -25,6 +25,7 @@ @import "./app/components/resource_usage"; @import "./app/components/modals"; @import "./app/components/tables"; +@import "./app/components/application_tree"; @import "./app/liveview"; diff --git a/assets/css/app/components/_application_tree.scss b/assets/css/app/components/_application_tree.scss new file mode 100644 index 00000000..68d21387 --- /dev/null +++ b/assets/css/app/components/_application_tree.scss @@ -0,0 +1,47 @@ +.active_applications_list { + ul.active_items button:focus { + outline: none !important; + background-color: $color-gray-100; + } + + ul.active_items button:hover { + outline: none !important; + background-color: $color-gray-100; + } +} + +.application_tree { + .node{ + fill: $color-gray-100; + stroke: $color-gray-warm-200; + stroke-width:1; + } + + .node:hover { + cursor: pointer; + } + + .line { + stroke: $color-gray-warm-600; + stroke-width:2 + } + + .tree { + position: relative; + top: 20px; + } + .tree_node_text { + font-size: 14px; + font-family: 'LiveDashboardFont'; + } +} + +.applications_tab{ + .overflow { + max-height: 1000px; + margin-bottom: 10px; + overflow:auto; + -webkit-overflow-scrolling: touch; + padding-right: 0px; + } +} \ No newline at end of file diff --git a/lib/phoenix/live_dashboard/helpers/tree_helpers/reingold_tifford_algorithm.ex b/lib/phoenix/live_dashboard/helpers/tree_helpers/reingold_tifford_algorithm.ex new file mode 100644 index 00000000..19be2525 --- /dev/null +++ b/lib/phoenix/live_dashboard/helpers/tree_helpers/reingold_tifford_algorithm.ex @@ -0,0 +1,217 @@ +defmodule Phoenix.LiveDashboard.ReingoldTilford do + # Reingold-Tilford algorithm for drawing trees + @moduledoc false + + @node_height 30 + @node_y_separation 10 + @total_y_distance @node_height + @node_y_separation + @node_x_separation 50 + + def set_layout_settings(tree, fun) do + tree + |> change_representation(0, fun) + |> calculate_initial_y(0, []) + |> ensure_children_inside_screen() + |> put_final_y_values(0) + |> put_x_position(:first_call) + end + + defp change_representation({value, children}, level, fun) do + children = Enum.map(children, &change_representation(&1, level + 1, fun)) + + %{ + x: 0, + y: 0, + children: children, + modifier: 0, + type: if(children == [], do: :leaf, else: :subtree), + height: @node_height, + width: fun.(value), + level: level, + value: value + } + end + + defp calculate_initial_y(%{children: children} = node, previous_sibling, top_siblings) do + {_, children} = + children + |> Enum.reduce({0, []}, fn n, {prev_sibling, nodes} -> + new_node = calculate_initial_y(n, prev_sibling, nodes) + {new_node.y, [new_node | nodes]} + end) + + {first_child, last_child} = + if node.type != :leaf do + [last_child | _] = children + [first_child | _] = Enum.reverse(children) + {first_child, last_child} + else + {nil, nil} + end + + new_node = + case {node_type(node), top_siblings} do + {:leaf, []} -> + %{node | y: 0} + + {:leaf, _} -> + %{node | y: previous_sibling + @total_y_distance} + + {:small_subtree, []} -> + %{node | y: first_child.y} + + {:small_subtree, _} -> + %{ + node + | y: previous_sibling + @total_y_distance, + modifier: previous_sibling + @total_y_distance - first_child.y + } + + {:big_subtree, []} -> + mid = (last_child.y + first_child.y) / 2 + %{node | y: mid} + + {:big_subtree, _} -> + mid = (last_child.y + first_child.y) / 2 + + %{ + node + | y: previous_sibling + @total_y_distance, + modifier: previous_sibling + @total_y_distance - mid + } + end + + if children != [] and top_siblings != [] do + fix_sibling_conflicts(%{new_node | children: children}, top_siblings) + else + %{new_node | children: children} + end + end + + defp node_type(node) do + cond do + node.type == :leaf -> :leaf + match?([_], node.children) -> :small_subtree + true -> :big_subtree + end + end + + defp put_final_y_values(%{children: children} = node, mod) do + new_children = Enum.map(children, &put_final_y_values(&1, node.modifier + mod)) + + %{node | y: node.y + mod, children: new_children} + end + + def fix_sibling_conflicts(node, [top_most_sibling | other_siblings]) do + top = search_contour({node, %{}, 1, 0}, :top) + bottom = search_contour({top_most_sibling, %{}, 1, 0}, :bottom) + + distance = + [Map.values(top), Map.values(bottom)] + |> Enum.zip() + |> Enum.reduce(0, fn {t, b}, acc -> + if t - b + acc < @total_y_distance do + @total_y_distance - (t - b) + else + acc + end + end) + + if distance > 0 do + new_node = %{ + node + | y: node.y + distance, + modifier: node.modifier + distance + } + + fix_sibling_conflicts(new_node, other_siblings) + else + fix_sibling_conflicts(node, other_siblings) + end + end + + def fix_sibling_conflicts(node, []), do: node + + def search_contour({node, contour, level, mod_sum}, :top) do + result = + if Map.has_key?(contour, level) do + Map.put(contour, level, min(contour[level], node.y + mod_sum)) + else + Map.put(contour, level, node.y + mod_sum) + end + + Enum.reduce( + node.children, + result, + &search_contour({&1, &2, level + 1, mod_sum + node.modifier}, :top) + ) + end + + def search_contour({node, contour, level, mod_sum}, :bottom) do + result = + if Map.has_key?(contour, level) do + Map.put(contour, level, max(contour[level], node.y + mod_sum)) + else + Map.put(contour, level, node.y + mod_sum) + end + + Enum.reduce( + node.children, + result, + &search_contour({&1, &2, level + 1, mod_sum + node.modifier}, :bottom) + ) + end + + defp ensure_children_inside_screen(node) do + result = + {node, %{}, 1, 0} + |> search_contour(:top) + |> Enum.reduce(0, fn {_, value}, acc -> + if value + acc < 0, do: value * -1, else: acc + end) + + %{node | y: node.y + result, modifier: node.modifier + result} + end + + defp put_x_position(%{children: children} = node, position, max_width) do + children = + Enum.reduce( + children, + [], + &[ + put_x_position(&1, max_width[node.level] + position + @node_x_separation, max_width) + | &2 + ] + ) + + %{node | x: position, children: children} + end + + defp put_x_position(%{children: children} = tree, :first_call) do + max_width = find_max_width_by_level(tree, %{}) + + children = + Enum.reduce( + children, + [], + &[put_x_position(&1, tree.width + @node_x_separation, max_width) | &2] + ) + + %{tree | x: 0, children: children} + end + + defp find_max_width_by_level(node, max_values) do + max_values = + if Map.has_key?(max_values, node.level) do + Map.put(max_values, node.level, max(max_values[node.level], node.width)) + else + Map.put(max_values, node.level, node.width) + end + + Enum.reduce( + node.children, + max_values, + &find_max_width_by_level(&1, &2) + ) + end +end diff --git a/lib/phoenix/live_dashboard/helpers/tree_helpers/tree_drawing_helpers.ex b/lib/phoenix/live_dashboard/helpers/tree_helpers/tree_drawing_helpers.ex new file mode 100644 index 00000000..0d7f3e7a --- /dev/null +++ b/lib/phoenix/live_dashboard/helpers/tree_helpers/tree_drawing_helpers.ex @@ -0,0 +1,90 @@ +defmodule Phoenix.LiveDashboard.TreeDrawingHelpers do + @node_x_separation 50 + def extract_nodes(%{children: children} = node) do + [ + format_node(node) + | Enum.reduce(children, [], &(extract_nodes(&1) ++ &2)) + ] + end + + def extract_lines(%{children: children} = node) do + lines_to_children = lines_to_children(node) + + aditional_lines = + cond do + [node] == children -> + [child | _] = children + line_from_parent(node, child) + + match?([_ | _], children) -> + [child | _] = children + [vertical_line(node, child), line_from_parent(node, child)] + + true -> + [] + end + + children_lines = Enum.reduce(children, [], &(extract_lines(&1) ++ &2)) + lines_to_children ++ aditional_lines ++ children_lines + end + + defp line_from_parent(node, child) do + %{ + x1: node.x + node.width, + x2: child.x - @node_x_separation / 2, + y1: node.y + node.height / 2, + y2: node.y + node.height / 2 + } + end + + defp vertical_line(%{children: children} = node, child) do + [top_most_child | _] = children + [bottom_most_child | _] = Enum.reverse(children) + + %{ + x1: child.x - @node_x_separation / 2, + x2: child.x - @node_x_separation / 2, + y1: top_most_child.y + node.height / 2, + y2: bottom_most_child.y + node.height / 2 + } + end + + defp lines_to_children(%{children: children} = node) do + Enum.reduce(children, [], fn n, acc -> + [ + %{ + x1: n.x - @node_x_separation / 2, + x2: n.x, + y1: n.y + node.height / 2, + y2: n.y + node.height / 2 + } + | acc + ] + end) + end + + def svg_size(nodes) do + node_y = Enum.max_by(nodes, fn x -> {x.y, x.height} end) + node_x = Enum.max_by(nodes, fn x -> {x.x, x.width} end) + {node_x.x + node_x.width, node_y.y + node_y.height} + end + + defp format_node(%{value: {_, pid, name}} = node) do + %{ + name: format_name({pid, name}), + pid: pid, + x: node.x, + y: node.y, + height: node.height, + width: node.width + } + end + + defp format_name({pid, name}) do + if name == [] do + pid |> inspect |> String.trim_leading("#PID") + else + inspect(name) + end + end +end diff --git a/lib/phoenix/live_dashboard/live/apps_live.ex b/lib/phoenix/live_dashboard/live/apps_live.ex new file mode 100644 index 00000000..49d6012d --- /dev/null +++ b/lib/phoenix/live_dashboard/live/apps_live.ex @@ -0,0 +1,149 @@ +defmodule Phoenix.LiveDashboard.AppsLive do + use Phoenix.LiveDashboard.Web, :live_view + + alias Phoenix.LiveDashboard.{ + SystemInfo, + ProcessInfoComponent, + ReingoldTilford, + TreeDrawingHelpers + } + + @temporary_assigns [ + nodes: [], + lines: [], + height: 500, + width: 500, + params: %{} + ] + + @impl true + def mount(%{"node" => _} = params, session, socket) do + {:ok, assign_mount(socket, :apps, params, session, true), temporary_assigns: @temporary_assigns} + end + + @impl true + def handle_params(params, _url, socket) do + socket = + socket + |> assign_params(params) + |> fetch_started_applications() + |> assign_application(params) + |> fetch_nodes_and_lines() + |> fetch_width_and_height() + + {:noreply, socket} + end + + @impl true + def render(assigns) do + ~L""" +