defmodule Mix.Tasks.PromEx.Dashboard.Export do @moduledoc """ This will render a PromEx dashboard either to STDOUT or to a file depending on the CLI arguments that are provided. The following CLI flags are supported: ```md -d, --dashboard The name of the dashboard that you would like to export from PromEx. For example, if you would like to export the Ecto dashboard, provide the value `ecto.json`. -m, --module The PromEx module which will be used to render the dashboards. This is needed to fetch any relevant assigns from the `c:PromEx.dashboard_assigns/0` callback -s, --std_out A boolean flag denoting that the rendered dashboard should be output to STDOUT. -f, --file_path If you would like the write the generated JSON dashboard definition to a file, you can provide a relative file path in the project's `priv` directory. -a, --assign Any additional assigns you would like to pass to the dashboard for rendering. You are able to pass multiple assigns by passing multiple --assign arguments. For example: `--assign some=thing --assign another=thing`. ``` """ @shortdoc "Export a rendered dashboard to STDOUT or a file" use Mix.Task alias PromEx.DashboardRenderer @impl true def run(args) do # Compile the project Mix.Task.run("compile") # Get CLI args and set up uploader cli_args = parse_options(args) prom_ex_module = "Elixir.#{cli_args.module}" |> String.to_atom() |> Code.ensure_compiled() |> case do {:module, module} -> module {:error, reason} -> raise "#{cli_args.module} is not a valid PromEx module because #{inspect(reason)}" end check_valid_dashboard(cli_args) render_dashboard(prom_ex_module, cli_args) end defp parse_options(args) do cli_options = [module: :string, stdout: :boolean, file_path: :string, dashboard: :string, assign: [:string, :keep]] cli_aliases = [m: :module, s: :stdout, f: :file_path, d: :dashboard, a: :assign] # Parse out the arguments and put defaults where necessary args |> OptionParser.parse(aliases: cli_aliases, strict: cli_options) |> case do {options, _remaining_args, [] = _errors} -> options |> Enum.reduce(%{}, fn {:assign, assign_value}, acc when is_map_key(acc, :assigns) -> [key, value] = String.split(assign_value, "=", parts: 2) new_assign = {String.to_existing_atom(key), value} Map.put(acc, :assigns, [new_assign | acc.assigns]) {:assign, assign_value}, acc -> [key, value] = String.split(assign_value, "=", parts: 2) Map.put(acc, :assigns, [{String.to_existing_atom(key), value}]) {opt, value}, acc -> Map.put(acc, opt, value) end) |> Map.put_new(:assigns, []) {_options, _remaining_args, errors} -> raise "Invalid CLI args were provided: #{inspect(errors)}" end |> Map.put_new(:stdout, false) |> Map.put_new(:dashboard, nil) |> Map.put_new_lazy(:module, fn -> Mix.Project.config() |> Keyword.get(:app) |> Atom.to_string() |> Macro.camelize() |> Kernel.<>(".PromEx") end) end defp check_valid_dashboard(%{dashboard: nil}) do raise "You must provide a --dashboard argument" end defp check_valid_dashboard(_args) do :ok end defp render_dashboard(prom_ex_module, cli_args) do user_provided_assigns = prom_ex_module.dashboard_assigns() default_title = prom_ex_module.__otp_app__() |> Atom.to_string() |> Macro.camelize() default_dashboard_name = cli_args.dashboard |> Path.basename() |> normalize_file_name() |> Macro.camelize() default_dashboard_assigns = [ otp_app: prom_ex_module.__otp_app__(), title: "#{default_title} - PromEx #{default_dashboard_name} Dashboard" ] dashboard_render = :prom_ex |> DashboardRenderer.build(cli_args.dashboard) |> DashboardRenderer.merge_assigns(default_dashboard_assigns) |> DashboardRenderer.merge_assigns(user_provided_assigns) |> DashboardRenderer.merge_assigns(cli_args.assigns) |> DashboardRenderer.render_dashboard(prom_ex_module) |> DashboardRenderer.decode_dashboard() |> check_dashboard_render() handle_export(cli_args, prom_ex_module, dashboard_render) end defp handle_export(%{stdout: true}, _prom_ex_module, dashboard_render) do IO.puts(dashboard_render.rendered_file) end defp handle_export(%{file_path: file_path}, prom_ex_module, dashboard_render) do priv_path = prom_ex_module.__otp_app__() |> :code.priv_dir() |> :erlang.list_to_binary() full_path = Path.join([priv_path, file_path]) File.write!(full_path, dashboard_render.rendered_file) end defp handle_export(_cli_args, _prom_ex_module, _dashboard_render) do raise "You must specify either a file path to write the dashboard to, or provide the --stdout flag to print to STDOUT" end defp check_dashboard_render(%DashboardRenderer{valid_json?: false}) do raise "The rendered dashboard yielded an invalid JSON data structure. Be sure to check your assigns." end defp check_dashboard_render(%DashboardRenderer{valid_file?: false}) do raise "The dashboard that you selected does not exist in PromEx. Be sure that you typed it correctly." end defp check_dashboard_render(dashboard_render) do dashboard_render end defp normalize_file_name(path) do if Path.extname(path) == "" do path else path |> Path.rootname() |> normalize_file_name() end end end