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

Filtered process list #411

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
ff627d2
Enable filtering of process PIDs
bokner Feb 17, 2023
63bc1bf
Fix process count
bokner Feb 22, 2023
3e92e97
Use the process naming if it's created by process filter
bokner Feb 27, 2023
9f0c928
Customize process_info page
bokner Mar 1, 2023
a8fb339
Let the callback decide what to do with the filter name
bokner Mar 2, 2023
6c97999
Behaviour for process filter
bokner Mar 2, 2023
e628820
Merge branch 'main' into filtered_process_list
bokner Mar 2, 2023
94265e3
Fixes
bokner Mar 2, 2023
a13b1a9
Refactoring
bokner Mar 3, 2023
ed5d42f
process_filter -> page_filter
bokner Mar 3, 2023
d1e4834
Refactoring, fixes
bokner Mar 6, 2023
9db7de8
The content returned by the info_content/2 can be either of Phoenix.L…
bokner Mar 6, 2023
1be224a
Update lib/phoenix/live_dashboard/page_filter.ex
bokner Mar 7, 2023
fe0f424
Update lib/phoenix/live_dashboard/components/table_component.ex
bokner Mar 7, 2023
2c3a7be
Update lib/phoenix/live_dashboard/pages/processes_page.ex
bokner Mar 7, 2023
2b1f7c4
Update lib/phoenix/live_dashboard/components/table_component.ex
bokner Mar 7, 2023
2f12ee8
Fix formatting and indentation
bokner Mar 7, 2023
58dc67f
Add description for Phoenix.LiveDashboard.PageFilter
bokner Mar 8, 2023
3ac44c1
Get rid of rendering the modal info page (might go to another PR)
bokner Mar 9, 2023
547ca85
Retrieve filter list from the remote node
bokner Mar 9, 2023
71798ec
Put it all together (wip, cleanup to follow)
bokner Mar 11, 2023
e4a5222
Various fixes
bokner Mar 12, 2023
8e7c2cd
Update lib/phoenix/live_dashboard/components/table_component.ex
bokner Mar 24, 2023
df782d6
Various changes to address the review
bokner Mar 24, 2023
7eec63d
Have row_fetcher return 5-tuple
bokner Mar 25, 2023
7242996
Add documentation for the case row_fetcher returns page filter data
bokner Mar 25, 2023
bbdbb62
Unit tests for process filter
bokner Mar 25, 2023
97b5838
Unit tests: add filter to process paths for compability with table co…
bokner Mar 25, 2023
5f0c700
Formatting
bokner Mar 27, 2023
d146580
Fix process filter module in test helper
bokner Mar 27, 2023
30d5ee6
Use :if for conditional filter form
bokner Apr 16, 2023
6c9a3e1
Fix active_filter form
bokner Apr 16, 2023
a74954b
Rename process_filter to filter
bokner Apr 16, 2023
fe86d97
Fix misplaced @impl
bokner Apr 16, 2023
e6d9ec1
Have process filter tests for SystemInfo to run with async: false
bokner Apr 26, 2023
235a6d5
Unit tests for the filter in the process page
bokner Apr 26, 2023
e056334
do not use active_filter; instead, update filter in fetch_rows
bokner Apr 27, 2023
408a28b
Refactoring, fixes
bokner Apr 29, 2023
a3b5137
Simplify the base implementation of default_filter/0
bokner Apr 29, 2023
2076105
More refactoring
bokner Apr 29, 2023
d6e566b
Merge branch 'filtered_process_list' of github.com:bokner/phoenix_liv…
bokner Apr 29, 2023
ce63526
Use default filter if the filter is not listed
bokner Apr 29, 2023
93d1fb5
Minor
bokner Apr 29, 2023
e988f05
Do not use hardcoded filter values in test cases
bokner Apr 29, 2023
a0ce9f8
Update docs for row_fetcher
bokner Apr 29, 2023
f826536
Fix the invalid filter value handling
bokner Apr 29, 2023
fa3f109
Move filter param down the list of params
bokner Apr 29, 2023
7fe72ca
Adjust filter_list to nil, if it's empty
bokner Apr 30, 2023
e5c4b6f
Merge branch 'phoenixframework:main' into filtered_process_list
bokner May 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 60 additions & 7 deletions lib/phoenix/live_dashboard/components/table_component.ex
Expand Up @@ -83,22 +83,45 @@ defmodule Phoenix.LiveDashboard.TableComponent do
row_fetcher: row_fetcher
} = assigns

