Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Applications tab with supervision trees #136

Merged
merged 11 commits into from May 30, 2020
1 change: 1 addition & 0 deletions assets/css/app.scss
Expand Up @@ -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";

Expand Down
47 changes: 47 additions & 0 deletions 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;
}
}
@@ -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
@@ -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