MishkaCmsCoreWeb.Live.Admin.Components.ActivityFeed (Mishka CMS Core v0.0.1)

Copy Markdown View Source

Real-time activity feed driven by a pluggable list of sources, rendered as a collapsed "Logs" toggle over a System Logs panel.

Source adapters implement MishkaCmsCore.Activity.Behaviours.Source. Each adapter knows its own topics, broadcasts, entry shape, and initial fetch — this component just composes them.

Usage

Wire a page with setup/2 and render it with feed/1; see setup/2 for the options and the ordering rule. A page with a collapsible panel of its own pairs the two with close_panel/2, so only one of them is open at a time.

Behaviour

  • The panel is collapsed on load. The toggle shows/hides it client-side with Phoenix.LiveView.JS (a fade/slide transition), and pushes to the server so the first open can fetch.
  • History is fetched on the FIRST open, not on mount — a page carrying a feed pays nothing for it until asked. Skeleton rows stand in while the fetch runs.
  • The panel does not page. It shows one window of :limit rows and links out to the full activity page, which pages properly — asking a reader to manage a growing preview and a full list at once was the worse of the two.
  • Live broadcasts arriving before the first open are buffered and merged into that first fetch — sources with no history (RuntimeCompile) only ever appear live, so dropping them on the opening fetch would lose them.

Parent responsibilities

The component runs in the parent LiveView's process, so PubSub messages arrive at the parent's mailbox. setup/2 handles this; a parent driving the component directly must:

  1. On mount, subscribe to topics_for/2,3.
  2. Forward incoming %Phoenix.Socket.Broadcast{} messages to this component via send_update/3, under :broadcast. Use handles?/2 to filter.

See also: MishkaCmsCore.Activity.Sources.PaperTrail, MishkaCmsCore.Activity.Sources.Oban.

Summary

Functions

JS that closes the feed's panel: the client-side hide, plus the push that tells the feed its panel is shut so its toggle stops reading open.

Renders the feed from the @activity_feed map produced by setup/2. The scope (resources/sources) is fixed by setup/2 so the subscription and the display always agree; only :limit may be overridden per call.

A failed fetch still opens the feed to whatever arrives live.

Closes the panel without regard to what it was doing, so a page that closed it client-side to make room for its own panel can say so. Unlike "toggle_logs" this is not a flip: a page fires it on every one of its own opens, whether the feed was open or not.

True when at least one source claims ownership of the broadcast. Use in the parent's handle_info/2 to decide whether to forward.

One-call wiring for a parent LiveView. Subscribes to the activity topics for opts[:resources] and attaches a handle_info hook that forwards matching broadcasts to this component — the page's own handle_info clauses are left untouched (resource notifications pass through so the table still updates; Oban broadcasts are consumed here so they never reach an unmatched clause).

PubSub topics the parent should subscribe to so the feed receives live updates. Pass the same current_user, sources, and resources you pass to the component itself.

Functions

close_panel(js \\ %JS{}, setup)

JS that closes the feed's panel: the client-side hide, plus the push that tells the feed its panel is shut so its toggle stops reading open.

A page with a panel of its own chains this onto that panel's toggle, and passes the reverse to feed/1's :on_toggle, so opening either panel closes the other. Both halves are unconditional: hiding an already-hidden panel does nothing, so neither toggle has to know which panel is open.

<ActivityFeed.feed setup={@activity_feed} on_toggle={close_issues()} />

defp toggle_issues(setup) do
  %JS{}
  |> ActivityFeed.close_panel(setup)
  |> JS.toggle(to: "#issues-panel")
  |> JS.push("toggle_issues")
end

Takes the @activity_feed map from setup/2, whose :id names both the panel and the feed's own root element — the push is aimed at that root, so it reaches the component rather than the page.

feed(assigns)

Renders the feed from the @activity_feed map produced by setup/2. The scope (resources/sources) is fixed by setup/2 so the subscription and the display always agree; only :limit may be overridden per call.

<ActivityFeed.feed setup={@activity_feed} />
<ActivityFeed.feed setup={@activity_feed} limit={10} />

Attributes

  • setup (:map) (required)

  • limit (:integer) - Defaults to nil.

  • on_toggle (Phoenix.LiveView.JS) - JS to run when the feed's toggle is clicked, ahead of the panel's own transition. A page with a panel of its own passes the JS closing that panel, so only one is ever open; close_panel/2 is the mirror image, for that panel's toggle to close the feed.

    Defaults to %Phoenix.LiveView.JS{ops: []}.

Slots

  • toggles - Buttons that sit beside the feed's own toggle, on its row. A page with its own panel puts its toggle here rather than beside the feed: the feed is full width so its panel is, and wrapping it in a column to make room would narrow the panel too.

handle_async(atom, arg, socket)

A failed fetch still opens the feed to whatever arrives live.

The history could not be read, but broadcasts keep coming, and they are the only entries this feed will ever have. Leaving loaded? false would buffer every one of them into :pending while the header went on counting them — a list that says "3 entries" and shows none.

handle_event(binary, params, socket)

Closes the panel without regard to what it was doing, so a page that closed it client-side to make room for its own panel can say so. Unlike "toggle_logs" this is not a flip: a page fires it on every one of its own opens, whether the feed was open or not.

handles?(msg, sources \\ [MishkaCmsCore.Activity.Sources.PaperTrail])

True when at least one source claims ownership of the broadcast. Use in the parent's handle_info/2 to decide whether to forward.

setup(socket, opts \\ [])

One-call wiring for a parent LiveView. Subscribes to the activity topics for opts[:resources] and attaches a handle_info hook that forwards matching broadcasts to this component — the page's own handle_info clauses are left untouched (resource notifications pass through so the table still updates; Oban broadcasts are consumed here so they never reach an unmatched clause).

Because that hook halts the Oban and runtime-compile broadcasts, any hook of the page's own that needs them (RuntimeStatusLive.setup/2, PreviewHelpers.setup/1, a site-health hook) must be attached BEFORE this one.

Call it in mount/3 after current_user is assigned, then render the feed with feed/1, passing the single @activity_feed assign it sets:

socket =
  ActivityFeed.setup(socket,
    resources: [MishkaCmsCore.Runtime.Component],
    feed_id: "components-activity"
  )

# in the template:
<ActivityFeed.feed setup={@activity_feed} />

Options

  • :resources — modules to scope to; omit for a site-wide feed.
  • :feed_id — this component's id (default "activity-feed").
  • :sources — source adapters (default: all built-in — PaperTrail, EventLog, Oban, RuntimeCompile, RuntimeDraft).
  • :limit — how many rows the panel shows (default 5). A template may override it per call via <ActivityFeed.feed setup={...} limit={10} />.

topics_for(current_user, sources \\ [MishkaCmsCore.Activity.Sources.PaperTrail], resources \\ nil)

PubSub topics the parent should subscribe to so the feed receives live updates. Pass the same current_user, sources, and resources you pass to the component itself.