{rows, total, socket} = fetch_rows(row_fetcher, table_params, page.node, socket)
assigns = Map.merge(assigns, %{rows: rows, total: total})
{rows, total, active_filter, available_filters, socket} =
fetch_rows(row_fetcher, table_params, page.node, socket)

## For the view, adjust available_filters to nil if it's empty
available_filters = if available_filters == [], do: nil, else: available_filters

assigns =
Map.merge(assigns, %{
rows: rows,
total: total,
table_params: Map.put(table_params, :filter, active_filter),
filter_list: available_filters
})

{:ok, assign(socket, assigns)}
end

defp fetch_rows(row_fetcher, table_params, page_node, socket)
when is_function(row_fetcher, 2) do
{rows, total} = row_fetcher.(table_params, page_node)
{rows, total, socket}
{active_filter, available_filters, rows, total} =
with {rows, total} <- row_fetcher.(table_params, page_node) do
{nil, [], rows, total}
end

{rows, total, active_filter, available_filters, socket}
end

defp fetch_rows({row_fetcher, initial_state}, table_params, page_node, socket)
when is_function(row_fetcher, 3) do
state = Map.get(socket.assigns, :row_fetcher_state, initial_state)
{rows, total, state} = row_fetcher.(table_params, page_node, state)
{rows, total, assign(socket, :row_fetcher_state, state)}

{active_filter, available_filters, rows, total, state} =
with {rows, total, state} <- row_fetcher.(table_params, page_node, state) do
{nil, [], rows, total, state}
end

{rows, total, active_filter, available_filters,
socket
|> assign(:row_fetcher_state, state)}
end

defp normalize_table_params(assigns) do
Expand All @@ -125,7 +148,15 @@ defmodule Phoenix.LiveDashboard.TableComponent do
search = all_params["search"]
search = if search == "", do: nil, else: search

table_params = %{sort_by: sort_by, sort_dir: sort_dir, limit: limit, search: search}
filter = all_params["filter"]

table_params = %{
sort_by: sort_by,
sort_dir: sort_dir,
limit: limit,
search: search,
filter: filter
}

assigns
|> Map.put(:table_params, table_params)
Expand Down Expand Up @@ -186,6 +217,19 @@ defmodule Phoenix.LiveDashboard.TableComponent do
</div>
</form>

<form :if={@filter_list} phx-change="select_filter" phx-target={@myself} class="form-inline">
<div class="form-row align-items-center">
<div class="col-auto">Filter</div>
<div class="col-auto">
<div class="input-group input-group-sm">
<select name="filter" class="custom-select" id="filter-select">
<%= options_for_select(@filter_list, @table_params.filter) %>
</select>
</div>
</div>
</div>
</form>

<div class="card tabular-card mb-4 mt-4">
<div class="card-body p-0">
<div class="dash-table-wrapper">
Expand Down Expand Up @@ -249,6 +293,15 @@ defmodule Phoenix.LiveDashboard.TableComponent do
{:noreply, push_patch(socket, to: to)}
end

def handle_event("select_filter", %{"filter" => filter}, socket) do
# If filter is not in thefilter list, force the default filter
filter = filter in socket.assigns.filter_list && filter

table_params = %{socket.assigns.table_params | filter: filter}
to = PageBuilder.live_dashboard_path(socket, socket.assigns.page, table_params)
{:noreply, push_patch(socket, to: to)}
end

