diff --git a/.formatter.exs b/.formatter.exs
index ecde7b6..73d29a7 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -1,5 +1,6 @@
# Used by "mix format"
[
- inputs: ["{mix,.formatter,dev,dev.*}.exs", "{config,lib,test}/**/*.{ex,exs}"],
- import_deps: [:ecto, :ecto_sql, :plug, :phoenix]
+ import_deps: [:ecto, :ecto_sql, :plug, :phoenix],
+ inputs: ["{mix,.formatter,dev,dev.*}.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
+ plugins: [Phoenix.LiveView.HTMLFormatter]
]
diff --git a/.gitignore b/.gitignore
index 28dc7b0..21d1524 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,9 @@ error_tracker-*.tar
# Temporary files, for example, from tests.
/tmp/
+/assets/node_modules
+
+/priv/static/app.js
+/priv/static/app.css
+
dev.local.exs
diff --git a/README.md b/README.md
index 3024954..1430ec5 100644
--- a/README.md
+++ b/README.md
@@ -24,3 +24,42 @@ defmodule MyApp.Endpoint do
use ErrorTracker.Integrations.Plug
end
```
+
+## Development
+
+### Development server
+
+We have a `dev.exs` script that starts a development server.
+
+To run it together with an `IEx` console you can do:
+
+```
+iex -S mix dev
+```
+
+### Assets
+
+In ortder to participate in the development of this library, you may need to
+know how to compile the assets needed to use the Web UI.
+
+To do so, you need to first make a clean build:
+
+```
+mix do assets.install, assets.build
+```
+
+That task will build the JS and CSS of the project.
+
+The JS is not expected to change too much because we rely in LiveView, but if
+you make any change just execute that command again and you are good to go.
+
+In the case of CSS, as it is automatically generated by Tailwind, you need to
+start the watcher when your intention is to modify the classes used.
+
+To do so you can execute this task in a separate terminal:
+
+```
+mix assets.watch
+```
+
+
diff --git a/assets/bun.lockb b/assets/bun.lockb
new file mode 100755
index 0000000..b0a82ae
Binary files /dev/null and b/assets/bun.lockb differ
diff --git a/assets/css/app.css b/assets/css/app.css
new file mode 100644
index 0000000..71a77f0
--- /dev/null
+++ b/assets/css/app.css
@@ -0,0 +1,4 @@
+@import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities";
+
diff --git a/assets/js/app.js b/assets/js/app.js
new file mode 100644
index 0000000..5faf26a
--- /dev/null
+++ b/assets/js/app.js
@@ -0,0 +1,20 @@
+// Establish Phoenix Socket and LiveView configuration.
+import { Socket, LongPoll } from "phoenix";
+import { LiveSocket } from "phoenix_live_view";
+import topbar from "topbar";
+
+let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
+let socketPath = document.querySelector("meta[name='socket-path']").getAttribute("content");
+let socketTransport = document.querySelector("meta[name='socket-transport']").getAttribute("content");
+let normalizedTransport = (socketTransport == "longpoll") ? LongPoll : WebSocket;
+
+let liveSocket = new LiveSocket(socketPath, Socket, { transport: normalizedTransport, params: { _csrf_token: csrfToken }});
+
+// Show progress bar on live navigation and form submits
+topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
+window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300));
+window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide());
+
+// connect if there are any LiveViews on the page
+liveSocket.connect();
+window.liveSocket = liveSocket;
diff --git a/assets/package.json b/assets/package.json
new file mode 100644
index 0000000..31b8464
--- /dev/null
+++ b/assets/package.json
@@ -0,0 +1,10 @@
+{
+ "workspaces": [
+ "../deps/*"
+ ],
+ "dependencies": {
+ "phoenix": "workspace:*",
+ "phoenix_live_view": "workspace:*",
+ "topbar": "^3.0.0"
+ }
+}
diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js
new file mode 100644
index 0000000..1447c42
--- /dev/null
+++ b/assets/tailwind.config.js
@@ -0,0 +1,22 @@
+// See the Tailwind configuration guide for advanced usage
+// https://tailwindcss.com/docs/configuration
+
+let plugin = require('tailwindcss/plugin')
+
+module.exports = {
+ content: [
+ './js/**/*.js',
+ '../lib/error_tracker/web.ex',
+ '../lib/error_tracker/web/**/*.*ex'
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [
+ require('@tailwindcss/forms'),
+ plugin(({addVariant}) => addVariant('phx-no-feedback', ['&.phx-no-feedback', '.phx-no-feedback &'])),
+ plugin(({addVariant}) => addVariant('phx-click-loading', ['&.phx-click-loading', '.phx-click-loading &'])),
+ plugin(({addVariant}) => addVariant('phx-submit-loading', ['&.phx-submit-loading', '.phx-submit-loading &'])),
+ plugin(({addVariant}) => addVariant('phx-change-loading', ['&.phx-change-loading', '.phx-change-loading &']))
+ ]
+}
diff --git a/config/config.exs b/config/config.exs
index 0845f95..3c99d05 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -7,8 +7,16 @@ if config_env() == :dev do
args: ~w(
--config=tailwind.config.js
--input=css/app.css
- --output=../priv/static/assets/app.css
+ --output=../priv/static/app.css
),
cd: Path.expand("../assets", __DIR__)
]
+
+ config :bun,
+ version: "1.1.18",
+ default: [
+ args: ~w(build app.js --outdir=../../priv/static),
+ cd: Path.expand("../assets/js", __DIR__),
+ env: %{}
+ ]
end
diff --git a/dev.exs b/dev.exs
index b02d8e6..55ea57c 100644
--- a/dev.exs
+++ b/dev.exs
@@ -94,6 +94,7 @@ end
defmodule ErrorTrackerDevWeb.Router do
use Phoenix.Router
+ use ErrorTracker.Web, :router
pipeline :browser do
plug :fetch_session
@@ -106,6 +107,8 @@ defmodule ErrorTrackerDevWeb.Router do
get "/noroute", ErrorTrackerDevWeb.PageController, :noroute
get "/exception", ErrorTrackerDevWeb.PageController, :exception
get "/exit", ErrorTrackerDevWeb.PageController, :exit
+
+ error_tracker_dashboard("/errors")
end
end
diff --git a/lib/error_tracker/web.ex b/lib/error_tracker/web.ex
new file mode 100644
index 0000000..f866fb5
--- /dev/null
+++ b/lib/error_tracker/web.ex
@@ -0,0 +1,83 @@
+defmodule ErrorTracker.Web do
+ @moduledoc """
+ ErrorTracker includes a Web UI to view and inspect errors occurred on your
+ application and already stored on the database.
+
+ In order to use it, you need to add the following to your Phoenix's `
+ router.ex` file:
+
+ ```elixir
+ defmodule YourAppWeb.Router do
+ use Phoenix.Router
+ use ErrorTracker.Web, :router
+
+ ...
+
+ error_tracker_dashboard "/errors"
+ end
+ ```
+
+ This will add the routes needed for the ErrorTracker LiveView UI to work.
+
+ ## LiveView socket options
+
+ By default the library expects you to have your LiveView socket at `/live` and
+ using `websocket` transport.
+
+ If that's not the case, you can configure it adding the following
+ configuration to your app's config files:
+
+ ```elixir
+ config :error_tracker,
+ socket: [
+ path: "/my-custom-socket-path"
+ transport: :longpoll # (accepted values are :longpoll or :websocket)
+ ]
+ ```
+ """
+
+ def html do
+ quote do
+ import Phoenix.Controller, only: [get_csrf_token: 0]
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_view do
+ quote do
+ use Phoenix.LiveView, layout: {ErrorTracker.Web.Layouts, :live}
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def router do
+ quote do
+ import ErrorTracker.Web.Router
+ end
+ end
+
+ defp html_helpers do
+ quote do
+ use Phoenix.Component
+
+ import Phoenix.HTML
+ import Phoenix.LiveView.Helpers
+
+ alias Phoenix.LiveView.JS
+ end
+ end
+
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/lib/error_tracker/web/components/layouts.ex b/lib/error_tracker/web/components/layouts.ex
new file mode 100644
index 0000000..33ecade
--- /dev/null
+++ b/lib/error_tracker/web/components/layouts.ex
@@ -0,0 +1,19 @@
+defmodule ErrorTracker.Web.Layouts do
+ use ErrorTracker.Web, :html
+
+ @css_path :code.priv_dir(:error_tracker) |> Path.join("static/app.css")
+ @js_path :code.priv_dir(:error_tracker) |> Path.join("static/app.js")
+
+ @default_docket_config %{path: "/live", transport: :websocket}
+
+ embed_templates "layouts/*"
+
+ def get_content(:css), do: File.read!(@css_path)
+ def get_content(:js), do: File.read!(@js_path)
+
+ def get_socket_config(key) do
+ default = Map.get(@default_docket_config, key)
+ config = Application.get_env(:error_tracker, :live_view_socket, [])
+ Keyword.get(config, key, default)
+ end
+end
diff --git a/lib/error_tracker/web/components/layouts/live.html.heex b/lib/error_tracker/web/components/layouts/live.html.heex
new file mode 100644
index 0000000..c753bc6
--- /dev/null
+++ b/lib/error_tracker/web/components/layouts/live.html.heex
@@ -0,0 +1,3 @@
+
Hello world!
+Number of presses: <%= @counter %>
+ diff --git a/lib/error_tracker/web/router.ex b/lib/error_tracker/web/router.ex new file mode 100644 index 0000000..52136df --- /dev/null +++ b/lib/error_tracker/web/router.ex @@ -0,0 +1,35 @@ +defmodule ErrorTracker.Web.Router do + @moduledoc false + + @doc """ + Creates the routes needed to use the `ErrorTracker` web interface. + + It requires a path in which you are going to serve the web interface. + """ + defmacro error_tracker_dashboard(path, opts \\ []) do + {session_name, session_opts} = parse_options(opts) + + quote do + scope unquote(path), alias: false, as: false do + import Phoenix.LiveView.Router, only: [live: 4, live_session: 3] + + live_session unquote(session_name), unquote(session_opts) do + live "/", ErrorTracker.Web.Live.Dashboard, :index, as: unquote(session_name) + end + end + end + end + + @doc false + def parse_options(opts) do + on_mount = Keyword.get(opts, :on_mount, []) + session_name = Keyword.get(opts, :as, :error_tracker_dashboard) + + session_opts = [ + on_mount: on_mount, + root_layout: {ErrorTracker.Web.Layouts, :root} + ] + + {session_name, session_opts} + end +end diff --git a/mix.exs b/mix.exs index 00ee6a7..38a3fe7 100644 --- a/mix.exs +++ b/mix.exs @@ -50,6 +50,7 @@ defmodule ErrorTracker.MixProject do {:plug, "~> 1.10"}, {:postgrex, ">= 0.0.0"}, # Dev dependencies + {:bun, "~> 1.3", only: :dev}, {:credo, "~> 1.7", only: [:dev, :test]}, {:ex_doc, "~> 0.33", only: :dev}, {:phoenix_live_reload, ">= 0.0.0", only: :dev}, @@ -62,7 +63,9 @@ defmodule ErrorTracker.MixProject do [ setup: ["deps.get", "cmd --cd assets npm install"], dev: "run --no-halt dev.exs", - "assets.build": ["esbuild default --minify"] + "assets.install": ["bun.install", "cmd _build/bun install --cwd assets/"], + "assets.watch": ["tailwind default --watch"], + "assets.build": ["bun default --minify", "tailwind default --minify"] ] end end diff --git a/mix.lock b/mix.lock index b24680c..0f48b67 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "bun": {:hex, :bun, "1.3.0", "6833722da5b073777e043aec42091b0cf8bbacb84262ec6d348a914dda4c6a98", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "dde1b8116ba57269a9f398b4b28492b16fb29a78800c9533b7c9fb036793d62a"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},