From 4d4601f8f5f4db34fea48b82dcbfbe091cf9c9a9 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 14 Jul 2026 09:44:50 +1000 Subject: [PATCH 1/3] refactor: use assigns for log event modal state --- lib/logflare_web/live/log_event_live.ex | 15 ++++- .../search_log_event_viewer_component.ex | 25 ++------ .../search_live/event_context_component.ex | 17 ++--- .../live/search_live/log_event_components.ex | 12 +--- .../live/search_live/logs_search_lv.ex | 64 +++++++++++-------- .../templates/log/log_event.html.heex | 25 ++++---- .../logflare_web/live/log_event_live_test.exs | 6 +- .../search_live/log_event_components_test.exs | 33 ---------- .../live/search_live/logs_search_lv_test.exs | 38 ++++++++++- 9 files changed, 123 insertions(+), 112 deletions(-) diff --git a/lib/logflare_web/live/log_event_live.ex b/lib/logflare_web/live/log_event_live.ex index 3c2d22688f..e8fc8198a5 100644 --- a/lib/logflare_web/live/log_event_live.ex +++ b/lib/logflare_web/live/log_event_live.ex @@ -24,6 +24,8 @@ defmodule LogflareWeb.LogEventLive do lql = params["lql"] || "" is_tailing = params["tailing?"] == "true" + search_timezone = params["tz"] || preferred_timezone(socket.assigns) + opts = [ source: source, @@ -49,7 +51,7 @@ defmodule LogflareWeb.LogEventLive do |> assign(:log_event_id, params["uuid"]) |> assign(:lql, lql) |> assign(:tailing?, is_tailing) - |> assign(:tz, params["tz"]) + |> assign(:search_timezone, search_timezone) |> assign(:timestamp, timestamp) {:ok, socket} @@ -68,4 +70,15 @@ defmodule LogflareWeb.LogEventLive do defp maybe_put_timestamp(opts, timestamp), do: Keyword.put(opts, :timestamp, DateTime.truncate(timestamp, :second)) + + @spec preferred_timezone(map()) :: String.t() + defp preferred_timezone(%{team_user: %{preferences: %{timezone: timezone}}}) + when is_binary(timezone), + do: timezone + + defp preferred_timezone(%{user: %{preferences: %{timezone: timezone}}}) + when is_binary(timezone), + do: timezone + + defp preferred_timezone(_assigns), do: "Etc/UTC" end diff --git a/lib/logflare_web/live/log_event_live/search_log_event_viewer_component.ex b/lib/logflare_web/live/log_event_live/search_log_event_viewer_component.ex index 33d8a0c3ae..0ef4c9285d 100644 --- a/lib/logflare_web/live/log_event_live/search_log_event_viewer_component.ex +++ b/lib/logflare_web/live/log_event_live/search_log_event_viewer_component.ex @@ -37,7 +37,7 @@ defmodule LogflareWeb.Search.LogEventViewerComponent do params = event_params(assigns) |> Map.merge(%{log_event_id: id, timestamp: d}) - |> Map.put(:lql, assigns.params["lql"] || "") + |> Map.put(:lql, assigns.lql) socket = socket @@ -94,11 +94,7 @@ defmodule LogflareWeb.Search.LogEventViewerComponent do @impl true def render(%{source: source, log_event: %LE{body: body} = le} = assigns) do - tz = - if assigns.team_user, - do: Map.get(assigns.team_user.preferences || %{}, :timezone, "Etc/UTC"), - else: Map.get(assigns.user.preferences || %{}, :timezone, "Etc/UTC") - + tz = assigns.search_timezone timestamp = Timex.from_unix(body["timestamp"], :microsecond) local_timestamp = @@ -110,7 +106,7 @@ defmodule LogflareWeb.Search.LogEventViewerComponent do LogView.render("log_event_body.html", source: source, source_schema_flat_map: assigns.source_schema_flat_map, - search_params: assigns.search_params, + search_params: %{"tz" => tz}, team: assigns.team, body: body, fmt_body: BqSchema.encode_metadata(body), @@ -119,8 +115,7 @@ defmodule LogflareWeb.Search.LogEventViewerComponent do lql: assigns.lql, lql_schema: get_lql_schema(source), timestamp: timestamp, - local_timezone: tz, - search_timezone: assigns.search_params["tz"] || tz, + local_timezone: assigns.search_timezone, local_timestamp: local_timestamp ) end @@ -136,13 +131,12 @@ defmodule LogflareWeb.Search.LogEventViewerComponent do team = socket.assigns[:team] || assigns[:team] source = socket.assigns[:source] || assigns[:source] timestamp = socket.assigns[:timestamp] || assigns[:timestamp] - lql = socket.assigns[:lql] || assigns[:lql] || assigns.params["lql"] || "" + lql = assigns[:lql] || socket.assigns[:lql] || "" source_schema_flat_map = socket.assigns[:source_schema_flat_map] || assigns[:source_schema_flat_map] - search_params = - socket.assigns[:search_params] || extract_search_params(assigns) + search_timezone = assigns[:search_timezone] || socket.assigns[:search_timezone] || "Etc/UTC" socket |> assign(:user, user) @@ -152,7 +146,7 @@ defmodule LogflareWeb.Search.LogEventViewerComponent do |> assign(:timestamp, timestamp) |> assign(:lql, lql) |> assign(:source_schema_flat_map, source_schema_flat_map) - |> assign(:search_params, search_params) + |> assign(:search_timezone, search_timezone) |> assign(:error, nil) end @@ -172,9 +166,4 @@ defmodule LogflareWeb.Search.LogEventViewerComponent do _ -> SchemaBuilder.initial_table_schema() end end - - defp extract_search_params(%{params: params}) when is_map(params), - do: Map.take(params, ["tz"]) - - defp extract_search_params(_assigns), do: %{} end diff --git a/lib/logflare_web/live/search_live/event_context_component.ex b/lib/logflare_web/live/search_live/event_context_component.ex index f678c0e327..0c1a041180 100644 --- a/lib/logflare_web/live/search_live/event_context_component.ex +++ b/lib/logflare_web/live/search_live/event_context_component.ex @@ -14,18 +14,19 @@ defmodule LogflareWeb.SearchLive.EventContextComponent do %{ params: %{ "log-event-timestamp" => log_timestamp, - "log-event-id" => log_event_id, - "querystring" => query_string, - "source-id" => source_id, - "timezone" => timezone - } + "log-event-id" => log_event_id + }, + querystring: query_string, + search_timezone: timezone, + source: source } = assigns + source_id = source.id + event_timestamp = log_timestamp |> String.to_integer() |> Timex.from_unix(:microsecond) lql_rules = - Sources.get_source_for_lv_param(source_id) - |> prepare_lql_rules(query_string, event_timestamp) + prepare_lql_rules(source, query_string, event_timestamp) {:ok, socket @@ -33,7 +34,7 @@ defmodule LogflareWeb.SearchLive.EventContextComponent do |> assign(target_event_id: log_event_id, timezone: timezone) |> assign(is_truncated_before: false) |> assign(is_truncated_after: false) - |> assign(source: Sources.get_source_for_lv_param(source_id)) + |> assign(source: source) |> assign(:logs, AsyncResult.loading()) |> start_async(:logs, fn -> search_logs(log_event_id, event_timestamp, source_id, lql_rules) diff --git a/lib/logflare_web/live/search_live/log_event_components.ex b/lib/logflare_web/live/search_live/log_event_components.ex index b6da8d20e3..7d412fca37 100644 --- a/lib/logflare_web/live/search_live/log_event_components.ex +++ b/lib/logflare_web/live/search_live/log_event_components.ex @@ -24,8 +24,6 @@ defmodule LogflareWeb.SearchLive.LogEventComponents do attr :last_query_completed_at, :any, default: nil attr :loading, :boolean, required: true attr :search_timezone, :string, required: true - attr :tailing?, :boolean, required: true - attr :querystring, :string, required: true attr :empty_event_message_placeholder, :string, default: @default_empty_event_message attr :source_schema_flat_map, :map, default: %{} attr :search_op, Logflare.Logs.SearchOperation @@ -47,24 +45,18 @@ defmodule LogflareWeb.SearchLive.LogEventComponents do title="Log Event" phx-value-log-event-id={log.id} phx-value-log-event-timestamp={log.body["timestamp"]} - phx-value-lql={@querystring} - phx-value-tailing?={@tailing?} - phx-value-tz={@search_timezone} > view <.modal_link component={LogflareWeb.SearchLive.EventContextComponent} - click={JS.push("soft_pause")} - close={if(@tailing?, do: JS.push("soft_play", target: "#source-logs-search-control") |> JS.push("close"), else: nil)} + click={JS.push("open_event_context")} + close={JS.push("close_event_context", target: "#source-logs-search-control") |> JS.push("close")} class="tw-text-[0.65rem]" modal_id={:log_event_context_viewer} title="View Event Context" phx-value-log-event-id={log.id} - phx-value-source-id={@search_op.source.id} phx-value-log-event-timestamp={log.body["timestamp"]} - phx-value-timezone={@search_timezone} - phx-value-querystring={@querystring} > context diff --git a/lib/logflare_web/live/search_live/logs_search_lv.ex b/lib/logflare_web/live/search_live/logs_search_lv.ex index 611222faaa..48842d96db 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -92,6 +92,7 @@ defmodule LogflareWeb.Source.SearchLV do tailing_initial?: true, tailing_timer: nil, tailing?: tailing?, + resume_tailing_after_context?: false, # search states search_op: nil, search_op_error: nil, @@ -245,6 +246,9 @@ defmodule LogflareWeb.Source.SearchLV do search_op_error: @search_op_error, team_user: @team_user, team: @team, + lql: @querystring, + querystring: @querystring, + search_timezone: @search_timezone, close: @modal.body[:close], return_to: @modal.body.return_to )} @@ -258,16 +262,7 @@ defmodule LogflareWeb.Source.SearchLV do
- +
{live_react_component( @@ -417,6 +412,28 @@ defmodule LogflareWeb.Source.SearchLV do soft_pause(ev, socket) end + def handle_event("open_event_context", _, socket) do + resume_tailing? = socket.assigns.tailing? + + socket = + socket + |> assign(:resume_tailing_after_context?, resume_tailing?) + |> pause_tailing() + + {:noreply, socket} + end + + def handle_event("close_event_context", _, socket) do + socket = + if socket.assigns.resume_tailing_after_context? do + resume_tailing(socket) + else + socket + end + + {:noreply, assign(socket, :resume_tailing_after_context?, false)} + end + def handle_event("hard_play" = ev, _, socket) do hard_play(ev, socket) end @@ -993,17 +1010,7 @@ defmodule LogflareWeb.Source.SearchLV do {:noreply, error_socket(socket, "Tailing is disabled for this source")} end - defp soft_play(_ev, %{assigns: prev_assigns} = socket) do - %{source: %{token: stoken} = _source} = prev_assigns - - kickoff_queries(stoken, socket.assigns) - - socket = - socket - |> assign(:tailing?, true) - - {:noreply, socket} - end + defp soft_play(_ev, socket), do: {:noreply, resume_tailing(socket)} defp soft_pause( _ev, @@ -1012,15 +1019,20 @@ defmodule LogflareWeb.Source.SearchLV do {:noreply, socket} end - defp soft_pause(_ev, %{assigns: %{source: _source, executor_pid: executor_pid}} = socket) do + defp soft_pause(_ev, socket), do: {:noreply, pause_tailing(socket)} + + defp pause_tailing(%{assigns: %{tailing?: false}} = socket), do: socket + + defp pause_tailing(%{assigns: %{executor_pid: executor_pid}} = socket) do maybe_cancel_tailing_timer(socket) SearchQueryExecutor.cancel_query(executor_pid) - socket = - socket - |> assign(:tailing?, false) + assign(socket, :tailing?, false) + end - {:noreply, socket} + defp resume_tailing(socket) do + kickoff_queries(socket.assigns.source.token, socket.assigns) + assign(socket, :tailing?, true) end defp hard_play( diff --git a/lib/logflare_web/templates/log/log_event.html.heex b/lib/logflare_web/templates/log/log_event.html.heex index cc0d089876..127ccb8676 100644 --- a/lib/logflare_web/templates/log/log_event.html.heex +++ b/lib/logflare_web/templates/log/log_event.html.heex @@ -15,18 +15,17 @@ module={LogflareWeb.Search.LogEventViewerComponent} id={:log_event_viewer} {%{ - user: @user, - source_schema_flat_map: @source_schema_flat_map, - source: @source, - timestamp: @timestamp, - log_event: @log_event, - params: %{ - "log-event-id" => @log_event_id, - "log-event-timestamp" => @timestamp, - "lql" => assigns[:lql], - "tailing?" => assigns[:tailing?], - "tz" => assigns[:tz] - } - }} + user: @user, + source_schema_flat_map: @source_schema_flat_map, + source: @source, + timestamp: @timestamp, + log_event: @log_event, + lql: @lql, + search_timezone: @search_timezone, + params: %{ + "log-event-id" => @log_event_id, + "log-event-timestamp" => @timestamp + } + }} />
diff --git a/test/logflare_web/live/log_event_live_test.exs b/test/logflare_web/live/log_event_live_test.exs index 67426d6a4e..d7ed57d5f6 100644 --- a/test/logflare_web/live/log_event_live_test.exs +++ b/test/logflare_web/live/log_event_live_test.exs @@ -8,7 +8,7 @@ defmodule LogflareWeb.LogEventLiveTest do setup %{conn: conn} do insert(:plan) - user = insert(:user) + user = insert(:user, preferences: build(:user_preferences, timezone: "Singapore")) source = insert(:source, user: user) insert(:source_schema, source: source) conn = login_user(conn, user) @@ -33,12 +33,14 @@ defmodule LogflareWeb.LogEventLiveTest do TestUtils.gen_bq_response([%{"id" => le.id, "event_message" => le.body["event_message"]}])} end) - {:ok, _view, _html} = + {:ok, view, _html} = live( conn, ~p"/sources/#{source.id}/event?#{%{timestamp: "2024-01-10T20:13:03Z", uuid: le.id}}" ) + assert view |> element("a", "inspect") |> render() =~ "tz=Singapore" + TestUtils.retry_assert(fn -> assert_receive {:query, ^ref, body} assert Enum.any?(body.queryParameters, &(&1.parameterValue.value =~ "2024-01-")) diff --git a/test/logflare_web/live/search_live/log_event_components_test.exs b/test/logflare_web/live/search_live/log_event_components_test.exs index 9cdf5bb41c..4734ff5d8b 100644 --- a/test/logflare_web/live/search_live/log_event_components_test.exs +++ b/test/logflare_web/live/search_live/log_event_components_test.exs @@ -23,39 +23,6 @@ defmodule LogflareWeb.SearchLive.LogEventComponentsTest do search_op: nil } - defmodule TestLive do - use LogflareWeb, :live_view - - def render(assigns) do - ~H""" -
- -
- """ - end - - def mount(_params, session, socket) do - {:ok, - assign(socket, - search_op_log_events: session["search_op_log_events"], - search_op: session["search_op"], - last_query_completed_at: session["last_query_completed_at"], - loading: session["loading"], - search_timezone: session["search_timezone"], - tailing?: session["tailing?"], - querystring: session["querystring"] - )} - end - end - describe "results_list/1" do setup do user = insert(:user) diff --git a/test/logflare_web/live/search_live/logs_search_lv_test.exs b/test/logflare_web/live/search_live/logs_search_lv_test.exs index 3c8ec9cffe..c014408c73 100644 --- a/test/logflare_web/live/search_live/logs_search_lv_test.exs +++ b/test/logflare_web/live/search_live/logs_search_lv_test.exs @@ -1052,7 +1052,6 @@ defmodule LogflareWeb.Source.SearchLVTest do |> render() assert link =~ ~r/phx-value-log-event-timestamp="\d+/ - assert link =~ ~r/phx-value-lql="\w+/ end @tag source_schema: @@ -1563,6 +1562,43 @@ defmodule LogflareWeb.Source.SearchLVTest do assert get_view_assigns(view).tailing? end + test "closing context does not resume a paused search", %{ + conn: conn, + source: source + } do + {:ok, view, _html} = live_with_redirect(conn, ~p"/sources/#{source.id}/search") + + view |> TestUtils.wait_for_render("#logs-list li:first-of-type a") + assert get_view_assigns(view).tailing? + + render_click(view, "soft_pause", %{}) + refute get_view_assigns(view).tailing? + + render_click(view, "open_event_context", %{}) + + render_click(view, "close_event_context", %{}) + + refute get_view_assigns(view).tailing? + end + + test "closing context resumes a search that was live", %{ + conn: conn, + source: source + } do + {:ok, view, _html} = live_with_redirect(conn, ~p"/sources/#{source.id}/search") + + view |> TestUtils.wait_for_render("#logs-list li:first-of-type a") + assert get_view_assigns(view).tailing? + + render_click(view, "open_event_context", %{}) + + refute get_view_assigns(view).tailing? + + render_click(view, "close_event_context", %{}) + + assert get_view_assigns(view).tailing? + end + test "datetime_update", %{conn: conn, source: source} do {:ok, view, _html} = live_with_redirect(conn, Routes.live_path(conn, SearchLV, source, querystring: "error")) From e5818eafde4638e3af3313b9a2b3cf79a6f02869 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 14 Jul 2026 09:44:50 +1000 Subject: [PATCH 2/3] refactor: render log events with LiveView streams --- lib/logflare/logs/log_event.ex | 1 - lib/logflare/logs/search_query_executor.ex | 14 +-- .../live/search_live/form_components.ex | 1 - .../live/search_live/log_event_components.ex | 14 +-- .../live/search_live/logs_search_lv.ex | 101 ++++++++------- test/logflare/log_event_test.exs | 2 - .../search_live/log_event_components_test.exs | 18 ++- .../live/search_live/logs_search_lv_test.exs | 118 +++++++++++++++++- 8 files changed, 193 insertions(+), 76 deletions(-) diff --git a/lib/logflare/logs/log_event.ex b/lib/logflare/logs/log_event.ex index e0bd3c796a..e6a001ef66 100644 --- a/lib/logflare/logs/log_event.ex +++ b/lib/logflare/logs/log_event.ex @@ -25,7 +25,6 @@ defmodule Logflare.LogEvent do field :body, :map, default: %{} field :valid, :boolean field :drop, :boolean, default: false - field :is_from_stale_query, :boolean field :timestamp_inferred, :boolean, default: false field :ingested_at, :utc_datetime_usec field :source_uuid, Ecto.UUID.Atom diff --git a/lib/logflare/logs/search_query_executor.ex b/lib/logflare/logs/search_query_executor.ex index 2c7ef8251c..6a4837edf8 100644 --- a/lib/logflare/logs/search_query_executor.ex +++ b/lib/logflare/logs/search_query_executor.ex @@ -127,23 +127,11 @@ defmodule Logflare.Logs.SearchQueryExecutor do rows = Enum.map(events_so.rows, &LogEvent.make_from_db(&1, %{source: params.source})) - old_rows = if params.search_op_log_events, do: params.search_op_log_events.rows, else: [] - - # prevents removal of log events loaded - # during initial tailing query - log_events = - old_rows - |> Enum.reject(& &1.is_from_stale_query) - |> Enum.concat(rows) - |> Enum.uniq_by(&{&1.body, &1.id}) - |> Enum.sort_by(& &1.body["timestamp"], &>=/2) - |> Enum.take(100) - send( state.caller, {:search_result, %{ - events: %{events_so | rows: log_events} + events: %{events_so | rows: rows} }} ) diff --git a/lib/logflare_web/live/search_live/form_components.ex b/lib/logflare_web/live/search_live/form_components.ex index dfeeb35893..d9d3cc5f23 100644 --- a/lib/logflare_web/live/search_live/form_components.ex +++ b/lib/logflare_web/live/search_live/form_components.ex @@ -135,7 +135,6 @@ defmodule LogflareWeb.SearchLive.FormComponents do attr :uri_params, :map, required: true attr :lql_rules, :list, required: true attr :user, Logflare.User, required: true - attr :search_op_log_events, :any, default: nil attr :search_op_log_aggregates, :any, default: nil attr :has_results?, :boolean attr :source, Logflare.Sources.Source, required: true diff --git a/lib/logflare_web/live/search_live/log_event_components.ex b/lib/logflare_web/live/search_live/log_event_components.ex index 7d412fca37..4c63b4fc47 100644 --- a/lib/logflare_web/live/search_live/log_event_components.ex +++ b/lib/logflare_web/live/search_live/log_event_components.ex @@ -21,6 +21,8 @@ defmodule LogflareWeb.SearchLive.LogEventComponents do @default_empty_event_message "(empty event message)" attr :search_op_log_events, :map, default: nil + attr :search_op_log_aggregates, :map, default: nil + attr :log_events, :any, default: [] attr :last_query_completed_at, :any, default: nil attr :loading, :boolean, required: true attr :search_timezone, :string, required: true @@ -33,8 +35,9 @@ defmodule LogflareWeb.SearchLive.LogEventComponents do ~H"""
-
    - <.log_event :for={log <- @search_op_log_events.rows} timezone={@search_timezone} log_event={log} select_fields={build_select_fields(@search_op)} source_schema_flat_map={@source_schema_flat_map}> +
      + <.empty_result_list :if={not @loading} search_op_log_events={@search_op_log_events} search_op_log_aggregates={@search_op_log_aggregates} /> + <.log_event :for={{dom_id, log} <- @log_events} id={dom_id} timezone={@search_timezone} log_event={log} select_fields={build_select_fields(@search_op)} source_schema_flat_map={@source_schema_flat_map}> {log.body["event_message"]} <:actions phx-no-format>
      @@ -275,7 +278,7 @@ defmodule LogflareWeb.SearchLive.LogEventComponents do ) ~H""" -
      +