defp sort_link(assigns) do
if assigns.table_params.sort_by == assigns.column.field do
~H"""
Expand Down
13 changes: 12 additions & 1 deletion lib/phoenix/live_dashboard/page_builder.ex
Expand Up @@ -96,7 +96,7 @@ defmodule Phoenix.LiveDashboard.PageBuilder do

We currently support `card/1`, `fields_card/1`, `row/1`,
`shared_usage_card/1`, and `usage_card/1`;
and the live components `live_layered_graph/1`, `live_nav_bar/1`,
and the live components `live_layered_graph/1`, `live_nav_bar/1`,
and `live_table/1`.

## Helpers
Expand Down Expand Up @@ -257,6 +257,17 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
In this case, the function will receive the state as third argument and must return
a tuple with the rows, the total number, and the new state for the following call:
`{(params(), node(), term() -> {list(), integer() | binary(), term()}), term()}`

Optionally, if the page decides to implement custom page filter
(please see `Phoenix.LiveDashboard.PageFilter` for details), the function must return
- for `row_fetcher/2`:
a tuple with the active filter, list of available filters, the list of rows, and the total number
for the following call:
`(params(), node() -> {binary() | nil, list(), list(), integer() | binary()})`
- for `row_fetcher/3`:
a tuple with the active filter, list of available filters, the list of rows, the total number,
and the new state for the following call:
`(params(), node(), term() -> {binary() | nil, list(), list(), integer() | binary(), term()})`
"""

attr :rows_name, :string,
Expand Down
69 changes: 69 additions & 0 deletions lib/phoenix/live_dashboard/page_filter.ex
@@ -0,0 +1,69 @@
defmodule Phoenix.LiveDashboard.PageFilter do
@moduledoc """
Page filter allows to customize the list of items for table components.
This could be useful for cases where there are too many items and/or
the customized list of items is desired.
For instance, you may have millions of processes per node. Then, using the standard Processes page
may present a performance problem. Besides, you may want to look at the application-specific groups of processes,
rather then going through the whole list.

Example of custom page filter implementation:

defmodule ProcessFilter.Demo do
@behaviour Phoenix.LiveDashboard.PageFilter

@impl true
def list() do
["All", "Registered", "Phoenix"]
end

@impl true
def default_filter() do
"Phoenix"
end

@impl true
def filter("Registered") do
Process.registered() |> Enum.map(fn name -> Process.whereis(name) end)
end

def filter("All") do
Process.list()
end

def filter("Phoenix") do
Process.registered() |> Enum.flat_map(fn name -> String.contains?(to_string(name), "Phoenix") &&
[Process.whereis(name)] || []
end)
end
end


To enable the filter for Processes page, add this to your config.exs:

config :phoenix_live_dashboard, :process_filter, ProcessFilter.Demo

What will happen:

- The Processes page will have Filter dropdown defined in list() function;
- The groups of processes will be displayed according to selected filter

"""

@callback list() :: [String.t()]

@callback filter(filter_name :: String.t()) :: [any()]

@callback default_filter() :: String.t()

defmacro __using__(_) do
quote do
@behaviour Phoenix.LiveDashboard.PageFilter
def default_filter() do
List.first(list())
end

defoverridable default_filter: 0
end
end
end
8 changes: 2 additions & 6 deletions lib/phoenix/live_dashboard/pages/processes_page.ex
bokner marked this conversation as resolved.
Show resolved Hide resolved
Expand Up @@ -35,12 +35,8 @@ defmodule Phoenix.LiveDashboard.ProcessesPage do
end

defp fetch_processes(params, node, state) do
%{search: search, sort_by: sort_by, sort_dir: sort_dir, limit: limit} = params

{processes, count, state} =
SystemInfo.fetch_processes(node, search, sort_by, sort_dir, limit, state)

{processes, count, state}
%{search: search, sort_by: sort_by, sort_dir: sort_dir, limit: limit, filter: filter} = params
SystemInfo.fetch_processes(node, search, sort_by, sort_dir, limit, filter, state)
end

defp row_attrs(process) do
Expand Down
37 changes: 32 additions & 5 deletions lib/phoenix/live_dashboard/system_info.ex
Expand Up @@ -44,14 +44,23 @@ defmodule Phoenix.LiveDashboard.SystemInfo do

## Fetchers

def fetch_processes(node, search, sort_by, sort_dir, limit, prev_reductions \\ nil) do
def fetch_processes(
node,
search,
sort_by,
sort_dir,
limit,
filter,
prev_reductions \\ nil
) do
search = search && String.downcase(search)

