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

Per-context dashboards #287

Merged
merged 7 commits into from
Sep 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions .github/FUNDING.yml

This file was deleted.

3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Crash when updating records.

### Added
- Support for filtering `{:array, :string}` fielsd [PR#285](https://github.com/aesmail/kaffy/pull/262)
- Support for filtering `{:array, :string}` fields [PR#285](https://github.com/aesmail/kaffy/pull/262)
- Per-context dashboard [PR#287](https://github.com/aesmail/kaffy/pull/287)


## v0.10.0-rc.0 (2023-09-02)
Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,18 @@ plug Plug.Static,

# in your config/config.exs
config :kaffy,
otp_app: :my_app,
ecto_repo: MyApp.Repo,
router: MyAppWeb.Router
# required keys
otp_app: :my_app, # required
ecto_repo: MyApp.Repo, # required
router: MyAppWeb.Router, # required
# optional keys
admin_title: "My Awesome App",
admin_logo: "/images/logo.png",
admin_logo_mini: "/images/logo-mini.png",
hide_dashboard: true,
home_page: [schema: [:accounts, :user]],
enable_context_dashboards: true, # since v0.10.0
admin_footer: "Kaffy © 2023" # since v0.10.0
```

Note that providing pipelines with the `:pipe_through` option will add those pipelines to kaffy's `:kaffy_browser` pipeline which is defined as follows:
Expand Down Expand Up @@ -151,7 +160,9 @@ config :kaffy,
admin_title: "My Awesome App",
admin_logo: "/images/logo.png",
admin_logo_mini: "/images/logo-mini.png",
admin_footer: "Kaffy © 2023",
hide_dashboard: false,
enable_context_dashboards: true,
home_page: [kaffy: :dashboard],
ecto_repo: MyApp.Repo,
router: MyAppWeb.Router,
Expand Down
10 changes: 8 additions & 2 deletions lib/kaffy/resource_admin.ex
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,14 @@ defmodule Kaffy.ResourceAdmin do
)
end

def collect_widgets(conn) do
Enum.reduce(Kaffy.Utils.contexts(conn), [], fn c, all ->
def collect_widgets(conn, context \\ :kaffy_dashboard) do
main_dashboard? = context == :kaffy_dashboard
show_context_dashboard? = Kaffy.Utils.show_context_dashboards?()

conn
|> Kaffy.Utils.contexts()
|> Enum.filter(fn c -> main_dashboard? or (show_context_dashboard? and c == context) end)
|> Enum.reduce([], fn c, all ->
widgets =
Enum.reduce(Kaffy.Utils.schemas_for_context(conn, c), [], fn {_, resource}, all ->
all ++ Kaffy.ResourceAdmin.widgets(resource, conn)
Expand Down
5 changes: 5 additions & 0 deletions lib/kaffy/routes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ defmodule Kaffy.Routes do
get("/dashboard", HomeController, :dashboard, as: :kaffy_dashboard)
get("/tasks", TaskController, :index, as: :kaffy_task)
get("/p/:slug", PageController, :index, as: :kaffy_page)

if Kaffy.Utils.show_context_dashboards?() do
get("/:context", ResourceController, :dashboard, as: :kaffy_context_dashboard)
end

get("/:context/:resource", ResourceController, :index, as: :kaffy_resource)
post("/:context/:resource", ResourceController, :create, as: :kaffy_resource)

Expand Down
13 changes: 13 additions & 0 deletions lib/kaffy/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,20 @@ defmodule Kaffy.Utils do
true
"""
@spec has_function?(module(), atom()) :: boolean()
def has_function?(nil, _), do: false

def has_function?(admin, func) do
functions = admin.__info__(:functions)
Keyword.has_key?(functions, func)
end

def context_admins_include_function?(conn, context, func) do
schemas_for_context(conn, context)
|> Enum.filter(fn {_, options} -> Keyword.has_key?(options, :admin) end)
|> Enum.map(fn {_, options} -> has_function?(Keyword.get(options, :admin), func) end)
|> Enum.any?()
end

@doc """
Returns true if `thing` is a module, false otherwise.
"""
Expand Down Expand Up @@ -390,6 +399,10 @@ defmodule Kaffy.Utils do
)
end

def show_context_dashboards?() do
env(:enable_context_dashboards, true)
end

defp env(key, default \\ nil) do
Application.get_env(:kaffy, key, default)
end
Expand Down
5 changes: 4 additions & 1 deletion lib/kaffy_web/controllers/home_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ defmodule KaffyWeb.HomeController do
end

def dashboard(conn, _params) do
render(conn, "index.html", layout: {KaffyWeb.LayoutView, "app.html"})
render(conn, "index.html",
layout: {KaffyWeb.LayoutView, "app.html"},
context: :kaffy_dashboard
)
end
end
7 changes: 7 additions & 0 deletions lib/kaffy_web/controllers/resource_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ defmodule KaffyWeb.ResourceController do
use Phoenix.HTML
alias Kaffy.Pagination

def dashboard(conn, %{"context" => context}) do
render(conn, "dashboard.html",
layout: {KaffyWeb.LayoutView, "app.html"},
context: String.to_existing_atom(context)
)
end

def index(
conn,
%{
Expand Down
6 changes: 4 additions & 2 deletions lib/kaffy_web/templates/home/_progress.html.eex
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<div class="col-md-<%= @widget.width %> grid-margin stretch-card">
<% color = Map.get(@widget, :color, "danger") %>
<% width = Map.get(@widget, :width, 6) %>
<div class="col-md-<%= width %> grid-margin stretch-card">
<div class="card shadow">
<div class="card-header">
<h4><%= @widget.title %></h4>
</div>
<div class="card-body">
<p class="font-weight-bold text-black"><%= @widget.content %> <span class="float-right"><%= @widget.percentage %>%</span></p>
<div class="progress">
<div class="progress-bar bg-danger" role="progressbar" style="width: <%= @widget.percentage %>%" aria-valuenow="<%= @widget.percentage %>" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-<%= color %>" role="progressbar" style="width: <%= @widget.percentage %>%" aria-valuenow="<%= @widget.percentage %>" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion lib/kaffy_web/templates/home/_text.html.eex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<div class="col-md-<%= @widget.width %> grid-margin stretch-card">
<% width = Map.get(@widget, :width, 6) %>
<div class="col-md-<%= width %> grid-margin stretch-card">
<div class="card shadow">
<div class="card-header py-3">
<h4><%= @widget.title %></h4>
Expand Down
9 changes: 6 additions & 3 deletions lib/kaffy_web/templates/home/_tidbit.html.eex
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<div class="col-md-<%= @widget.width %> grid-margin stretch-card">
<div class="card shadow border-left-success">
<% color = Map.get(@widget, :color, "success") %>
<% width = Map.get(@widget, :width, 3) %>

<div class="col-md-<%= width %> grid-margin stretch-card">
<div class="card shadow border-left-<%= color %>">
<div class="card-body">
<p class="font-weight-normal mb-1 text-success"><strong><%= @widget.title %></strong> <i class="<%= if @widget[:full_icon] do %><%= @widget.full_icon %><% else %>fas fa-<%= @widget.icon %><% end %> float-right"></i></p>
<p class="font-weight-normal mb-1 text-<%= color %>"><strong><%= @widget.title %></strong> <i class="<%= if @widget[:full_icon] do %><%= @widget.full_icon %><% else %>fas fa-<%= @widget.icon %><% end %> float-right"></i></p>
<h2 class="mt-1"><%= @widget.content %></h2>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions lib/kaffy_web/templates/home/index.html.eex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="page-header">
<h3 class="page-title">Dashboard </h3>
<h3 class="page-title">Main Dashboard</h3>
<nav aria-label="breadcrumb">
<ul class="breadcrumb">
<li class="breadcrumb-item active" aria-current="page">
Expand All @@ -11,7 +11,7 @@
</nav>
</div>

<% widgets = Kaffy.ResourceAdmin.collect_widgets(@conn) %>
<% widgets = Kaffy.ResourceAdmin.collect_widgets(@conn, @context) %>

<%= if Enum.empty?(widgets) do %>

Expand Down
9 changes: 6 additions & 3 deletions lib/kaffy_web/templates/layout/app.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,18 @@
<% end %>

<%= for context <- Kaffy.Utils.contexts(@conn) do %>
<li class="nav-item<%= if @conn.assigns[:context] == to_string(context) do %> active<% end %>">
<a class="nav-link <%= if @conn.assigns[:context] != to_string(context) do %> collapsed<% end %>"
<li class="nav-item<%= if @conn.path_params["context"] == to_string(context) do %> active<% end %>">
<a class="nav-link <%= if @conn.path_params["context"] != to_string(context) do %> collapsed<% end %>"
href="#<%= context %>-context" data-toggle="collapse" aria-expanded="false" aria-controls="<%= context %>-context">
<span class="menu-title"><%= Kaffy.Utils.context_name(@conn, context) %></span>
<i class="menu-arrow"></i>
<i class="fas fa-bars menu-icon"></i>
</a>
<div class="collapse<%= if @conn.assigns[:context] == to_string(context) do %> show<% end %>" id="<%= context %>-context">
<div class="collapse<%= if @conn.path_params["context"] == to_string(context) do %> show<% end %>" id="<%= context %>-context">
<ul class="nav flex-column sub-menu">
<%= if Kaffy.Utils.show_context_dashboards?() and Kaffy.Utils.context_admins_include_function?(@conn, context, :widgets) do %>
<li class="nav-item"><%= link "Dashboard", to: Kaffy.Utils.router().kaffy_context_dashboard_path(@conn, :dashboard, context), class: "nav-link" %></li>
<% end %>
<%= for {resource, options} <- Kaffy.Utils.schemas_for_context(@conn, context) do %>
<%= if Kaffy.ResourceAdmin.authorized?(options, @conn) && Kaffy.Utils.visible?(options) do %>
<li class="nav-item"><%= link Kaffy.ResourceAdmin.plural_name(options), to: Kaffy.Utils.router().kaffy_resource_path(@conn, :index, context, resource), class: "nav-link" %></li>
Expand Down
47 changes: 47 additions & 0 deletions lib/kaffy_web/templates/resource/dashboard.html.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<div class="page-header">
<h3 class="page-title"><%= String.capitalize(to_string(@context)) %> Dashboard</h3>
<nav aria-label="breadcrumb">
<ul class="breadcrumb">
<li class="breadcrumb-item active" aria-current="page">
<span>
Overview <i class="fas fa-feather icon-sm text-primary align-middle"></i>
</span>
</li>
</ul>
</nav>
</div>

<% widgets = Kaffy.ResourceAdmin.collect_widgets(@conn, @context) %>

<%= if Enum.empty?(widgets) do %>

<div class="row mt-3">
<div class="col-md-12 text-center">
<h4>A powerfully simple admin package for phoenix applications.</h4>
<h4>You can add widgets to this page by defining <code>widgets/2</code> in your admin modules.</h4>
</div>
</div>

<% else %>

<div class="row mt-1 row-cols-1 row-cols-md-2 row-cols-sm-1 row-cols-xs-1">
<%= for widget <- widgets do %>
<%= if widget.type == "text" do %>
<%= render KaffyWeb.HomeView, "_text.html", widget: widget %>
<% end %>

<%= if widget.type == "chart" do %>
<%= render KaffyWeb.HomeView, "_chart.html", widget: widget %>
<% end %>

<%= if widget.type == "progress" do %>
<%= render KaffyWeb.HomeView, "_progress.html", widget: widget %>
<% end %>

<%= if widget.type == "tidbit" do %>
<%= render KaffyWeb.HomeView, "_tidbit.html", widget: widget %>
<% end %>
<% end %>
</div>

<% end %>