Packages
phoenix_kit
1.7.91
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
Current section
Files
lib/phoenix_kit_web/live/activity/index.ex
defmodule PhoenixKitWeb.Live.Activity.Index do
@moduledoc """
Admin LiveView for the activity feed.
Shows a real-time stream of business-level actions across the platform
with filtering by action type, user, resource type, and date range.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Activity
alias PhoenixKit.PubSub.Manager, as: PubSubManager
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Utils.Routes
@impl true
def mount(_params, _session, socket) do
scope = socket.assigns[:phoenix_kit_current_scope]
if scope && Scope.has_module_access?(scope, "dashboard") do
if connected?(socket) do
PubSubManager.subscribe(Activity.pubsub_topic())
end
project_title = Settings.get_project_title()
socket =
socket
|> assign(:page_title, "Activity")
|> assign(:project_title, project_title)
|> assign(:modules, Activity.list_modules())
|> assign(:modes, Activity.list_modes())
|> assign(:action_types, Activity.list_action_types())
|> assign(:resource_types, Activity.list_resource_types())
|> assign_filter_defaults()
|> load_activities()
{:ok, socket}
else
{:ok,
socket
|> put_flash(:error, "Access denied")
|> push_navigate(to: Routes.path("/admin"))}
end
end
@impl true
def handle_params(params, url, socket) do
socket =
socket
|> assign(:url_path, URI.parse(url).path)
|> apply_params(params)
|> load_activities()
{:noreply, socket}
end
@impl true
def handle_event("filter", params, socket) do
filter_params = Map.get(params, "filter", %{})
new_params =
%{"page" => "1"}
|> maybe_put("module", filter_params["module"])
|> maybe_put("mode", filter_params["mode"])
|> maybe_put("action", filter_params["action"])
|> maybe_put("resource_type", filter_params["resource_type"])
query = URI.encode_query(new_params)
{:noreply, push_patch(socket, to: Routes.path("/admin/activity?#{query}"))}
end
@impl true
def handle_event("clear_filters", _params, socket) do
{:noreply, push_patch(socket, to: Routes.path("/admin/activity"))}
end
@impl true
def handle_info({:activity_logged, _entry}, socket) do
# Reload on new activity (real-time update)
{:noreply, load_activities(socket)}
end
## Private
defp assign_filter_defaults(socket) do
socket
|> assign(:page, 1)
|> assign(:per_page, 50)
|> assign(:filter_module, nil)
|> assign(:filter_mode, nil)
|> assign(:filter_action, nil)
|> assign(:filter_resource_type, nil)
end
defp apply_params(socket, params) do
socket
|> assign(:page, parse_int(params["page"], 1))
|> assign(:filter_module, blank_to_nil(params["module"]))
|> assign(:filter_mode, blank_to_nil(params["mode"]))
|> assign(:filter_action, blank_to_nil(params["action"]))
|> assign(:filter_resource_type, blank_to_nil(params["resource_type"]))
end
defp load_activities(socket) do
result =
Activity.list(
page: socket.assigns.page,
per_page: socket.assigns.per_page,
module: socket.assigns.filter_module,
mode: socket.assigns.filter_mode,
action: socket.assigns.filter_action,
resource_type: socket.assigns.filter_resource_type,
preload: [:actor, :target]
)
resource_users = Activity.resolve_resource_users(result.entries)
socket
|> assign(:entries, result.entries)
|> assign(:resource_users, resource_users)
|> assign(:total, result.total)
|> assign(:total_pages, result.total_pages)
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, _key, ""), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp parse_int(nil, default), do: default
defp parse_int(str, default) when is_binary(str) do
case Integer.parse(str) do
{n, _} -> max(n, 1)
:error -> default
end
end
defp blank_to_nil(nil), do: nil
defp blank_to_nil(""), do: nil
defp blank_to_nil(val), do: val
defp action_badge_color(action) do
cond do
String.contains?(action, "created") ->
"badge-success"
String.contains?(action, "deleted") ->
"badge-error"
String.contains?(action, "updated") or String.contains?(action, "changed") ->
"badge-warning"
String.contains?(action, "liked") or String.contains?(action, "followed") ->
"badge-info"
true ->
"badge-ghost"
end
end
defp summarize_details(metadata) do
meta = metadata || %{}
if meta["added"] || meta["removed"] do
# For role updates, show added/removed summary
parts = []
parts = if meta["added"], do: parts ++ ["added: #{meta["added"]}"], else: parts
parts = if meta["removed"], do: parts ++ ["removed: #{meta["removed"]}"], else: parts
Enum.join(parts, ", ")
else
# For profile updates, extract field names from _from/_to pairs
changed_fields =
meta
|> Map.keys()
|> Enum.filter(&String.ends_with?(&1, "_to"))
|> Enum.map(&String.trim_trailing(&1, "_to"))
|> Enum.reject(&(&1 == ""))
if changed_fields != [] do
fields = Enum.map_join(changed_fields, ", ", &String.replace(&1, "_", " "))
"#{fields} updated"
else
summarize_remaining_meta(meta)
end
end
end
defp summarize_remaining_meta(meta) do
meta
|> Map.drop(["method", "actor_role"])
|> Enum.reject(fn {_k, v} -> v == nil or v == "" end)
|> case do
[] -> nil
entries -> Enum.map_join(entries, ", ", fn {k, v} -> "#{k}: #{v}" end)
end
end
defp format_time_ago(datetime) do
diff = DateTime.diff(DateTime.utc_now(), datetime, :second)
cond do
diff < 60 -> "just now"
diff < 3600 -> "#{div(diff, 60)}m ago"
diff < 86_400 -> "#{div(diff, 3600)}h ago"
diff < 604_800 -> "#{div(diff, 86_400)}d ago"
true -> Calendar.strftime(datetime, "%b %d, %Y")
end
end
end