:rpc.call(node, __MODULE__, :processes_callback, [
search,
sort_by,
sort_dir,
limit,
filter,
prev_reductions
])
end
Expand Down Expand Up @@ -279,11 +288,12 @@ defmodule Phoenix.LiveDashboard.SystemInfo do
]

@doc false
def processes_callback(search, sort_by, sort_dir, limit, prev_reductions) do
def processes_callback(search, sort_by, sort_dir, limit, filter, prev_reductions) do
multiplier = sort_dir_multipler(sort_dir)
{active_filter, available_filters, process_list} = get_process_filter_data(filter)

processes =
for pid <- Process.list(),
for pid <- process_list,
info = process_info(pid, prev_reductions[pid]),
show_process?(info, search) do
sorter = info[sort_by] * multiplier
Expand All @@ -292,10 +302,23 @@ defmodule Phoenix.LiveDashboard.SystemInfo do

next_state = for {_sorter, info} <- processes, into: %{}, do: {info[:pid], info[:reductions]}

count = if search, do: length(processes), else: :erlang.system_info(:process_count)
count = length(processes)
processes = processes |> Enum.sort() |> Enum.take(limit) |> Enum.map(&elem(&1, 1))

{processes, count, next_state}
{active_filter, available_filters, processes, count, next_state}
end

defp get_process_filter_data(filter) do
case Application.get_env(:phoenix_live_dashboard, :process_filter) do
bokner marked this conversation as resolved.
Show resolved Hide resolved
nil ->
{nil, [], Process.list()}

filter_mod ->
available_filters = filter_mod.list()
active_filter = (filter in available_filters && filter) || filter_mod.default_filter()
alexcastano marked this conversation as resolved.
Show resolved Hide resolved
process_list = filter_mod.filter(active_filter)
{active_filter, available_filters, process_list}
end
end

defp process_info(pid, prev_reductions) do
Expand Down Expand Up @@ -796,6 +819,10 @@ defmodule Phoenix.LiveDashboard.SystemInfo do
defp pid_or_port_details(port) when is_port(port), do: to_port_details(port)
defp pid_or_port_details(reference) when is_reference(reference), do: reference

def to_process_details(detail_map) when is_map(detail_map) do
struct(ProcessDetails, detail_map)
end

def to_process_details(pid) when is_pid(pid) and node(pid) == node() do
{name, initial_call} =
case Process.info(pid, [:initial_call, :dictionary, :registered_name]) do
Expand Down
Expand Up @@ -75,6 +75,6 @@ defmodule Phoenix.LiveDashboard.ApplicationsPageTest do

defp applications_path(limit, search, sort_by, sort_dir) do
"/dashboard/applications?" <>
"limit=#{limit}&search=#{search}&sort_by=#{sort_by}&sort_dir=#{sort_dir}"
"filter=&limit=#{limit}&search=#{search}&sort_by=#{sort_by}&sort_dir=#{sort_dir}"
alexcastano marked this conversation as resolved.
Show resolved Hide resolved
end
end
2 changes: 1 addition & 1 deletion test/phoenix/live_dashboard/pages/ets_page_test.exs
Expand Up @@ -93,6 +93,6 @@ defmodule Phoenix.LiveDashboard.EtsPageTest do

defp ets_path(limit, search, sort_by, sort_dir) do
"/dashboard/ets?" <>
"limit=#{limit}&search=#{search}&sort_by=#{sort_by}&sort_dir=#{sort_dir}"
"filter=&limit=#{limit}&search=#{search}&sort_by=#{sort_by}&sort_dir=#{sort_dir}"
end
end
2 changes: 1 addition & 1 deletion test/phoenix/live_dashboard/pages/ports_page_test.exs
Expand Up @@ -98,6 +98,6 @@ defmodule Phoenix.LiveDashboard.PortsPageTest do

defp ports_path(limit, search, sort_by, sort_dir) do
"/dashboard/ports?" <>
"limit=#{limit}&search=#{search}&sort_by=#{sort_by}&sort_dir=#{sort_dir}"
"filter=&limit=#{limit}&search=#{search}&sort_by=#{sort_by}&sort_dir=#{sort_dir}"
end
end