Packages

phoenix_kit

1.7.39
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 modules db web activity.ex
Raw

lib/modules/db/web/activity.ex

defmodule PhoenixKit.Modules.DB.Web.Activity do
@moduledoc """
Live activity monitor for database changes.
Shows real-time INSERT, UPDATE, DELETE operations across all tables
with full row data.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Modules.DB
alias PhoenixKit.Modules.DB.Listener
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
@impl true
def mount(params, _session, socket) do
locale = params["locale"] || Routes.get_default_admin_locale()
# Subscribe to all table changes
if connected?(socket) do
Listener.subscribe_all()
# Update the trigger function to include row_id
update_trigger_function()
end
# Load list of tables for the filter dropdown
tables = load_tables()
# Check for table filter from query params
initial_table_filter =
case params["table"] do
nil -> nil
"" -> nil
table -> table
end
socket =
socket
|> assign(:page_title, "Live Activity")
|> assign(:project_title, Settings.get_project_title())
|> assign(:current_locale, locale)
|> assign(:current_path, Routes.path("/admin/db/activity", locale: locale))
|> assign(:activity_log, [])
|> assign(:paused, false)
|> assign(:filter_table, initial_table_filter)
|> assign(:filter_operation, nil)
|> assign(:tables, tables)
# Track previous row states for diff highlighting
|> assign(:row_states, %{})
{:ok, socket}
end
defp load_tables do
# Get all tables (use high per_page to get them all)
result = DB.list_tables(%{page: 1, per_page: 1000})
result.entries
|> Enum.map(fn t -> "#{t.schema}.#{t.name}" end)
|> Enum.sort()
end
@impl true
def handle_info({:table_changed, schema, table, operation, row_id}, socket) do
if socket.assigns.paused do
{:noreply, socket}
else
socket = add_activity_entry(socket, schema, table, operation, row_id)
{:noreply, socket}
end
end
@impl true
def handle_event("toggle_pause", _, socket) do
{:noreply, assign(socket, :paused, !socket.assigns.paused)}
end
@impl true
def handle_event("clear_log", _, socket) do
socket =
socket
|> assign(:activity_log, [])
|> assign(:row_states, %{})
{:noreply, socket}
end
@impl true
def handle_event("filter_table", %{"table" => table}, socket) do
filter = if table == "", do: nil, else: table
{:noreply, assign(socket, :filter_table, filter)}
end
@impl true
def handle_event("filter_operation", %{"operation" => operation}, socket) do
filter = if operation == "", do: nil, else: operation
{:noreply, assign(socket, :filter_operation, filter)}
end
# Update the trigger function to ensure it includes row_id
defp update_trigger_function do
# This will CREATE OR REPLACE the function with the new version that includes row_id
DB.ensure_trigger("public", "phoenix_kit_settings")
end
defp add_activity_entry(socket, schema, table, operation, row_id) do
# Apply filters
if matches_filters?(socket, schema, table, operation) do
timestamp = DateTime.utc_now()
row_key = {schema, table, row_id}
# Fetch row data for INSERT/UPDATE
row_data =
if operation in ["INSERT", "UPDATE"] and row_id do
case DB.fetch_row(schema, table, row_id) do
{:ok, row} -> row
_ -> nil
end
else
nil
end
# Calculate changed keys by comparing with previous state
previous_state = Map.get(socket.assigns.row_states, row_key)
{changed_keys, new_keys} =
if row_data && previous_state do
compute_diff(previous_state, row_data)
else
{MapSet.new(), MapSet.new()}
end
# For INSERT, mark all keys as "new"
new_keys =
if operation == "INSERT" && row_data do
row_data |> Map.keys() |> MapSet.new()
else
new_keys
end
entry = %{
id: System.unique_integer([:positive]),
timestamp: timestamp,
schema: schema,
table: table,
operation: operation,
row_id: row_id,
row_data: row_data,
changed_keys: changed_keys,
new_keys: new_keys
}
# Update row_states with current state
row_states =
if row_data && row_id do
Map.put(socket.assigns.row_states, row_key, row_data)
else
socket.assigns.row_states
end
# Keep last 100 entries, newest first
activity_log =
[entry | socket.assigns.activity_log]
|> Enum.take(100)
socket
|> assign(:activity_log, activity_log)
|> assign(:row_states, row_states)
else
socket
end
end
# Compare previous and current row data, returns {changed_keys, new_keys}
defp compute_diff(previous, current) do
all_keys = MapSet.union(MapSet.new(Map.keys(previous)), MapSet.new(Map.keys(current)))
Enum.reduce(all_keys, {MapSet.new(), MapSet.new()}, fn key, {changed, new} ->
prev_val = Map.get(previous, key)
curr_val = Map.get(current, key)
cond do
# Key didn't exist before, it's new
is_nil(prev_val) && !is_nil(curr_val) ->
{changed, MapSet.put(new, key)}
# Value changed
prev_val != curr_val ->
{MapSet.put(changed, key), new}
# No change
true ->
{changed, new}
end
end)
end
defp matches_filters?(socket, schema, table, operation) do
full_table_name = "#{schema}.#{table}"
table_match =
case socket.assigns.filter_table do
nil -> true
filter -> full_table_name == filter
end
operation_match =
case socket.assigns.filter_operation do
nil -> true
filter -> operation == filter
end
table_match and operation_match
end
def operation_badge_class("INSERT"), do: "badge-success"
def operation_badge_class("UPDATE"), do: "badge-warning"
def operation_badge_class("DELETE"), do: "badge-error"
def operation_badge_class(_), do: "badge-ghost"
def format_value(value) when is_map(value), do: Jason.encode!(value, pretty: true)
def format_value(value) when is_list(value), do: inspect(value, pretty: true)
def format_value(value) when is_binary(value) and byte_size(value) > 200 do
String.slice(value, 0, 200) <> "..."
end
def format_value(value), do: inspect(value)
# Helper to check if a key was changed (value modified)
def key_changed?(entry, key) do
MapSet.member?(entry.changed_keys, key)
end
# Helper to check if a key is new (didn't exist before)
def key_new?(entry, key) do
MapSet.member?(entry.new_keys, key)
end
# Get CSS class for highlighting
def field_highlight_class(entry, key) do
cond do
key_new?(entry, key) -> "bg-success/20 border-l-2 border-success"
key_changed?(entry, key) -> "bg-warning/20 border-l-2 border-warning"
true -> ""
end
end
end