feat: create a LiveDashboard page for viewing configuration servers.

This commit is contained in:
James Harton 2021-09-29 10:40:17 +13:00
parent 477e938539
commit 94b23e98de
6 changed files with 286 additions and 16 deletions

38
LICENSE Normal file
View file

@ -0,0 +1,38 @@
Copyright 2021 James Harton
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
* No Harm: The software may not be used by anyone for systems or activities that
actively and knowingly endanger, harm, or otherwise threaten the physical,
mental, economic, or general well-being of other individuals or groups, in
violation of the United Nations Universal Declaration of Human Rights
(https://www.un.org/en/universal-declaration-human-rights/).
* Services: If the Software is used to provide a service to others, the licensee
shall, as a condition of use, require those others not to use the service in
any way that violates the No Harm clause above.
* Enforceability: If any portion or provision of this License shall to any
extent be declared illegal or unenforceable by a court of competent
jurisdiction, then the remainder of this License, or the application of such
portion or provision in circumstances other than those as to which it is so
declared illegal or unenforceable, shall not be affected thereby, and each
portion and provision of this Agreement shall be valid and enforceable to the
fullest extent permitted by law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This Hippocratic License is an Ethical Source license
(https://ethicalsource.dev) derived from the MIT License, amended to limit the
impact of the unethical use of open source software.

View file

@ -1,11 +1,15 @@
# Lamina.Dashboard
**TODO: Add description**
`Lamina.Dashboard` is a tool to visualise the current runtime configuration of
the system.
It works as an additional page for [Phoenix LiveDashboard](https://hex.pm/packages/phoenix_live_dashboard).
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `lamina_dashboard` to your list of dependencies in `mix.exs`:
`Lamina.Dashboard` is [available in Hex](https://hex.pm/packages/lamina_dashboard),
the package can be installed by adding `lamina_dashboard` to your list of
dependencies in `mix.exs`:
```elixir
def deps do
@ -15,7 +19,19 @@ def deps do
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/lamina_dashboard](https://hexdocs.pm/lamina_dashboard).
Documentation for the latest release can be found on [HexDocs](https://hexdocs.pm/lamina) and for the `main` branch [here](https://jimsy.gitlab.io/lamina_dashboard/api-reference.html).
## Integration with LiveDashboard
You can add this page to your Phoenix LiveDashboard by adding it as a page in the `live_dashboard` macro in your router file:
```elixir
live_dashboard "/dashboard",
additional_pages: [lamina: Lamina.Dashboard]
```
Once configured, you will be able to access `Lamina.Dashboard` at `/dashboard/lamina`.
## Distribution
You can use `Lamina.Dashboard` to view the configuration on remote nodes by simply adding the `lamina_dashboard` package as a dependency of your remote nodes.

View file

@ -1,18 +1,117 @@
defmodule Lamina.Dashboard do
use Phoenix.LiveDashboard.PageBuilder
alias Lamina.Dashboard.Remote
alias Lamina.Registry.ServerRegistry
alias Phoenix.LiveView.Socket
@moduledoc """
Documentation for `Lamina.Dashboard`.
A LiveDashboard.PageBuilder which shows information about currently running
Lamina configuration servers.
## Usage:
Add the following to your LiveDashboard entry in your Phoenix router:
```elixir
live_dashboard "/dashboard",
additional_pages: [lamina: Lamina.Dashboard]
```
"""
@doc """
Hello world.
@doc false
@impl true
@spec mount(map, any, Socket.t()) :: {:ok, Socket.t()}
def mount(params, _session, socket) do
node = socket.assigns.page.node
servers = Remote.list_servers(node)
socket = assign(socket, servers: servers)
nav = params["nav"]
server = find_server(nav)
first_server = hd(servers)
## Examples
cond do
is_nil(server) && first_server ->
to = live_dashboard_path(socket, socket.assigns.page, nav: server_id(first_server))
{:ok, push_redirect(socket, to: to)}
iex> Lamina.Dashboard.hello()
:world
server ->
config_keys = Remote.list_config_keys(node, server)
providers = Remote.list_providers(node, server)
"""
def hello do
:world
{:ok, assign(socket, providers: providers, config_keys: config_keys, server: server)}
true ->
{:ok, socket}
end
end
@doc false
@impl true
def menu_link(_, _), do: {:ok, "Lamina"}
@doc false
@impl true
def render_page(%{servers: []} = assigns) do
fn ->
~H"""
No Lamina servers running on #{@page.node}.
"""
end
end
def render_page(assigns) do
items =
for server <- assigns.servers do
{server_id(server),
name: inspect(server), render: render_server(assigns), method: :redirect}
end
nav_bar(items: items)
end
defp render_server(assigns) do
fn ->
table(
columns: table_columns(),
id: assigns.server,
row_attrs: &row_attrs/1,
row_fetcher: &fetch_table(assigns.server, &1, &2),
title: "configuration settings"
)
end
end
defp server_id(server) when is_atom(server) do
server
|> Atom.to_string()
|> Base.encode64(padding: false)
end
defp find_server(server_id) do
ServerRegistry.all_servers()
|> Enum.find(&(server_id(&1) == server_id))
end
defp table_columns,
do: [
%{
field: :name,
header: "Config key",
header_attrs: [class: "p1-4"],
cell_attrs: [class: "tabular-column-name p1-4"],
sortable: :asc
},
%{
field: :value,
format: &format_value/1
}
]
defp format_value(value), do: inspect(value)
defp row_attrs(_table), do: []
defp fetch_table(server, params, node) do
Remote.get_configs(node, server, params)
end
end

View file

@ -0,0 +1,99 @@
defmodule Lamina.Dashboard.Remote do
alias Lamina.Registry.ServerRegistry
alias Lamina.Server
@moduledoc """
Uses the Erlang `:rpc` module to interact with potentially-remote Lamina
servers.
"""
@doc """
List the running configuration servers on the remote node.
"""
@spec list_servers(node) :: [module]
def list_servers(node) do
:rpc.call(node, __MODULE__, :do_list_servers, [])
end
@doc """
List the configuration keys for a specific server on the remote node.
"""
@spec list_config_keys(node, module) :: [atom]
def list_config_keys(node, server) do
:rpc.call(node, __MODULE__, :do_list_config_keys, [server])
end
@doc """
List the configured providers for server on node.
"""
@spec list_providers(node, module) :: [{module, keyword}]
def list_providers(node, server) do
:rpc.call(node, __MODULE__, :do_list_providers, [server])
end
@doc """
Retrieve all configuration values from a remote server.
"""
@spec get_configs(node, module, map) :: [map]
def get_configs(node, server, params) do
:rpc.call(node, __MODULE__, :do_get_configs, [server, params])
end
@doc false
@spec do_list_servers :: [module]
def do_list_servers do
ServerRegistry.all_servers()
|> Enum.sort()
end
@doc false
@spec do_list_config_keys(module) :: [atom]
def do_list_config_keys(server) do
apply(server, :__lamina__, [:config_keys])
end
@doc false
@spec do_list_providers(module) :: [{module, keyword}]
def do_list_providers(server) do
apply(server, :__lamina__, [:providers])
end
@doc false
@spec do_get_configs(module, map) :: {[map], non_neg_integer()}
def do_get_configs(server, params) do
sort_dir = if params.sort_dir == :asc, do: &<=/2, else: &>=/2
all_rows =
server
|> apply(:__lamina__, [:config_keys])
|> Enum.map(fn config_key ->
%{
name: config_key,
value: Server.get!(server, config_key)
}
end)
rows =
all_rows
|> maybe_filter_rows(params.search)
|> Enum.sort_by(&Map.get(&1, params.sort_by), sort_dir)
|> Enum.take(params.limit)
{rows, length(all_rows)}
end
defp maybe_filter_rows(rows, nil), do: rows
defp maybe_filter_rows(rows, search_term) do
Stream.filter(rows, fn row ->
row
|> Map.values()
|> Enum.any?(fn value ->
value
|> inspect()
|> String.downcase()
|> String.contains?(search_term)
end)
end)
end
end

View file

@ -39,7 +39,11 @@ defmodule Lamina.Dashboard.MixProject do
[
{:credo, "~> 1.5", only: ~w[dev test]a},
{:ex_doc, ">= 0.0.0", only: ~w[dev test]a},
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false}
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false},
{:jason, "~> 1.0", only: ~w[dev test]a},
{:lamina, "~> 0.4"},
{:phoenix_live_dashboard, "~> 0.5.1", optional: true},
{:phoenix_live_reload, "~> 1.2", only: :dev}
]
end

View file

@ -7,8 +7,22 @@
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"git_ops": {:hex, :git_ops, "2.4.5", "185a724dfde3745edd22f7571d59c47a835cf54ded67e9ccbc951920b7eec4c2", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e323a5b01ad53bc8c19c3a444be3e61ed7803ecd2e95530446ae9327d0143ecc"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"lamina": {:hex, :lamina, "0.4.0", "d9f984a53e64cfb1c6fcf2409fc7cd3347e12cca7887735da312651858459598", [:mix], [{:recase, "~> 0.7", [hex: :recase, repo: "hexpm", optional: false]}], "hexpm", "99add10816fa58ea5f0f7e3a24341b1f2037f9b2fd199f950f47027db0644596"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"mime": {:hex, :mime, "2.0.1", "0de4c81303fe07806ebc2494d5321ce8fb4df106e34dd5f9d787b637ebadc256", [:mix], [], "hexpm", "7a86b920d2aedce5fb6280ac8261ac1a739ae6c1a1ad38f5eadf910063008942"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"phoenix": {:hex, :phoenix, "1.6.0", "7b85023f7ddef9a5c70909a51cc37c8b868b474d853f90f4280efd26b0e7cce5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "52ffdd31f2daeb399b2e1eb57d468f99a1ad6eee5d8ea19d2353492f06c9fc96"},
"phoenix_html": {:hex, :phoenix_html, "3.0.4", "232d41884fe6a9c42d09f48397c175cd6f0d443aaa34c7424da47604201df2e1", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ce17fd3cf815b2ed874114073e743507704b1f5288bb03c304a77458485efc8b"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.5.2", "b3b863ba9da3c9bd0b18fc32e96e8e5e25faf6a5f62db1dd91029835ea4cc90f", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.16.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "6d7124f36ee6c74be334386b8b5a1eb27223c77f86f4167de132b9358036f199"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.16.4", "5692edd0bac247a9a816eee7394e32e7a764959c7d0cf9190662fc8b0cd24c97", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "754ba49aa2e8601afd4f151492c93eb72df69b0b9856bab17711b8397e43bba0"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"},
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"},
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
}