Packages

phoenix_kit

1.7.209
1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web live activity index.html.heex
Raw

lib/phoenix_kit_web/live/activity/index.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
socket={@socket}
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={gettext("Activity")}
page_subtitle={gettext("Business-level activity across the platform")}
current_path={@url_path}
project_title={@project_title}
current_locale={assigns[:current_locale]}
>
<div class="container flex flex-col mx-auto px-4 py-6">
<%!-- Activity List --%>
<.table_default
id="activity-table"
variant="zebra"
size="sm"
class="w-full table-fixed md:table-auto"
toggleable={true}
show_toggle={false}
view_mode={@view_mode}
view_event="set_view_mode"
items={@entries}
card_title={fn entry -> entry.action end}
card_fields={
fn entry ->
fields = [
%{
label: gettext("Actor"),
value: if(entry.actor, do: entry.actor.email, else: gettext("System"))
}
]
fields =
if entry.resource_type do
fields ++ [%{label: gettext("Subject"), value: entry.resource_type}]
else
fields
end
fields =
if entry.target do
fields ++ [%{label: gettext("Target"), value: entry.target.email}]
else
fields
end
fields =
if entry.metadata["role"] do
fields ++ [%{label: gettext("Role"), value: entry.metadata["role"]}]
else
fields
end
fields ++
[
%{
label: gettext("Date"),
value:
"#{UtilsDate.format_date_with_user_format(entry.inserted_at)} #{UtilsDate.format_time_with_user_format(entry.inserted_at)}"
}
]
end
}
>
<:toolbar_title>
<%!-- Filters as compact media-style dropdowns: each shows an icon, with
the active value's label (revealed on mobile only when set) so the
whole toolbar holds one row. Options are patch links that preserve
the other filters via filter_path/2. --%>
<div class="flex flex-wrap items-center gap-2">
<%!-- Display (grid/list) — media-style dropdown --%>
<% display_options = [
{"card", gettext("Grid"), "hero-squares-2x2"},
{"table", gettext("List"), "hero-bars-3-bottom-left"}
] %>
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-sm gap-2">
<.icon
name={
if @view_mode == "table",
do: "hero-bars-3-bottom-left",
else: "hero-squares-2x2"
}
class="w-4 h-4"
/>
<span class="hidden sm:inline">
{if @view_mode == "table", do: gettext("List"), else: gettext("Grid")}
</span>
<.icon
name="hero-chevron-down"
class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
/>
</div>
<ul
tabindex="0"
onclick="document.activeElement.blur()"
class="dropdown-content menu bg-base-100 rounded-box z-20 w-44 p-2 shadow border border-base-200"
>
<li :for={{val, label, icon} <- display_options}>
<button
phx-click="set_view_mode"
phx-value-mode={val}
class={@view_mode == val && "active"}
>
<.icon name={icon} class="w-4 h-4" /> {label}
</button>
</li>
</ul>
</div>
<%!-- Module --%>
<div class="dropdown">
<div
tabindex="0"
role="button"
class={["btn btn-sm gap-2", @filter_module && "btn-active"]}
>
<.icon name="hero-cube" class="w-4 h-4" />
<span class={["sm:inline", is_nil(@filter_module) && "hidden"]}>
{(@filter_module && String.capitalize(@filter_module)) || gettext("Module")}
</span>
<.icon
name="hero-chevron-down"
class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
/>
</div>
<ul
tabindex="0"
onclick="document.activeElement.blur()"
class="dropdown-content menu bg-base-100 rounded-box z-20 w-52 p-2 shadow border border-base-200 max-h-72 overflow-y-auto flex-nowrap"
>
<li>
<.link
patch={filter_path(assigns, module: "")}
class={is_nil(@filter_module) && "active"}
>
{gettext("All Modules")}
</.link>
</li>
<li :for={mod <- @modules}>
<.link
patch={filter_path(assigns, module: mod)}
class={@filter_module == mod && "active"}
>
{String.capitalize(mod)}
</.link>
</li>
</ul>
</div>
<%!-- Mode --%>
<div class="dropdown">
<div
tabindex="0"
role="button"
class={["btn btn-sm gap-2", @filter_mode && "btn-active"]}
>
<.icon name="hero-cog-6-tooth" class="w-4 h-4" />
<span class={["sm:inline", is_nil(@filter_mode) && "hidden"]}>
{(@filter_mode && String.capitalize(@filter_mode)) || gettext("Mode")}
</span>
<.icon
name="hero-chevron-down"
class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
/>
</div>
<ul
tabindex="0"
onclick="document.activeElement.blur()"
class="dropdown-content menu bg-base-100 rounded-box z-20 w-52 p-2 shadow border border-base-200 max-h-72 overflow-y-auto flex-nowrap"
>
<li>
<.link
patch={filter_path(assigns, mode: "")}
class={is_nil(@filter_mode) && "active"}
>
{gettext("All Modes")}
</.link>
</li>
<li :for={mode <- @modes}>
<.link
patch={filter_path(assigns, mode: mode)}
class={@filter_mode == mode && "active"}
>
{String.capitalize(mode)}
</.link>
</li>
</ul>
</div>
<%!-- Action --%>
<div class="dropdown">
<div
tabindex="0"
role="button"
class={["btn btn-sm gap-2", @filter_action && "btn-active"]}
>
<.icon name="hero-bolt" class="w-4 h-4" />
<span class={[
"sm:inline truncate max-w-[10rem]",
is_nil(@filter_action) && "hidden"
]}>
{@filter_action || gettext("Action")}
</span>
<.icon
name="hero-chevron-down"
class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
/>
</div>
<ul
tabindex="0"
onclick="document.activeElement.blur()"
class="dropdown-content menu bg-base-100 rounded-box z-20 w-56 p-2 shadow border border-base-200 max-h-72 overflow-y-auto flex-nowrap"
>
<li>
<.link
patch={filter_path(assigns, action: "")}
class={is_nil(@filter_action) && "active"}
>
{gettext("All Actions")}
</.link>
</li>
<li :for={action <- @action_types}>
<.link
patch={filter_path(assigns, action: action)}
class={@filter_action == action && "active"}
>
{action}
</.link>
</li>
</ul>
</div>
<%!-- Type --%>
<div class="dropdown">
<div
tabindex="0"
role="button"
class={["btn btn-sm gap-2", @filter_resource_type && "btn-active"]}
>
<.icon name="hero-tag" class="w-4 h-4" />
<span class={["sm:inline", is_nil(@filter_resource_type) && "hidden"]}>
{(@filter_resource_type && String.capitalize(@filter_resource_type)) ||
gettext("Type")}
</span>
<.icon
name="hero-chevron-down"
class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
/>
</div>
<ul
tabindex="0"
onclick="document.activeElement.blur()"
class="dropdown-content menu bg-base-100 rounded-box z-20 w-52 p-2 shadow border border-base-200 max-h-72 overflow-y-auto flex-nowrap"
>
<li>
<.link
patch={filter_path(assigns, resource_type: "")}
class={is_nil(@filter_resource_type) && "active"}
>
{gettext("All Types")}
</.link>
</li>
<li :for={rt <- @resource_types}>
<.link
patch={filter_path(assigns, resource_type: rt)}
class={@filter_resource_type == rt && "active"}
>
{String.capitalize(rt)}
</.link>
</li>
</ul>
</div>
</div>
</:toolbar_title>
<:toolbar_actions>
<span class="text-sm text-base-content/60 whitespace-nowrap">
{ngettext("%{count} activity", "%{count} activities", @total, count: @total)}
</span>
<%= if any_filter_active?(assigns) do %>
<button type="button" phx-click="clear_filters" class="btn btn-sm btn-outline">
<.icon name="hero-x-mark" class="w-4 h-4" /> {gettext("Clear filters")}
</button>
<% end %>
</:toolbar_actions>
<:card_header :let={entry}>
<span class={"badge badge-sm #{action_badge_color(entry.action)}"}>
{entry.action}
</span>
</:card_header>
<:card_actions :let={entry}>
<.link
navigate={Routes.path("/admin/activity/#{entry.uuid}")}
class="btn btn-sm btn-outline btn-info"
>
<.icon name="hero-arrow-top-right-on-square" class="w-4 h-4" /> {gettext("View")}
</.link>
</:card_actions>
<.table_default_header>
<.table_default_row hover={false}>
<.table_default_header_cell class="hidden md:table-cell">
{gettext("Module")}
</.table_default_header_cell>
<.table_default_header_cell class="hidden md:table-cell">
{gettext("Mode")}
</.table_default_header_cell>
<.table_default_header_cell>{gettext("Activity")}</.table_default_header_cell>
<.table_default_header_cell class="hidden md:table-cell">
{gettext("Actor")}
</.table_default_header_cell>
<.table_default_header_cell class="hidden md:table-cell">
{gettext("Details")}
</.table_default_header_cell>
<.table_default_header_cell class="hidden md:table-cell">
{gettext("Subject")}
</.table_default_header_cell>
<.table_default_header_cell class="hidden md:table-cell">
{gettext("Date")}
</.table_default_header_cell>
<.table_default_header_cell class="w-12 md:w-auto"></.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= if Enum.empty?(@entries) do %>
<.table_default_row>
<.table_default_cell colspan={8}>
<div class="text-center py-12 text-base-content/60">
<.icon name="hero-bell-slash" class="w-12 h-12 mx-auto mb-2 opacity-50" />
<%= if any_filter_active?(assigns) do %>
<p>{gettext("No activities match the current filters")}</p>
<button
type="button"
phx-click="clear_filters"
class="btn btn-sm btn-outline mt-3"
>
<.icon name="hero-x-mark" class="w-4 h-4" /> {gettext("Clear filters")}
</button>
<% else %>
<p>{gettext("No activities recorded yet")}</p>
<% end %>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
<%= for entry <- @entries do %>
<.table_default_row>
<.table_default_cell class="text-sm hidden md:table-cell">
<%= if entry.module do %>
<span class="badge badge-outline badge-xs">{entry.module}</span>
<% else %>
<span class="text-base-content/30"></span>
<% end %>
</.table_default_cell>
<.table_default_cell class="text-sm hidden md:table-cell">
<%= if entry.mode do %>
<span class={"badge badge-xs #{mode_badge_color(entry.mode)}"}>
{entry.mode}
</span>
<% else %>
<span class="text-base-content/30"></span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<span class={"badge badge-sm #{action_badge_color(entry.action)}"}>
{entry.action}
</span>
<%!-- Actor folded in on mobile (the Actor column is hidden there). --%>
<div class="md:hidden text-xs text-base-content/60 truncate mt-0.5">
{if entry.actor, do: entry.actor.email, else: gettext("System")}
</div>
</.table_default_cell>
<.table_default_cell class="text-sm hidden md:table-cell">
<%= if entry.actor do %>
<.resource_email_link
info={Map.get(@resource_links, {"user", entry.actor_uuid})}
label={entry.actor.email}
class="font-semibold"
/>
<%= if entry.metadata["actor_role"] do %>
<span class={[
"badge badge-xs ml-1",
if(entry.metadata["actor_role"] == "admin",
do: "badge-warning",
else: "badge-ghost"
)
]}>
{entry.metadata["actor_role"]}
</span>
<% end %>
<% else %>
<span class="text-base-content/50">{gettext("System")}</span>
<% end %>
</.table_default_cell>
<.table_default_cell class="text-xs text-base-content/60 hidden md:table-cell">
<%= if entry.target && entry.target_uuid != entry.resource_uuid do %>
<div class="mb-0.5">
<span class="text-base-content/50">&rarr;</span>
<.resource_email_link
info={Map.get(@resource_links, {"user", entry.target_uuid})}
label={entry.target.email}
/>
</div>
<% end %>
<% summary = summarize_details(entry.metadata) %>
<%= if summary do %>
<span>{summary}</span>
<% else %>
<span class="text-base-content/30"></span>
<% end %>
</.table_default_cell>
<.table_default_cell class="hidden md:table-cell">
<%= if entry.resource_type do %>
<% link_info =
Map.get(@resource_links, {entry.resource_type, entry.resource_uuid}) %>
<span class="badge badge-ghost badge-xs">{entry.resource_type}</span>
<%!-- Deep-link chip to the underlying resource when it resolves
(same mechanism as the comments moderation admin). Falls
back to the resolved user email or a truncated uuid. --%>
<%= cond do %>
<% link_info -> %>
<.resource_link info={link_info} class="ml-1" />
<% match?(%{email: _}, Map.get(@resource_users, entry.resource_uuid)) -> %>
<span class="text-xs ml-1">
{Map.get(@resource_users, entry.resource_uuid).email}
</span>
<% entry.resource_uuid -> %>
<span class="text-xs text-base-content/40 ml-1">
{String.slice(to_string(entry.resource_uuid), 0..7)}
</span>
<% true -> %>
<% end %>
<% else %>
<span class="text-base-content/30"></span>
<% end %>
</.table_default_cell>
<.table_default_cell class="text-xs text-base-content/60 hidden md:table-cell">
<div>{UtilsDate.format_date_with_user_format(entry.inserted_at)}</div>
<div class="text-base-content/40">
{UtilsDate.format_time_with_user_format(entry.inserted_at)}
</div>
</.table_default_cell>
<.table_default_cell>
<.link
navigate={Routes.path("/admin/activity/#{entry.uuid}")}
class="btn btn-ghost btn-xs btn-square"
title={gettext("View details")}
>
<.icon name="hero-arrow-top-right-on-square" class="w-4 h-4" />
</.link>
</.table_default_cell>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
<%!-- Pagination --%>
<%= if @total_pages > 1 do %>
<div class="flex justify-center mt-6">
<div class="join">
<%= for page <- max(1, @page - 2)..min(@total_pages, @page + 2) do %>
<.link
patch={
Routes.path(
"/admin/activity?#{URI.encode_query(Enum.reject([{"page", page}, {"module", @filter_module}, {"mode", @filter_mode}, {"action", @filter_action}, {"resource_type", @filter_resource_type}], fn {_k, v} -> is_nil(v) or v == "" end))}"
)
}
class={["join-item btn btn-sm", if(page == @page, do: "btn-active", else: "")]}
>
{page}
</.link>
<% end %>
</div>
</div>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>