From 104c32a6f21cd53d71ccf9c664e9aa643d29031d Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Fri, 27 Mar 2026 10:09:10 +1000 Subject: [PATCH 01/44] refactor: backend execute_query/3 returns Adaptor.QueryResult From d4bb1da1805a5ace4b0324c3d4c374d5bac3ab75 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Fri, 27 Mar 2026 11:53:29 +1000 Subject: [PATCH 02/44] feat: PostgresAdaptor include total_rows in result --- .../backends/adaptor/postgres_adaptor.ex | 35 +++++++++++-------- .../adaptor/postgres_adaptor_test.exs | 6 ++-- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/logflare/backends/adaptor/postgres_adaptor.ex b/lib/logflare/backends/adaptor/postgres_adaptor.ex index 2304c5d6c..571ea55e3 100644 --- a/lib/logflare/backends/adaptor/postgres_adaptor.ex +++ b/lib/logflare/backends/adaptor/postgres_adaptor.ex @@ -96,7 +96,7 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptor do |> mod.all() |> Enum.map(&nested_map_update/1) - {:ok, QueryResult.new(result)} + {:ok, QueryResult.new(result, pg_meta(result))} end def execute_query(%Backend{} = backend, query_string, opts) @@ -106,20 +106,20 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptor do def execute_query(%Backend{} = backend, {query_string, params}, _opts) when is_non_empty_binary(query_string) and is_list(params) do - {:ok, result} = - SharedRepo.with_repo(backend, fn -> - SharedRepo.query(query_string, params) - end) - - rows = - for row <- result.rows do - result.columns - |> Enum.zip(row) - |> Map.new() - |> nested_map_update() - end - - {:ok, QueryResult.new(rows)} + with {:ok, result} <- + SharedRepo.with_repo(backend, fn -> + SharedRepo.query(query_string, params) + end) do + rows = + for row <- result.rows do + result.columns + |> Enum.zip(row) + |> Map.new() + |> nested_map_update() + end + + {:ok, QueryResult.new(rows, pg_meta(rows))} + end end def execute_query(%Backend{} = backend, {query_string, declared_params, input_params}, opts) @@ -243,6 +243,11 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptor do SingleTenant.supabase_mode?() and SingleTenant.postgres_backend?() end + @spec pg_meta(list()) :: map() + defp pg_meta(rows) when is_list(rows) do + %{total_rows: length(rows)} + end + @spec nested_map_update(term()) :: term() defp nested_map_update(value) when is_struct(value), do: value diff --git a/test/logflare/backends/adaptor/postgres_adaptor_test.exs b/test/logflare/backends/adaptor/postgres_adaptor_test.exs index 5b1bba1c8..270e1e48a 100644 --- a/test/logflare/backends/adaptor/postgres_adaptor_test.exs +++ b/test/logflare/backends/adaptor/postgres_adaptor_test.exs @@ -71,12 +71,12 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptorTest do query = from(l in PostgresAdaptor.table_name(source), select: l.body) TestUtils.retry_assert(fn -> - assert {:ok, %QueryResult{rows: [%{"test" => "data"}]}} = + assert {:ok, %QueryResult{rows: [%{"test" => "data"}], total_rows: 1}} = PostgresAdaptor.execute_query(backend, query, []) end) # query by string - assert {:ok, %QueryResult{rows: [%{"body" => [%{"test" => "data"}]}]}} = + assert {:ok, %QueryResult{rows: [%{"body" => [%{"test" => "data"}]}], total_rows: 1}} = PostgresAdaptor.execute_query( backend, "select body from #{PostgresAdaptor.table_name(source)}", @@ -84,7 +84,7 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptorTest do ) # query by string with parameter - assert {:ok, %QueryResult{rows: [%{"value" => "data"}]}} = + assert {:ok, %QueryResult{rows: [%{"value" => "data"}], total_rows: 1}} = PostgresAdaptor.execute_query( backend, {"select body ->> $1 as value from #{PostgresAdaptor.table_name(source)}", From c4ddf4d385829cf94d2d7c2397792d17f65d1785 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 24 Mar 2026 09:39:21 +1000 Subject: [PATCH 03/44] refactor: decouple SearchLV LQL from BigQuery TableSchema From 8a3c1d34bb3e0177d079812d0b427eaa90dbf565 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 24 Mar 2026 10:00:12 +1000 Subject: [PATCH 04/44] fix: dialyzer warnings From 5591e814681c4142ac6f3452bdcc41f905c7e480 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 24 Mar 2026 14:35:53 +1000 Subject: [PATCH 05/44] feat: add Postgres support to SearchOperations --- lib/logflare/logs/search_operation.ex | 16 +- lib/logflare/logs/search_operations.ex | 406 ++++++++++++++++--------- test/logflare/logs/search_test.exs | 90 +++++- test/logflare/lql/integration_test.exs | 2 +- 4 files changed, 371 insertions(+), 143 deletions(-) diff --git a/lib/logflare/logs/search_operation.ex b/lib/logflare/logs/search_operation.ex index 308fc12d5..26acb1225 100644 --- a/lib/logflare/logs/search_operation.ex +++ b/lib/logflare/logs/search_operation.ex @@ -4,6 +4,7 @@ defmodule Logflare.Logs.SearchOperation do """ use TypedStruct + alias Logflare.Backends alias Logflare.Lql.Rules, as: LqlRules alias Logflare.Lql.Rules.ChartRule alias Logflare.Lql.Rules.FilterRule @@ -14,6 +15,7 @@ defmodule Logflare.Logs.SearchOperation do field :source, Source.t() field :source_token, atom() field :source_id, number() + field :backend_type, :bigquery | :postgres, default: :bigquery field :partition_by, :pseudo | :timestamp, enforce: true field :querystring, String.t(), enforce: true field :query, Ecto.Query.t() @@ -48,7 +50,19 @@ defmodule Logflare.Logs.SearchOperation do chart_rules: chart_rules, lql_ts_filters: ts_filters, source_token: so.source.token, - source_id: so.source.id + source_id: so.source.id, + backend_type: resolve_backend_type(so) } end + + @spec resolve_backend_type(t()) :: :bigquery | :postgres + defp resolve_backend_type(%__MODULE__{source: %{user: user}}) + when is_struct(user, Logflare.User) do + case Backends.get_default_backend(user) do + %{type: :postgres} -> :postgres + _ -> :bigquery + end + end + + defp resolve_backend_type(%__MODULE__{}), do: :bigquery end diff --git a/lib/logflare/logs/search_operations.ex b/lib/logflare/logs/search_operations.ex index 53e660483..db48f3546 100644 --- a/lib/logflare/logs/search_operations.ex +++ b/lib/logflare/logs/search_operations.ex @@ -5,7 +5,9 @@ defmodule Logflare.Logs.SearchOperations do import Logflare.Ecto.BQQueryAPI import Logflare.Logs.SearchQueries + alias Logflare.Backends alias Logflare.Backends.Adaptor.BigQueryAdaptor + alias Logflare.Backends.Adaptor.PostgresAdaptor alias Logflare.Backends.Adaptor.QueryResult alias Logflare.DateTimeUtils alias Logflare.Google.BigQuery.GCPConfig @@ -15,6 +17,7 @@ defmodule Logflare.Logs.SearchOperations do alias Logflare.Logs.SearchUtils alias Logflare.Lql alias Logflare.Lql.BackendTransformer.BigQuery, as: BigQueryTransformer + alias Logflare.Lql.BackendTransformer.Postgres, as: PostgresTransformer alias Logflare.Lql.Rules alias Logflare.Lql.Rules.ChartRule alias Logflare.Lql.Rules.FilterRule @@ -45,32 +48,40 @@ defmodule Logflare.Logs.SearchOperations do @spec do_query(SO.t()) :: SO.t() def do_query(%SO{} = so) do - bq_project_id = so.source.user.bigquery_project_id || GCPConfig.default_project_id() - %{bigquery_dataset_id: dataset_id} = GenUtils.get_bq_user_info(so.source.token) - - with {:ok, response} <- - BigQueryAdaptor.execute_query( - {bq_project_id, dataset_id, so.source.user.id}, - so.query, - query_type: :search - ) do + with {:ok, response} <- execute_backend_query(so) do so |> SearchUtils.put_result(:query_result, response) |> SearchUtils.put_result(:rows, response.rows) - |> put_sql_string_and_params(response) + |> put_sql_string(response) else {:error, err} -> SearchUtils.put_result(so, :error, err) end end - @spec put_sql_string_and_params(SO.t(), QueryResult.t()) :: - SO.t() - defp put_sql_string_and_params(%{sql_string: sql_string} = so, _response) - when is_binary(sql_string), - do: so + @spec execute_backend_query(SO.t()) :: {:ok, map()} | {:error, term()} + defp execute_backend_query(%SO{backend_type: :postgres} = so) do + backend = postgres_backend(so) + + PostgresAdaptor.execute_query(backend, so.query, query_type: :search) + end + + defp execute_backend_query(%SO{} = so) do + bq_project_id = so.source.user.bigquery_project_id || GCPConfig.default_project_id() + %{bigquery_dataset_id: dataset_id} = GenUtils.get_bq_user_info(so.source.token) - defp put_sql_string_and_params(so, %QueryResult{ + BigQueryAdaptor.execute_query( + {bq_project_id, dataset_id, so.source.user.id}, + so.query, + query_type: :search + ) + end + + @spec put_sql_string(SO.t(), QueryResult.t()) :: SO.t() + defp put_sql_string(%{sql_string: sql_string} = so, _response) when is_binary(sql_string), + do: so + + defp put_sql_string(so, %QueryResult{ query_string: query_string, bq_params: bq_params }) do @@ -81,10 +92,17 @@ defmodule Logflare.Logs.SearchOperations do } end + defp put_sql_string(%SO{} = so, _response) do + case PostgresAdaptor.ecto_to_sql(so.query, []) do + {:ok, {query_string, params}} -> %{so | sql_string: query_string, sql_params: params} + {:error, _reason} -> so + end + end + @spec apply_query_defaults(SO.t()) :: SO.t() def apply_query_defaults(%SO{} = so) do query = - from(so.source.bq_table_id) + from(table_name(so)) |> select(%{}) |> order_by([t], desc: t.timestamp) |> limit(@default_limit) @@ -94,7 +112,7 @@ defmodule Logflare.Logs.SearchOperations do @spec apply_halt_conditions(SO.t()) :: SO.t() def apply_halt_conditions(%SO{} = so) do - chart_period = hd(so.chart_rules).period + chart_period = chart_period(so) %{min: min_ts, max: max_ts} = SearchOperationHelpers.get_min_max_filter_timestamps(so.lql_ts_filters, chart_period) @@ -129,7 +147,7 @@ defmodule Logflare.Logs.SearchOperations do %{message: message} = SearchOperationHelpers.get_min_max_filter_timestamps( so.lql_ts_filters, - hd(so.chart_rules).period + chart_period(so) ) if message do @@ -191,15 +209,23 @@ defmodule Logflare.Logs.SearchOperations do def process_query_result(%SO{query_result: %QueryResult{rows: rows}, type: :aggregates} = so) do rows = Enum.map(rows, fn agg -> - Map.put(agg, "datetime", Timex.from_unix(agg["timestamp"], :microsecond)) + timestamp = normalize_aggregate_timestamp(agg["timestamp"]) + + agg + |> Map.put("timestamp", timestamp) + |> Map.put("datetime", Timex.from_unix(timestamp, :microsecond)) end) %{so | rows: rows} end + def apply_timestamp_filter_rules(%SO{backend_type: :postgres, type: :events} = so) do + %{so | query: apply_postgres_event_timestamp_filter_rules(so)} + end + def apply_timestamp_filter_rules(%SO{type: :events} = so) do %SO{tailing?: t?, tailing_initial?: ti?, query: query} = so - chart_period = hd(so.chart_rules).period + chart_period = chart_period(so) utc_today = Date.utc_today() ts_filters = so.lql_ts_filters @@ -273,80 +299,80 @@ defmodule Logflare.Logs.SearchOperations do @spec apply_timestamp_filter_rules(SO.t()) :: SO.t() def apply_timestamp_filter_rules(%SO{tailing?: t?, type: :aggregates} = so) do - query = from(so.source.bq_table_id) - ts_filters = so.lql_ts_filters + query = from(table_name(so)) + chart_period = chart_period(so) + filters = if(t? or Enum.empty?(so.lql_ts_filters), do: [], else: so.lql_ts_filters) + + q = + case so.backend_type do + :postgres -> + %{min: min, max: max} = + SearchOperationHelpers.get_min_max_filter_timestamps(filters, chart_period) - period = - so.chart_rules - |> hd() - |> Map.get(:period) - |> to_bq_interval_token() + query = where(query, [t], t.timestamp >= ^min and t.timestamp <= ^max) - tick_count = - so.chart_rules - |> hd() - |> Map.get(:period) - |> SearchOperationHelpers.default_period_tick_count() + if Enum.empty?(filters), + do: query, + else: Lql.apply_filter_rules(query, filters, dialect: :postgres) + + :bigquery -> + apply_bq_aggregate_timestamp_filters(query, so, filters, chart_period) + end + %{so | query: q} + end + + defp apply_bq_aggregate_timestamp_filters(query, so, filters, chart_period) do + period = to_bq_interval_token(chart_period) + tick_count = SearchOperationHelpers.default_period_tick_count(chart_period) utc_today = Date.utc_today() utc_now = DateTime.utc_now() partition_days = - case hd(so.chart_rules).period do + case chart_period do :day -> 14 :hour -> 3 :minute -> 1 :second -> 1 end - q = - if t? or Enum.empty?(ts_filters) do - query = - query - |> BigQueryTransformer.where_timestamp_ago( - utc_now, - tick_count, - period + if Enum.empty?(filters) do + query = + query + |> BigQueryTransformer.where_timestamp_ago(utc_now, tick_count, period) + |> limit([t], ^tick_count) + + case so.partition_by do + :pseudo -> + where( + query, + partition_date() >= bq_date_sub(^utc_today, ^partition_days, "day") or + in_streaming_buffer() ) - |> limit([t], ^tick_count) + :timestamp -> + query + end + else + %{min: min, max: max} = + SearchOperationHelpers.get_min_max_filter_timestamps(filters, chart_period) + + query = case so.partition_by do :pseudo -> - where( - query, - partition_date() >= bq_date_sub(^utc_today, ^partition_days, "day") or - in_streaming_buffer() + query + |> where( + partition_date() >= ^Timex.to_date(min) and + partition_date() <= ^Timex.to_date(max) ) + |> or_where(in_streaming_buffer()) :timestamp -> query end - else - %{min: min, max: max} = - SearchOperationHelpers.get_min_max_filter_timestamps( - ts_filters, - hd(so.chart_rules).period - ) - query = - case so.partition_by do - :pseudo -> - query - |> where( - partition_date() >= ^Timex.to_date(min) and - partition_date() <= ^Timex.to_date(max) - ) - |> or_where(in_streaming_buffer()) - - :timestamp -> - query - end - - query - |> Lql.apply_filter_rules(ts_filters) - end - - %{so | query: q} + Lql.apply_filter_rules(query, filters) + end end defp to_value_unit(average) when average < 10, do: {2, "DAY"} @@ -370,13 +396,13 @@ defmodule Logflare.Logs.SearchOperations do |> Kernel.++(default_rules) |> Rules.SelectRule.normalize() - q = Lql.apply_select_rules(q, select_rules) + q = Lql.apply_select_rules(q, select_rules, dialect: so.backend_type) %{so | query: q} end def apply_filters(%SO{type: :events, query: q} = so) do - q = Lql.apply_filter_rules(q, so.lql_meta_and_msg_filters) + q = Lql.apply_filter_rules(q, so.lql_meta_and_msg_filters, dialect: so.backend_type) %{so | query: q} end @@ -449,11 +475,31 @@ defmodule Logflare.Logs.SearchOperations do %{so | lql_ts_filters: lql_ts_filters} end - def apply_numeric_aggs( - %SO{query: query, chart_rules: chart_rules, lql_meta_and_msg_filters: filter_rules} = so - ) do - chart_period = hd(so.chart_rules).period - chart_path = hd(chart_rules).path + def apply_numeric_aggs(%SO{query: query, lql_meta_and_msg_filters: filter_rules} = so) do + chart_rule = hd(so.chart_rules) + + case so.backend_type do + :postgres -> + query = + query + |> Lql.apply_filter_rules(filter_rules, dialect: :postgres) + |> PostgresTransformer.transform_chart_rule( + chart_rule.aggregate, + chart_rule.path, + chart_rule.period, + "timestamp" + ) + + %{so | query: query} + + :bigquery -> + apply_bq_numeric_aggs(so, query, chart_rule, filter_rules) + end + end + + defp apply_bq_numeric_aggs(so, query, chart_rule, filter_rules) do + chart_period = chart_rule.period + chart_path = chart_rule.path non_chart_filters = Enum.reject(filter_rules, fn %FilterRule{path: path} -> path == chart_path end) @@ -464,87 +510,167 @@ defmodule Logflare.Logs.SearchOperations do |> order_by([t, ...], desc: 1) query = select_timestamp(query, chart_period) + query = apply_bq_chart_select(query, chart_rule, so.chart_data_shape_id, filter_rules) + query = group_by(query, 1) - query = - case chart_rules do - [%ChartRule{path: "timestamp", aggregate: :count, value_type: :datetime}] -> - case so.chart_data_shape_id do - :elixir_logger_levels -> - select_count_log_level(query) + %{so | query: query} + end - :cloudflare_status_codes -> - select_count_cloudflare_http_status_code(query) + defp apply_bq_chart_select( + query, + %ChartRule{path: "timestamp", aggregate: :count, value_type: :datetime}, + chart_data_shape_id, + _filter_rules + ) do + case chart_data_shape_id do + :elixir_logger_levels -> select_count_log_level(query) + :cloudflare_status_codes -> select_count_cloudflare_http_status_code(query) + :vercel_status_codes -> select_count_vercel_http_status_code(query) + :netlify_status_codes -> select_count_netlify_http_status_code(query) + nil -> select_merge_agg_value(query, :count, :timestamp) + end + end - :vercel_status_codes -> - select_count_vercel_http_status_code(query) + defp apply_bq_chart_select( + query, + %ChartRule{path: p, aggregate: agg}, + _chart_data_shape_id, + filter_rules + ) do + last_chart_field = + p + |> String.split(".") + |> List.last() + |> String.to_existing_atom() - :netlify_status_codes -> - select_count_netlify_http_status_code(query) + q = + if String.contains?(p, ".") do + query + |> Lql.handle_nested_field_access(p) + |> select_merge_agg_value(agg, last_chart_field, :joined_table) + else + query + |> select_merge_agg_value(agg, last_chart_field, :base_table) + end - nil -> - select_merge_agg_value(query, :count, :timestamp) - end + Enum.reduce(filter_rules, q, fn + %FilterRule{path: ^p, operator: operator, value: value, modifiers: modifiers}, acc -> + where( + acc, + ^Lql.transform_filter_rule(%FilterRule{ + path: p, + operator: operator, + value: value, + modifiers: modifiers + }) + ) + + %FilterRule{}, acc -> + acc + end) + end - [%ChartRule{value_type: _, path: p, aggregate: agg}] -> - last_chart_field = - p - |> String.split(".") - |> List.last() - |> String.to_existing_atom() + @spec chart_period(SO.t()) :: chart_period() + defp chart_period(%SO{chart_rules: [%{period: period} | _]}), do: period - # Only create UNNEST joins for nested fields (containing ".") - # Top-level fields should reference the base table directly - is_nested_field = String.contains?(p, ".") + @spec table_name(SO.t()) :: String.t() + defp table_name(%SO{backend_type: :postgres, source: source}), + do: PostgresAdaptor.table_name(source) - q = - if is_nested_field do - query - |> Lql.handle_nested_field_access(p) - |> select_merge_agg_value(agg, last_chart_field, :joined_table) - else - query - |> select_merge_agg_value(agg, last_chart_field, :base_table) - end + defp table_name(%SO{source: source}), do: source.bq_table_id - Enum.reduce(filter_rules, q, fn - %FilterRule{ - path: ^p, - operator: operator, - value: value, - modifiers: modifiers - }, - acc -> - where( - acc, - ^Lql.transform_filter_rule(%FilterRule{ - path: p, - operator: operator, - value: value, - modifiers: modifiers - }) - ) + defp postgres_backend(%SO{source: %{user: user}}) when not is_nil(user) do + Backends.get_default_backend(user) + end - %FilterRule{}, acc -> - acc - end) - end + defp postgres_backend(%SO{}), do: nil - query = group_by(query, 1) + @spec apply_postgres_event_timestamp_filter_rules(SO.t()) :: Ecto.Query.t() + defp apply_postgres_event_timestamp_filter_rules(%SO{} = so) do + %SO{tailing?: t?, tailing_initial?: ti?, query: query} = so + ts_filters = so.lql_ts_filters - %{so | query: query} + cond do + t? and !ti? -> + where( + query, + [t], + t.timestamp >= + ^Timex.shift(DateTime.utc_now(), minutes: -@tailing_timestamp_filter_minutes) + ) + + (t? and ti?) || Enum.empty?(ts_filters) -> + where(query, [t], t.timestamp >= ^Timex.shift(DateTime.utc_now(), days: -2)) + + true -> + Lql.apply_filter_rules(query, ts_filters, dialect: :postgres) + end end + @spec normalize_postgres_response([term()], :events | :aggregates) :: map() + defp normalize_postgres_response(rows, type) do + normalized_rows = Enum.map(rows, &normalize_postgres_row(&1, type)) + + %{ + rows: normalized_rows, + total_rows: length(normalized_rows), + total_bytes_processed: 0 + } + end + + defp normalize_postgres_row(%{} = row, :events) do + Map.new(row, fn {key, value} -> + {to_string(key), normalize_postgres_value(key, value)} + end) + end + + defp normalize_postgres_row(%{} = row, :aggregates) do + row + |> Enum.reduce(%{}, fn {key, value}, acc -> + key = to_string(key) + key = if key == "count", do: "value", else: key + Map.put(acc, key, normalize_postgres_value(key, value)) + end) + end + + defp normalize_postgres_row(other, _type), do: other + + defp normalize_postgres_value(_key, %DateTime{} = value), + do: DateTime.to_unix(value, :microsecond) + + defp normalize_postgres_value(_key, %NaiveDateTime{} = value) do + value + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix(:microsecond) + end + + defp normalize_postgres_value(_key, %Date{} = value), do: Date.to_iso8601(value) + + defp normalize_postgres_value(_key, %{} = value) do + Map.new(value, fn {nested_key, nested_value} -> + {to_string(nested_key), normalize_postgres_value(nested_key, nested_value)} + end) + end + + defp normalize_postgres_value(_key, value) when is_list(value) do + Enum.map(value, &normalize_postgres_value(nil, &1)) + end + + defp normalize_postgres_value(_key, value), do: value + + defp normalize_aggregate_timestamp([timestamp]), do: normalize_aggregate_timestamp(timestamp) + defp normalize_aggregate_timestamp(timestamp), do: timestamp + def add_missing_agg_timestamps(%SO{} = so) do + chart_period = chart_period(so) + %{min: min, max: max} = - SearchOperationHelpers.get_min_max_filter_timestamps( - so.lql_ts_filters, - hd(so.chart_rules).period - ) + SearchOperationHelpers.get_min_max_filter_timestamps(so.lql_ts_filters, chart_period) if min == max do so else - rows = intersperse_missing_range_timestamps(so.rows, min, max, hd(so.chart_rules).period) + rows = intersperse_missing_range_timestamps(so.rows, min, max, chart_period) %{so | rows: rows} end diff --git a/test/logflare/logs/search_test.exs b/test/logflare/logs/search_test.exs index fa1b9fd3f..d148d8e10 100644 --- a/test/logflare/logs/search_test.exs +++ b/test/logflare/logs/search_test.exs @@ -1,11 +1,16 @@ defmodule Logflare.Logs.SearchTest do - use Logflare.DataCase, async: true + use Logflare.DataCase, async: false + alias Logflare.Backends alias Logflare.Backends.Adaptor.BigQueryAdaptor + alias Logflare.Backends.Adaptor.PostgresAdaptor + alias Logflare.Google.BigQuery.SchemaUtils alias Logflare.Logs.Search alias Logflare.Logs.SearchOperation, as: SO alias Logflare.Lql.Rules.ChartRule alias Logflare.Lql.Rules.FilterRule + alias Logflare.SingleTenant + alias Logflare.SourceSchemas setup do insert(:plan, name: "Free", type: "standard") @@ -93,4 +98,87 @@ defmodule Logflare.Logs.SearchTest do assert sql =~ "STRPOS(t0.event_message" end end + + describe "postgres single tenant integration" do + TestUtils.setup_single_tenant(seed_user: true, backend_type: :postgres) + + setup do + start_supervised!(Logflare.SystemMetricsSup) + + user = SingleTenant.get_default_user() + source = insert(:source, user: user) + + assert :ok = Backends.ensure_source_sup_started(source) + + matching_message = "postgres-search-match-#{System.unique_integer([:positive])}" + non_matching_message = "postgres-search-miss-#{System.unique_integer([:positive])}" + + assert {:ok, 2} = + Backends.ingest_logs( + [ + %{"event_message" => matching_message}, + %{"event_message" => non_matching_message} + ], + source + ) + + schema = TestUtils.build_bq_schema(%{"event_message" => matching_message}) + + assert {:ok, _source_schema} = + SourceSchemas.create_or_update_source_schema(source, %{ + bigquery_schema: schema, + schema_flat_map: SchemaUtils.bq_schema_to_flat_typemap(schema) + }) + + Cachex.clear(Logflare.SourceSchemas.Cache) + + backend = Backends.get_default_backend(user) + + TestUtils.retry_assert(fn -> + assert {:ok, rows} = + PostgresAdaptor.execute_query( + backend, + "select event_message from #{PostgresAdaptor.table_name(source)} order by timestamp desc", + [] + ) + + assert Enum.any?(rows, &(&1["event_message"] == matching_message)) + assert Enum.any?(rows, &(&1["event_message"] == non_matching_message)) + end) + + %{ + user: user, + source: source, + matching_message: matching_message, + schema: schema + } + end + + test "event search filters postgres rows by event_message", %{ + source: source, + matching_message: matching_message, + schema: schema + } do + lql_rules = + matching_message + |> Logflare.Lql.decode!(schema) + |> Logflare.Lql.Rules.put_new_chart_rule(Logflare.Lql.Rules.default_chart_rule()) + + search_operation = + SO.new(%{ + source: source, + querystring: matching_message, + lql_rules: lql_rules, + chart_data_shape_id: nil, + tailing?: false, + type: :events, + partition_by: :timestamp + }) + + TestUtils.retry_assert(fn -> + assert {:ok, %{events: events_so}} = Search.search(search_operation) + assert [%{"event_message" => ^matching_message}] = events_so.rows + end) + end + end end diff --git a/test/logflare/lql/integration_test.exs b/test/logflare/lql/integration_test.exs index 62c7b5867..504c33897 100644 --- a/test/logflare/lql/integration_test.exs +++ b/test/logflare/lql/integration_test.exs @@ -15,7 +15,7 @@ defmodule Logflare.Lql.IntegrationTest do describe "end-to-end LQL processing" do setup do user = insert(:user) - source = insert(:source, user_id: user.id) + source = insert(:source, user: user) {:ok, source: source, user: user} end From 9e95727b9288f5ae4ff413ee840907eb17754b0f Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 24 Mar 2026 15:37:39 +1000 Subject: [PATCH 06/44] style: alias nested modules (credo) --- test/logflare/logs/search_test.exs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/logflare/logs/search_test.exs b/test/logflare/logs/search_test.exs index d148d8e10..34698d48d 100644 --- a/test/logflare/logs/search_test.exs +++ b/test/logflare/logs/search_test.exs @@ -7,6 +7,8 @@ defmodule Logflare.Logs.SearchTest do alias Logflare.Google.BigQuery.SchemaUtils alias Logflare.Logs.Search alias Logflare.Logs.SearchOperation, as: SO + alias Logflare.Lql + alias Logflare.Lql.Rules alias Logflare.Lql.Rules.ChartRule alias Logflare.Lql.Rules.FilterRule alias Logflare.SingleTenant @@ -161,8 +163,8 @@ defmodule Logflare.Logs.SearchTest do } do lql_rules = matching_message - |> Logflare.Lql.decode!(schema) - |> Logflare.Lql.Rules.put_new_chart_rule(Logflare.Lql.Rules.default_chart_rule()) + |> Lql.decode!(schema) + |> Rules.put_new_chart_rule(Rules.default_chart_rule()) search_operation = SO.new(%{ From 0bcb70a0a76e6311772b47d672ca8799db50af89 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Wed, 25 Mar 2026 09:36:54 +1000 Subject: [PATCH 07/44] implement PostgresAdaptor.test_connection/1 --- lib/logflare/backends/adaptor/postgres_adaptor.ex | 9 +++++++++ .../backends/adaptor/postgres_adaptor_test.exs | 15 +++++++++++++++ test/logflare/logs/search_test.exs | 10 +--------- test/test_helper.exs | 1 + 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/logflare/backends/adaptor/postgres_adaptor.ex b/lib/logflare/backends/adaptor/postgres_adaptor.ex index 571ea55e3..710382557 100644 --- a/lib/logflare/backends/adaptor/postgres_adaptor.ex +++ b/lib/logflare/backends/adaptor/postgres_adaptor.ex @@ -139,6 +139,15 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptor do execute_query(backend, {query_string, declared_params, input_params}, opts) end + @impl Logflare.Backends.Adaptor + @spec test_connection(Backend.t()) :: :ok | {:error, term()} + def test_connection(%Backend{} = backend) do + case execute_query(backend, "SELECT 1 AS result", []) do + {:ok, [%{"result" => 1}]} -> :ok + {:error, _} = error -> error + end + end + @impl Logflare.Backends.Adaptor def ecto_to_sql(%Ecto.Query{} = query, _opts) do SqlUtils.ecto_to_pg_sql(query) diff --git a/test/logflare/backends/adaptor/postgres_adaptor_test.exs b/test/logflare/backends/adaptor/postgres_adaptor_test.exs index 270e1e48a..ff27c26c6 100644 --- a/test/logflare/backends/adaptor/postgres_adaptor_test.exs +++ b/test/logflare/backends/adaptor/postgres_adaptor_test.exs @@ -6,6 +6,7 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptorTest do alias Logflare.Backends alias Logflare.Backends.Adaptor alias Logflare.Backends.Adaptor.PostgresAdaptor + alias Logflare.Backends.Adaptor.PostgresAdaptor.SharedRepo alias Logflare.Backends.AdaptorSupervisor alias Logflare.Backends.Adaptor.QueryResult alias Logflare.SystemMetrics.AllLogsLogged @@ -155,6 +156,20 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptorTest do end end + describe "test_connection/1" do + test "test_connection/1 returns :ok", %{backend: backend} do + assert :ok = PostgresAdaptor.test_connection(backend) + end + + test "returns error when connection fails", %{backend: backend} do + Mimic.expect(SharedRepo, :with_repo, fn _backend, _func -> + {:error, :cannot_connect} + end) + + assert {:error, :cannot_connect} = PostgresAdaptor.test_connection(backend) + end + end + describe "separate config fields" do test "special characters as password", %{source: source} do config = %{ diff --git a/test/logflare/logs/search_test.exs b/test/logflare/logs/search_test.exs index 34698d48d..957d4f2d5 100644 --- a/test/logflare/logs/search_test.exs +++ b/test/logflare/logs/search_test.exs @@ -137,15 +137,7 @@ defmodule Logflare.Logs.SearchTest do backend = Backends.get_default_backend(user) TestUtils.retry_assert(fn -> - assert {:ok, rows} = - PostgresAdaptor.execute_query( - backend, - "select event_message from #{PostgresAdaptor.table_name(source)} order by timestamp desc", - [] - ) - - assert Enum.any?(rows, &(&1["event_message"] == matching_message)) - assert Enum.any?(rows, &(&1["event_message"] == non_matching_message)) + assert :ok = PostgresAdaptor.test_connection(backend) end) %{ diff --git a/test/test_helper.exs b/test/test_helper.exs index 06e128b14..d81bfdeba 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -36,6 +36,7 @@ Mimic.copy(Logflare.Backends.Adaptor.BigQueryAdaptor) Mimic.copy(Logflare.Backends.Adaptor.BigQueryAdaptor.GoogleApiClient) Mimic.copy(Logflare.Cluster.Utils) Mimic.copy(Logflare.ContextCache) +Mimic.copy(Logflare.Backends.Adaptor.PostgresAdaptor.SharedRepo) Mimic.copy(Logflare.Backends.Adaptor.ClickHouseAdaptor) Mimic.copy(Logflare.Backends.Adaptor.ClickHouseAdaptor.ConnectionManager) Mimic.copy(Logflare.Backends.Adaptor.ClickHouseAdaptor.NativeIngester.Pool) From 13c03b617f1c6e5109677f811ad561980db73dbc Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Wed, 25 Mar 2026 10:57:27 +1000 Subject: [PATCH 08/44] fix: postgres adaptor aggregates --- lib/logflare/logs/search_operations.ex | 17 ++-- test/logflare/logs/search_operations_test.exs | 86 +++++++++++++++++++ .../lql/backend_transformer/postgres_test.exs | 16 ++++ 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/lib/logflare/logs/search_operations.ex b/lib/logflare/logs/search_operations.ex index db48f3546..dba3dfb04 100644 --- a/lib/logflare/logs/search_operations.ex +++ b/lib/logflare/logs/search_operations.ex @@ -478,11 +478,13 @@ defmodule Logflare.Logs.SearchOperations do def apply_numeric_aggs(%SO{query: query, lql_meta_and_msg_filters: filter_rules} = so) do chart_rule = hd(so.chart_rules) + non_chart_filters = reject_chart_filters(filter_rules, chart_rule) + case so.backend_type do :postgres -> query = query - |> Lql.apply_filter_rules(filter_rules, dialect: :postgres) + |> Lql.apply_filter_rules(non_chart_filters) |> PostgresTransformer.transform_chart_rule( chart_rule.aggregate, chart_rule.path, @@ -493,16 +495,12 @@ defmodule Logflare.Logs.SearchOperations do %{so | query: query} :bigquery -> - apply_bq_numeric_aggs(so, query, chart_rule, filter_rules) + apply_bq_numeric_aggs(so, query, chart_rule, non_chart_filters, filter_rules) end end - defp apply_bq_numeric_aggs(so, query, chart_rule, filter_rules) do + defp apply_bq_numeric_aggs(so, query, chart_rule, non_chart_filters, filter_rules) do chart_period = chart_rule.period - chart_path = chart_rule.path - - non_chart_filters = - Enum.reject(filter_rules, fn %FilterRule{path: path} -> path == chart_path end) query = query @@ -661,6 +659,11 @@ defmodule Logflare.Logs.SearchOperations do defp normalize_aggregate_timestamp([timestamp]), do: normalize_aggregate_timestamp(timestamp) defp normalize_aggregate_timestamp(timestamp), do: timestamp + @spec reject_chart_filters([FilterRule.t()], ChartRule.t()) :: [FilterRule.t()] + defp reject_chart_filters(filter_rules, %ChartRule{path: chart_path}) do + Enum.reject(filter_rules, fn %FilterRule{path: path} -> path == chart_path end) + end + def add_missing_agg_timestamps(%SO{} = so) do chart_period = chart_period(so) diff --git a/test/logflare/logs/search_operations_test.exs b/test/logflare/logs/search_operations_test.exs index f15f4fac2..0c0a94001 100644 --- a/test/logflare/logs/search_operations_test.exs +++ b/test/logflare/logs/search_operations_test.exs @@ -5,6 +5,7 @@ defmodule Logflare.Logs.SearchOperationsTest do import Logflare.Utils.Guards alias Logflare.Backends.Adaptor.BigQueryAdaptor + alias Logflare.Backends.Adaptor.PostgresAdaptor alias Logflare.Backends.Adaptor.QueryResult alias Logflare.Logs.SearchOperation, as: SO alias Logflare.Logs.SearchOperations @@ -175,6 +176,91 @@ defmodule Logflare.Logs.SearchOperationsTest do end end + describe "postgres chart aggregation" do + setup do + insert(:plan) + user = insert(:user) + source = insert(:source, user: user) + + base_so = %SO{ + source: source, + querystring: "", + chart_data_shape_id: nil, + tailing?: false, + partition_by: :timestamp, + type: :aggregates, + backend_type: :postgres, + lql_ts_filters: [], + lql_meta_and_msg_filters: [] + } + + [base_so: base_so] + end + + test "chart-path filters are excluded from WHERE clause", %{base_so: base_so} do + chart_filter = %FilterRule{ + path: "event_message", + operator: :string_contains, + value: "metrics", + modifiers: %{} + } + + non_chart_filter = %FilterRule{ + path: "metadata.level", + operator: :=, + value: "error", + modifiers: %{} + } + + chart_rule = %ChartRule{ + path: "event_message", + aggregate: :count, + period: :minute, + value_type: :string + } + + so = %{ + base_so + | chart_rules: [chart_rule], + lql_meta_and_msg_filters: [chart_filter, non_chart_filter], + query: from("test_table") + } + + so = SearchOperations.apply_numeric_aggs(so) + + assert length(so.query.wheres) == 1 + end + + test "generates expected SQL for all aggregate types", %{base_so: base_so} do + for agg <- [:count, :avg, :sum, :max] do + chart_rule = %ChartRule{ + path: "event_message", + aggregate: agg, + period: :minute, + value_type: :string + } + + so = %{ + base_so + | chart_rules: [chart_rule], + query: from("test_table") + } + + so = SearchOperations.apply_numeric_aggs(so) + {:ok, {sql, _params}} = PostgresAdaptor.ecto_to_sql(so.query, []) + sql = String.downcase(sql) + + expected_aggregate_sql = + case agg do + :count -> ~s|count(t0."timestamp")| + _ -> ~s|#{agg}(t0."event_message")| + end + + assert sql =~ expected_aggregate_sql + end + end + end + describe "apply_select_rules/1" do setup do insert(:plan) diff --git a/test/logflare/lql/backend_transformer/postgres_test.exs b/test/logflare/lql/backend_transformer/postgres_test.exs index 7c0b82be6..964ae1ccc 100644 --- a/test/logflare/lql/backend_transformer/postgres_test.exs +++ b/test/logflare/lql/backend_transformer/postgres_test.exs @@ -231,6 +231,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do assert result.group_bys != [] assert result.order_bys != [] assert result.select != nil + assert_select_keys(result, [:timestamp, :count]) end test "generates count aggregation by minute" do @@ -248,6 +249,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do assert %Ecto.Query{} = result assert result.group_bys != [] assert result.order_bys != [] + assert_select_keys(result, [:timestamp, :count]) end test "generates count aggregation by hour" do @@ -263,6 +265,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do ) assert %Ecto.Query{} = result + assert_select_keys(result, [:timestamp, :count]) end test "generates count aggregation by day" do @@ -278,6 +281,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do ) assert %Ecto.Query{} = result + assert_select_keys(result, [:timestamp, :count]) end test "generates avg aggregation on JSONB field" do @@ -293,6 +297,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do ) assert %Ecto.Query{} = result + assert_select_keys(result, [:timestamp, :count]) end test "generates sum aggregation on JSONB field" do @@ -308,6 +313,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do ) assert %Ecto.Query{} = result + assert_select_keys(result, [:timestamp, :count]) end test "generates max aggregation on JSONB field" do @@ -323,6 +329,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do ) assert %Ecto.Query{} = result + assert_select_keys(result, [:timestamp, :count]) end test "generates percentile aggregation (p50)" do @@ -338,6 +345,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do ) assert %Ecto.Query{} = result + assert_select_keys(result, [:timestamp, :count]) end test "generates percentile aggregation (p95)" do @@ -353,6 +361,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do ) assert %Ecto.Query{} = result + assert_select_keys(result, [:timestamp, :count]) end test "generates percentile aggregation (p99)" do @@ -368,6 +377,7 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do ) assert %Ecto.Query{} = result + assert_select_keys(result, [:timestamp, :count]) end end @@ -504,4 +514,10 @@ defmodule Logflare.Lql.BackendTransformer.PostgresTest do end end end + + defp assert_select_keys(%Query{select: select}, expected_keys) do + {:%{}, [], fields} = select.expr + keys = Enum.map(fields, fn {key, _} -> key end) |> Enum.sort() + assert keys == Enum.sort(expected_keys) + end end From b8a15b5f25af85d37a76ac1a08ced2549906d2ed Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Wed, 25 Mar 2026 16:30:28 +1000 Subject: [PATCH 09/44] fix: execute_query can return error (dialyzer) --- lib/logflare/backends/adaptor/postgres_adaptor.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logflare/backends/adaptor/postgres_adaptor.ex b/lib/logflare/backends/adaptor/postgres_adaptor.ex index 710382557..4771c8ed2 100644 --- a/lib/logflare/backends/adaptor/postgres_adaptor.ex +++ b/lib/logflare/backends/adaptor/postgres_adaptor.ex @@ -143,7 +143,7 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptor do @spec test_connection(Backend.t()) :: :ok | {:error, term()} def test_connection(%Backend{} = backend) do case execute_query(backend, "SELECT 1 AS result", []) do - {:ok, [%{"result" => 1}]} -> :ok + {:ok, %QueryResult{rows: [%{"result" => 1}]}} -> :ok {:error, _} = error -> error end end From b0279926baa0bf3778e3d5219ca2563cbd7fc1af Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Thu, 26 Mar 2026 11:48:38 +1000 Subject: [PATCH 10/44] test: SearchOperations postgres unit tests --- test/logflare/logs/search_operations_test.exs | 386 ++++++++++++++++-- 1 file changed, 360 insertions(+), 26 deletions(-) diff --git a/test/logflare/logs/search_operations_test.exs b/test/logflare/logs/search_operations_test.exs index 0c0a94001..c9088636f 100644 --- a/test/logflare/logs/search_operations_test.exs +++ b/test/logflare/logs/search_operations_test.exs @@ -4,6 +4,7 @@ defmodule Logflare.Logs.SearchOperationsTest do import Ecto.Query import Logflare.Utils.Guards + alias Logflare.Backends alias Logflare.Backends.Adaptor.BigQueryAdaptor alias Logflare.Backends.Adaptor.PostgresAdaptor alias Logflare.Backends.Adaptor.QueryResult @@ -15,10 +16,30 @@ defmodule Logflare.Logs.SearchOperationsTest do alias Logflare.Lql.Rules.SelectRule alias Logflare.Sources.Source.BigQuery.Schema + @postgres_search_attrs %{ + source: nil, + querystring: "", + query: nil, + chart_data_shape_id: nil, + tailing?: false, + tailing_initial?: nil, + partition_by: :timestamp, + type: :events, + backend_type: :postgres, + lql_rules: [], + lql_ts_filters: [], + lql_meta_and_msg_filters: [] + } + + setup do + insert(:plan) + + [user: insert(:user)] + end + describe "unnesting metadata if present" do - setup do - insert(:plan) - source = insert(:source, user: insert(:user), bq_table_id: "1") + setup %{user: user} do + source = insert(:source, user: user, bq_table_id: "1") [ so: %Logflare.Logs.SearchOperation{ @@ -78,9 +99,8 @@ defmodule Logflare.Logs.SearchOperationsTest do end describe "chart aggregation query generation" do - setup do - insert(:plan) - source = insert(:source, user: insert(:user), bq_table_id: "test_table") + setup %{user: user} do + source = insert(:source, user: user, bq_table_id: "test_table") base_so = %SO{ source: source, @@ -177,22 +197,15 @@ defmodule Logflare.Logs.SearchOperationsTest do end describe "postgres chart aggregation" do - setup do - insert(:plan) - user = insert(:user) + setup %{user: user} do source = insert(:source, user: user) - base_so = %SO{ - source: source, - querystring: "", - chart_data_shape_id: nil, - tailing?: false, - partition_by: :timestamp, - type: :aggregates, - backend_type: :postgres, - lql_ts_filters: [], - lql_meta_and_msg_filters: [] - } + stub(Backends, :get_default_backend, fn ^user -> %{type: :postgres} end) + + base_so = + @postgres_search_attrs + |> Map.merge(%{source: source, type: :aggregates}) + |> SO.new() [base_so: base_so] end @@ -261,10 +274,332 @@ defmodule Logflare.Logs.SearchOperationsTest do end end + describe "postgres query defaults and rules" do + setup %{user: user} do + source = insert(:source, user: user) + + stub(Backends, :get_default_backend, fn ^user -> %{type: :postgres} end) + + so = + %{@postgres_search_attrs | source: source} + |> SO.new() + + [so: so] + end + + test "apply_query_defaults/1 uses the postgres table name", %{so: so} do + so = SearchOperations.apply_query_defaults(so) + + {:ok, {sql, _params}} = PostgresAdaptor.ecto_to_sql(so.query, []) + + assert sql =~ ~s|FROM "#{PostgresAdaptor.table_name(so.source)}"| + assert sql =~ ~s|ORDER BY l0."timestamp" DESC| + assert sql =~ ~s|LIMIT 100| + end + + test "apply_select_rules/1 uses postgres dialect defaults", %{so: so} do + so = + so + |> SearchOperations.apply_query_defaults() + |> SearchOperations.apply_select_rules() + + {:ok, {sql, _params}} = PostgresAdaptor.ecto_to_sql(so.query, []) + + assert sql =~ ~s|SELECT l0."timestamp", l0."id", l0."event_message"| + end + + test "apply_filters/1 uses postgres dialect for top-level fields", %{so: so} do + filter = %FilterRule{ + path: "event_message", + operator: :=, + value: "error", + modifiers: %{} + } + + so = + %{so | lql_meta_and_msg_filters: [filter]} + |> SearchOperations.apply_query_defaults() + |> SearchOperations.apply_filters() + + {:ok, {sql, params}} = PostgresAdaptor.ecto_to_sql(so.query, []) + + assert sql =~ ~s|l0."event_message"| + assert params == ["error"] + end + end + + describe "postgres timestamp filter rules" do + setup %{user: user} do + source = insert(:source, user: user) + + stub(Backends, :get_default_backend, fn ^user -> %{type: :postgres} end) + + base_so = + @postgres_search_attrs + |> Map.merge(%{ + source: source, + tailing_initial?: false, + query: from("test_table") + }) + |> SO.new() + + [base_so: base_so] + end + + test "events live tail query applies the 10 minute timestamp window", %{base_so: base_so} do + before_call = DateTime.utc_now() + + so = + %{base_so | tailing?: true} + |> SearchOperations.apply_timestamp_filter_rules() + + after_call = DateTime.utc_now() + [%{params: [{cutoff, _type}]}] = so.query.wheres + + assert DateTime.compare(cutoff, DateTime.add(before_call, -601, :second)) == :gt + assert DateTime.compare(cutoff, DateTime.add(after_call, -599, :second)) == :lt + end + + test "events initial tail query applies the default 2 day window", %{base_so: base_so} do + before_call = DateTime.utc_now() + + so = + %{base_so | tailing?: true, tailing_initial?: true} + |> SearchOperations.apply_timestamp_filter_rules() + + after_call = DateTime.utc_now() + [%{params: [{cutoff, _type}]}] = so.query.wheres + + assert DateTime.compare(cutoff, DateTime.add(before_call, -(2 * 24 * 3600 + 1), :second)) == + :gt + + assert DateTime.compare(cutoff, DateTime.add(after_call, -(2 * 24 * 3600 - 1), :second)) == + :lt + end + + test "events explicit timestamp filters use postgres filter rules", %{base_so: base_so} do + min = ~U[2026-01-29 04:13:48.748909Z] + max = ~U[2026-01-29 06:13:48.748909Z] + + timestamp_filter = %FilterRule{ + path: "timestamp", + operator: :range, + values: [min, max], + modifiers: %{} + } + + so = + %{base_so | lql_ts_filters: [timestamp_filter]} + |> SearchOperations.apply_timestamp_filter_rules() + + [%{params: params}] = so.query.wheres + + assert [{^min, _}, {^max, _}] = params + end + + test "aggregate query without filter applys min/max", %{base_so: base_so} do + chart_rule = %ChartRule{ + path: "timestamp", + aggregate: :count, + period: :minute, + value_type: :datetime + } + + so = + %{base_so | type: :aggregates, chart_rules: [chart_rule], query: nil} + |> SearchOperations.apply_timestamp_filter_rules() + + assert so.query.from.source == {PostgresAdaptor.table_name(base_so.source), nil} + assert length(so.query.wheres) == 1 + + [%{params: params}] = so.query.wheres + + assert [{%DateTime{}, _}, {%DateTime{}, _}] = params + end + + test "aggregate query uses filters and timestamp", %{ + base_so: base_so + } do + min = ~U[2026-01-29 04:13:48.748909Z] + max = ~U[2026-01-29 06:13:48.748909Z] + + chart_rule = %ChartRule{ + path: "timestamp", + aggregate: :count, + period: :minute, + value_type: :datetime + } + + timestamp_filter = %FilterRule{ + path: "timestamp", + operator: :range, + values: [min, max], + modifiers: %{} + } + + so = + %{ + base_so + | type: :aggregates, + chart_rules: [chart_rule], + lql_ts_filters: [timestamp_filter], + query: nil + } + |> SearchOperations.apply_timestamp_filter_rules() + + assert length(so.query.wheres) == 2 + + [first_where, second_where] = so.query.wheres + + assert [{^min, _}, {^max, _}] = first_where.params + assert [{^min, _}, {^max, _}] = second_where.params + end + end + + describe "postgres backend adaptor integration" do + setup %{user: user} do + Mimic.copy(PostgresAdaptor) + + source = insert(:source, user: user) + backend = build(:backend, type: :postgres) + + stub(Backends, :get_default_backend, fn ^user -> backend end) + + base_so = + %{@postgres_search_attrs | source: source, query: from("test_table")} + |> SO.new() + + [backend: backend, base_so: base_so] + end + + test "do_query/1 uses Postgres backend adaptor and normalizes event rows", %{ + backend: backend, + base_so: base_so + } do + timestamp = ~U[2026-01-29 05:13:48.748909Z] + naive_timestamp = ~N[2026-01-29 05:14:48.748909] + nested_timestamp = ~N[2026-01-29 05:15:48.748909] + date = ~D[2026-01-29] + + Backends + |> expect(:get_default_backend, fn user -> + assert user.id == base_so.source.user.id + backend + end) + + PostgresAdaptor + |> expect(:execute_query, fn ^backend, %Ecto.Query{} = query, opts -> + assert opts == [query_type: :search] + assert %Ecto.Query{} = query + + {:ok, + [ + %{ + event_message: "postgres event", + timestamp: timestamp, + inserted_at: naive_timestamp, + log_date: date, + metadata: %{level: "error", seen_at: nested_timestamp}, + tags: [date, naive_timestamp] + } + ]} + end) + + PostgresAdaptor + |> expect(:ecto_to_sql, fn %Ecto.Query{}, [] -> + {:ok, {"SELECT * FROM test_table", ["param"]}} + end) + + result_so = SearchOperations.do_query(base_so) + + assert result_so.sql_string == "SELECT * FROM test_table" + assert result_so.sql_params == ["param"] + + assert result_so.rows == [ + %{ + "event_message" => "postgres event", + "timestamp" => DateTime.to_unix(timestamp, :microsecond), + "inserted_at" => + DateTime.to_unix( + DateTime.from_naive!(naive_timestamp, "Etc/UTC"), + :microsecond + ), + "log_date" => "2026-01-29", + "metadata" => %{ + "level" => "error", + "seen_at" => + DateTime.to_unix( + DateTime.from_naive!(nested_timestamp, "Etc/UTC"), + :microsecond + ) + }, + "tags" => [ + "2026-01-29", + DateTime.to_unix( + DateTime.from_naive!(naive_timestamp, "Etc/UTC"), + :microsecond + ) + ] + } + ] + + refute result_so.error + end + + test "do_query/1 stores postgres backend errors", %{backend: backend, base_so: base_so} do + Backends + |> expect(:get_default_backend, fn _user -> backend end) + + PostgresAdaptor + |> expect(:execute_query, fn ^backend, %Ecto.Query{}, [query_type: :search] -> + {:error, :postgres_failed} + end) + + result_so = SearchOperations.do_query(base_so) + + assert result_so.error == :postgres_failed + end + + test "do_query/1 normalizes aggregate postgres rows and process_query_result/1 adds datetime", + %{ + backend: backend, + base_so: base_so + } do + timestamp = ~N[2026-01-29 05:13:48.748909] + + Backends + |> expect(:get_default_backend, fn _user -> backend end) + + PostgresAdaptor + |> expect(:execute_query, fn ^backend, %Ecto.Query{}, [query_type: :search] -> + {:ok, [%{count: 2, timestamp: timestamp}]} + end) + + PostgresAdaptor + |> expect(:ecto_to_sql, fn %Ecto.Query{}, [] -> + {:ok, {"SELECT count(*) FROM test_table", []}} + end) + + result_so = + %{base_so | type: :aggregates} + |> SearchOperations.do_query() + |> SearchOperations.process_query_result() + + unix_timestamp = DateTime.to_unix(DateTime.from_naive!(timestamp, "Etc/UTC"), :microsecond) + + assert result_so.rows == [ + %{ + "value" => 2, + "timestamp" => unix_timestamp, + "datetime" => Timex.from_unix(unix_timestamp, :microsecond) + } + ] + end + end + describe "apply_select_rules/1" do - setup do - insert(:plan) - source = insert(:source, user: insert(:user), bq_table_id: "test_table") + setup %{user: user} do + source = insert(:source, user: user, bq_table_id: "test_table") so = %SO{ source: source, @@ -452,9 +787,8 @@ defmodule Logflare.Logs.SearchOperationsTest do end describe "backend adaptor integration" do - setup do - insert(:plan) - source = insert(:source, user: insert(:user), bq_table_id: "test_table") + setup %{user: user} do + source = insert(:source, user: user, bq_table_id: "test_table") base_so = %SO{ source: source, From 736c1873c48de9c55da97e68ef856eb223074114 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Fri, 27 Mar 2026 14:26:30 +1000 Subject: [PATCH 11/44] merge base: main + refactor/adaptor-query-result From 6a2e1856de63de8dfeef330e5ccb81af0272ff16 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Wed, 4 Mar 2026 15:17:21 +1000 Subject: [PATCH 12/44] feat: Monaco editor with LQL support for search field --- assets/css/source_log_search.scss | 7 + assets/js/app.js | 5 +- assets/js/lql_editor_wrapper_hook.js | 90 ++++++ assets/js/lql_language.js | 261 ++++++++++++++++++ .../live/search_live/form_components.ex | 57 +++- .../live/search_live/logs_search_lv.ex | 40 ++- 6 files changed, 436 insertions(+), 24 deletions(-) create mode 100644 assets/js/lql_editor_wrapper_hook.js create mode 100644 assets/js/lql_language.js diff --git a/assets/css/source_log_search.scss b/assets/css/source_log_search.scss index f8a190fe2..c7563547a 100644 --- a/assets/css/source_log_search.scss +++ b/assets/css/source_log_search.scss @@ -57,6 +57,13 @@ } } +// Prevent live_monaco_editor auto-resize from expanding the single-line editor +.lql-editor-wrapper [phx-hook="CodeEditorHook"] { + height: 32px !important; + min-height: 32px !important; + max-height: 32px !important; +} + .message { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; } diff --git a/assets/js/app.js b/assets/js/app.js index 64d5471a1..b253b4544 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -21,6 +21,7 @@ import sourceLiveViewHooks from "./source_lv_hooks"; import $ from "jquery"; import moment from "moment"; import { CodeEditorHook } from "../../deps/live_monaco_editor/priv/static/live_monaco_editor.esm" +import LqlEditorWrapper from "./lql_editor_wrapper_hook" // set moment globally before daterangepicker @@ -47,8 +48,8 @@ const hooks = { ...sourceLiveViewHooks, ...LiveModalHooks, ...BillingHooks, - CodeEditorHook - + CodeEditorHook, + LqlEditorWrapper, }; let liveSocket = new LiveSocket("/live", Socket, { diff --git a/assets/js/lql_editor_wrapper_hook.js b/assets/js/lql_editor_wrapper_hook.js new file mode 100644 index 000000000..b805475b3 --- /dev/null +++ b/assets/js/lql_editor_wrapper_hook.js @@ -0,0 +1,90 @@ +import { + registerLqlLanguage, + registerLqlCompletionProvider, +} from "./lql_language"; + +const parseSchemaFields = (schemaFieldsJson) => { + if (!schemaFieldsJson) return []; + + try { + return JSON.parse(schemaFieldsJson); + } catch { + return []; + } +}; + +const LqlEditorWrapper = { + mounted() { + this._schemaFields = parseSchemaFields(this.el.dataset.schemaFieldsJson); + this._completionDisposable = null; + this._editor = null; + + // Listen for CodeEditorHook's mount event (bubbles from inner element) + this.el.addEventListener("lme:editor_mounted", (event) => { + const { editor } = event.detail; + const standaloneEditor = editor.standalone_code_editor; + this._editor = standaloneEditor; + + const monaco = window.monaco; + + // Register LQL language and set model language + registerLqlLanguage(monaco); + const model = standaloneEditor.getModel(); + monaco.editor.setModelLanguage(model, "lql"); + + // Register completion provider + this._completionDisposable = registerLqlCompletionProvider( + monaco, + () => this._schemaFields, + ); + + // Enter submits the form (only when suggest widget is NOT visible) + standaloneEditor.addCommand( + monaco.KeyCode.Enter, + () => { + const form = this.el.closest("form"); + if (form) { + form.dispatchEvent( + new Event("submit", { bubbles: true, cancelable: true }), + ); + } + }, + "!suggestWidgetVisible", + ); + + // Sync value to hidden input on change + standaloneEditor.onDidChangeModelContent(() => { + const value = standaloneEditor.getValue(); + const hiddenInput = this.el.querySelector( + 'input[type="hidden"][name="search[querystring]"]', + ); + if (hiddenInput) { + hiddenInput.value = value; + hiddenInput.dispatchEvent(new Event("input", { bubbles: true })); + } + }); + + // Forward focus/blur to LiveView + standaloneEditor.onDidFocusEditorText(() => { + this.pushEvent("form_focus", { value: standaloneEditor.getValue() }); + }); + + standaloneEditor.onDidBlurEditorText(() => { + this.pushEvent("form_blur", { value: standaloneEditor.getValue() }); + }); + }); + }, + + updated() { + this._schemaFields = parseSchemaFields(this.el.dataset.schemaFieldsJson); + }, + + destroyed() { + if (this._completionDisposable) { + this._completionDisposable.dispose(); + this._completionDisposable = null; + } + }, +}; + +export default LqlEditorWrapper; diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js new file mode 100644 index 000000000..9f4499731 --- /dev/null +++ b/assets/js/lql_language.js @@ -0,0 +1,261 @@ +/** + * LQL syntax definition for Monaco Editor. + */ + +export function registerLqlLanguage(monaco) { + if (monaco.languages.getLanguages().some((lang) => lang.id === "lql")) { + return; + } + + monaco.languages.register({ id: "lql" }); + + monaco.languages.setMonarchTokensProvider("lql", { + ignoreCase: true, + + keywords: ["true", "false", "NULL"], + + logLevels: [ + "emergency", + "alert", + "critical", + "error", + "warning", + "notice", + "info", + "debug", + ], + + functions: [ + "count", + "countd", + "avg", + "sum", + "max", + "p50", + "p95", + "p99", + "group_by", + ], + + tokenizer: { + root: [ + // Chart/select/from prefixes + [ + /\b((?:timestamp|t):)(:(?:minute|min|m|second|s|hour|h|day|d)\b)/, + ["keyword.prefix.timestamp", "constant.period"], + ], + [/(?:chart|c|select|s|from|f|timestamp|t):/, "keyword.prefix"], + + // Metadata prefix (m. or metadata.) + [/(?:metadata|m)\./, "keyword.prefix", "@metadataPath"], + + // Operators (order matters: longest first) + [/:@>~/, "operator"], + [/:@>/, "operator"], + [/:>=/, "operator"], + [/:<=/, "operator"], + [/:>/, "operator"], + [/: + f.name.startsWith("metadata."), + ); + + // Determine what prefix is already typed after m./metadata. + // e.g. if user typed "m.request.", typedPath = "request." + // We want to show fields under "metadata.request." + const pathPrefix = typedPath.includes(".") + ? typedPath.substring(0, typedPath.lastIndexOf(".") + 1) + : ""; + const fullPrefix = "metadata." + pathPrefix; + + // Collect unique next segments at this level + const seen = new Set(); + const suggestions = []; + + for (const field of metadataFields) { + if (!field.name.startsWith(fullPrefix)) continue; + + const remainder = field.name.slice(fullPrefix.length); + if (!remainder) continue; + + const dotIdx = remainder.indexOf("."); + const segment = + dotIdx >= 0 ? remainder.substring(0, dotIdx) : remainder; + const isLeaf = dotIdx < 0; + + if (seen.has(segment)) continue; + seen.add(segment); + + suggestions.push({ + label: segment, + kind: isLeaf + ? monaco.languages.CompletionItemKind.Field + : monaco.languages.CompletionItemKind.Module, + detail: isLeaf ? field.type : "namespace", + insertText: segment, + range: replaceRange, + sortText: segment.padStart(50), + }); + } + + return { suggestions }; + } + + // At start of input or after whitespace: suggest LQL keywords + if (/(?:^|[\s])$/.test(textUntilPosition)) { + const suggestions = LQL_KEYWORDS.map((kw, i) => ({ + label: kw.label, + kind: monaco.languages.CompletionItemKind.Keyword, + detail: kw.detail, + insertText: kw.insertText, + insertTextRules: kw.insertText.includes("$0") + ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet + : undefined, + range: replaceRange, + sortText: String(i).padStart(3, "0"), + })); + + return { suggestions }; + } + + return { suggestions: [] }; + }, + }); +} diff --git a/lib/logflare_web/live/search_live/form_components.ex b/lib/logflare_web/live/search_live/form_components.ex index 4f3f00d8f..05674caec 100644 --- a/lib/logflare_web/live/search_live/form_components.ex +++ b/lib/logflare_web/live/search_live/form_components.ex @@ -130,7 +130,7 @@ defmodule LogflareWeb.SearchLive.FormComponents do attr :search_form, :any, required: true attr :querystring, :string, required: true - attr :search_history, :list, required: true + attr :lql_schema_fields_json, :string, required: true attr :search_timezone, :string, required: true attr :loading, :boolean, required: true attr :tailing?, :boolean, required: true @@ -152,26 +152,17 @@ defmodule LogflareWeb.SearchLive.FormComponents do <.recommended_field_inputs fields={Source.recommended_query_fields(@source)} id_prefix="search-field" />
- {text_input(f, :querystring, - phx_focus: :form_focus, - phx_blur: :form_blur, - value: @querystring, - class: "form-control tw-mt-0", - list: "matches" - )} +
+ + {hidden_input(f, :querystring, value: @querystring)} +
- {text_input(f, :search_timezone, class: "d-none", value: @search_timezone, id: "search-timezone" )} - - <%= for s <- @search_history do %> - - <% end %> -
@@ -196,6 +187,44 @@ defmodule LogflareWeb.SearchLive.FormComponents do """ end + defp lql_editor_opts do + Map.merge( + LiveMonacoEditor.default_opts(), + %{ + "language" => "lql", + "theme" => "default", + "minimap" => %{"enabled" => false}, + "lineNumbers" => "off", + "glyphMargin" => false, + "folding" => false, + "lineDecorationsWidth" => 0, + "lineNumbersMinChars" => 0, + "wordWrap" => "off", + "scrollBeyondLastLine" => false, + "scrollbar" => %{ + "horizontal" => "hidden", + "vertical" => "hidden", + "handleMouseWheel" => false + }, + "overviewRulerLanes" => 0, + "overviewRulerBorder" => false, + "hideCursorInOverviewRuler" => true, + "contextmenu" => false, + "fixedOverflowWidgets" => true, + "suggest" => %{"enabled" => true, "showWords" => false}, + "parameterHints" => %{"enabled" => false}, + "quickSuggestions" => true, + "renderLineHighlight" => "none", + "matchBrackets" => "never", + "fontSize" => 14, + "fontFamily" => + "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", + "padding" => %{"top" => 5, "bottom" => 5}, + "automaticLayout" => true + } + ) + end + defp search_agg_controls_enabled?(lql_rules) do lql_rules |> Enum.find(%{}, &match?(%Logflare.Lql.Rules.ChartRule{}, &1)) 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 543418bed..3c678aa88 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -101,6 +101,7 @@ defmodule LogflareWeb.Source.SearchLV do uri_params: nil, uri: nil, lql_rules: [], + lql_schema_fields_json: schema_fields_json(source), querystring: Map.get(params, "querystring", @default_qs), force_query: Map.get(params, "force", "false") == "true", search_history: [], @@ -198,7 +199,7 @@ defmodule LogflareWeb.Source.SearchLV do |> assign(:chart_loading, true) |> assign(:tailing_initial?, true) |> assign(:lql_rules, lql_rules) - |> assign(:querystring, qs) + |> assign_querystring(qs) |> assign(:search_op_log_events, search_op_log_events) if connected?(socket) do @@ -215,12 +216,12 @@ defmodule LogflareWeb.Source.SearchLV do {:error, error} -> socket - |> assign(:querystring, qs) + |> assign_querystring(qs) |> error_socket(error) {:error, :field_not_found = type, suggested_querystring, error} -> socket - |> assign(:querystring, qs) + |> assign_querystring(qs) |> error_socket(type, suggested_querystring, error) end @@ -289,7 +290,7 @@ defmodule LogflareWeb.Source.SearchLV do assign(:search_history, search_history) - |> assign(:querystring, qs) + |> assign_querystring(qs) |> assign(:lql_rules, lql_rules) |> assign(:loading, true) |> assign(:chart_loading, true) @@ -503,7 +504,7 @@ defmodule LogflareWeb.Source.SearchLV do socket |> assign(:tailing?, false) |> assign(:lql_rules, lql_list) - |> assign(:querystring, qs) + |> assign_querystring(qs) |> push_patch_with_params(%{querystring: qs, tailing?: false}) {:noreply, socket} @@ -758,7 +759,7 @@ defmodule LogflareWeb.Source.SearchLV do |> assign(:tailing_initial?, true) |> clear_flash() |> assign(:lql_rules, lql_rules) - |> assign(:querystring, qs) + |> assign_querystring(qs) |> push_patch_with_params(%{querystring: qs, tz: tz, tailing?: tailing?}) {:error, error} -> @@ -1075,7 +1076,7 @@ defmodule LogflareWeb.Source.SearchLV do qs = Lql.encode!(lql_rules) socket - |> assign(:querystring, qs) + |> assign_querystring(qs) |> assign(:lql_rules, lql_rules) end @@ -1182,4 +1183,27 @@ defmodule LogflareWeb.Source.SearchLV do class: "tw-block tw-pt-3" ) end + + defp assign_querystring(socket, qs) do + socket + |> assign(:querystring, qs) + |> LiveMonacoEditor.set_value(qs, to: "lql_query") + end + + defp schema_fields_json(source), do: source |> lql_schema_fields() |> Jason.encode!() + + defp lql_schema_fields(source) do + case SourceSchemas.Cache.get_source_schema_by(source_id: source.id) do + %{schema_flat_map: flat_map} when is_map(flat_map) -> + Enum.map(flat_map, fn {name, type} -> + %{name: name, type: format_schema_type(type)} + end) + + _ -> + [] + end + end + + defp format_schema_type({type, inner}), do: "#{type}[#{inner}]" + defp format_schema_type(type), do: to_string(type) end From 1a6ef6ca8a2840f3cc6493c0861544f9b098bfb2 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 9 Mar 2026 14:14:52 +1000 Subject: [PATCH 13/44] use suggest_saved_searches for autocomplete --- assets/js/lql_editor_wrapper_hook.js | 84 +++- assets/js/lql_language.js | 373 ++++++++++++++++-- .../live/search_live/logs_search_lv.ex | 28 +- 3 files changed, 435 insertions(+), 50 deletions(-) diff --git a/assets/js/lql_editor_wrapper_hook.js b/assets/js/lql_editor_wrapper_hook.js index b805475b3..ee802dfc0 100644 --- a/assets/js/lql_editor_wrapper_hook.js +++ b/assets/js/lql_editor_wrapper_hook.js @@ -1,4 +1,5 @@ import { + hasLqlSuggestions, registerLqlLanguage, registerLqlCompletionProvider, } from "./lql_language"; @@ -16,8 +17,11 @@ const parseSchemaFields = (schemaFieldsJson) => { const LqlEditorWrapper = { mounted() { this._schemaFields = parseSchemaFields(this.el.dataset.schemaFieldsJson); + this._suggestedSearches = []; this._completionDisposable = null; this._editor = null; + this._refreshSuggestionsTimer = null; + this._skipNextSavedSearchRequest = false; // Listen for CodeEditorHook's mount event (bubbles from inner element) this.el.addEventListener("lme:editor_mounted", (event) => { @@ -36,8 +40,23 @@ const LqlEditorWrapper = { this._completionDisposable = registerLqlCompletionProvider( monaco, () => this._schemaFields, + () => this._suggestedSearches, ); + standaloneEditor.addAction({ + id: "lql.dismissSavedSearchSuggest", + label: "Dismiss saved search suggest", + run: () => { + this._skipNextSavedSearchRequest = true; + + const suggestController = standaloneEditor.getContribution( + "editor.contrib.suggestController", + ); + + suggestController?.cancelSuggestWidget?.(); + }, + }); + // Enter submits the form (only when suggest widget is NOT visible) standaloneEditor.addCommand( monaco.KeyCode.Enter, @@ -62,11 +81,32 @@ const LqlEditorWrapper = { hiddenInput.value = value; hiddenInput.dispatchEvent(new Event("input", { bubbles: true })); } + + if (this._skipNextSavedSearchRequest) { + this._skipNextSavedSearchRequest = false; + clearTimeout(this._refreshSuggestionsTimer); + return; + } + + clearTimeout(this._refreshSuggestionsTimer); + this._refreshSuggestionsTimer = window.setTimeout(() => { + this.refreshSuggestions(); + }, 100); }); // Forward focus/blur to LiveView standaloneEditor.onDidFocusEditorText(() => { - this.pushEvent("form_focus", { value: standaloneEditor.getValue() }); + this.pushEvent( + "form_focus", + { value: standaloneEditor.getValue() }, + ({ suggestions = [] }) => { + this._suggestedSearches = Array.isArray(suggestions) + ? suggestions + : []; + + this.refreshSuggestions(); + }, + ); }); standaloneEditor.onDidBlurEditorText(() => { @@ -79,7 +119,49 @@ const LqlEditorWrapper = { this._schemaFields = parseSchemaFields(this.el.dataset.schemaFieldsJson); }, + refreshSuggestions() { + const model = this._editor?.getModel(); + const position = this._editor?.getPosition(); + const suggestController = this._editor?.getContribution?.( + "editor.contrib.suggestController", + ); + + suggestController?.cancelSuggestWidget?.(); + + if (!model || !position) { + return; + } + + const textUntilPosition = model.getValueInRange({ + startLineNumber: position.lineNumber, + startColumn: 1, + endLineNumber: position.lineNumber, + endColumn: position.column, + }); + const fullLine = model.getLineContent(position.lineNumber); + + if ( + !hasLqlSuggestions( + textUntilPosition, + fullLine, + this._schemaFields, + this._suggestedSearches, + ) + ) { + return; + } + + if (suggestController?.triggerSuggest) { + suggestController.triggerSuggest(); + return; + } + + this._editor?.getAction("editor.action.triggerSuggest").run(); + }, + destroyed() { + clearTimeout(this._refreshSuggestionsTimer); + if (this._completionDisposable) { this._completionDisposable.dispose(); this._completionDisposable = null; diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index 9f4499731..42b4c285c 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -58,12 +58,15 @@ export function registerLqlLanguage(monaco) { [/:", detail: "greater than", insertText: ":>" }, + { label: ":>=", detail: "greater than or equal", insertText: ":>=" }, + { label: ":<", detail: "less than", insertText: ":<" }, + { label: ":<=", detail: "less than or equal", insertText: ":<=" }, + { label: ":~", detail: "regex match", insertText: ":~" }, + { label: ":@>", detail: "array includes value", insertText: ":@>" }, + { label: ":@>~", detail: "array includes regex match", insertText: ":@>~" }, +]; + +function buildKeywordSuggestions(monaco, token, range) { + return LQL_KEYWORDS.filter( + (kw) => + kw.label.toLocaleLowerCase().startsWith(token.toLocaleLowerCase()) || + kw.insertText.toLocaleLowerCase().startsWith(token.toLocaleLowerCase()), + ).map((kw, i) => ({ + label: kw.label, + kind: monaco.languages.CompletionItemKind.Keyword, + detail: kw.detail, + insertText: kw.insertText, + insertTextRules: kw.insertText.includes("$0") + ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet + : undefined, + command: + kw.insertText === "m." || kw.insertText === "metadata." + ? { + id: "editor.action.triggerSuggest", + title: "Trigger suggest", + } + : undefined, + range, + sortText: String(i).padStart(3, "0"), + })); +} + +function buildOperatorSuggestions(monaco, operatorPrefix, range) { + return LQL_FILTER_OPERATORS.filter((operator) => + operator.label.startsWith(operatorPrefix), + ).map((operator, i) => ({ + label: operator.label, + kind: monaco.languages.CompletionItemKind.Operator, + detail: operator.detail, + insertText: operator.insertText, + range, + sortText: String(i).padStart(3, "0"), + })); +} + +function buildTimestampSuggestions(monaco, token, range) { + return LQL_TIMESTAMP_KEYWORDS.filter( + (kw) => + kw.label.toLocaleLowerCase().startsWith(token.toLocaleLowerCase()) || + kw.insertText.toLocaleLowerCase().startsWith(token.toLocaleLowerCase()), + ).map((kw, i) => ({ + label: kw.label, + kind: monaco.languages.CompletionItemKind.Keyword, + detail: kw.detail, + insertText: kw.insertText, + range, + sortText: String(i).padStart(3, "0"), + })); +} + +function getMetadataSuggestionSegments(fields, typedPath) { + const metadataFields = fields.filter((f) => f.name.startsWith("metadata.")); + const pathPrefix = typedPath.includes(".") + ? typedPath.substring(0, typedPath.lastIndexOf(".") + 1) + : ""; + const fullPrefix = "metadata." + pathPrefix; + const seen = new Set(); + const suggestions = []; + + for (const field of metadataFields) { + if (!field.name.startsWith(fullPrefix)) continue; + + const remainder = field.name.slice(fullPrefix.length); + if (!remainder) continue; + + const dotIdx = remainder.indexOf("."); + const segment = dotIdx >= 0 ? remainder.substring(0, dotIdx) : remainder; + + if (seen.has(segment)) continue; + seen.add(segment); + suggestions.push(segment); + } + + return suggestions; +} + +export function hasLqlSuggestions( + textUntilPosition, + fullLine, + schemaFields, + suggestedSearches, +) { + const operatorMatch = textUntilPosition.match( + /(?:^|\s)-?(?:(?:m|metadata)\.[\w.]+|(?:t|timestamp)|[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*):([@><~=]*)$/, + ); + const timestampValueMatch = textUntilPosition.match( + /(?:^|\s)(?:t|timestamp):([a-zA-Z@]*)$/, + ); + const tokenMatch = textUntilPosition.match(/(?:^|\s)(\S+)$/); + const currentToken = tokenMatch ? tokenMatch[1] : null; + + if ( + currentToken && + textUntilPosition.length > 0 && + /^\S+$/.test(textUntilPosition) && + fullLine === textUntilPosition && + !["t:", "timestamp:"].includes(currentToken) + ) { + return ( + [...new Set(suggestedSearches)].some((querystring) => + querystring + .toLocaleLowerCase() + .startsWith(currentToken.toLocaleLowerCase()), + ) || + buildKeywordSuggestions( + { languages: { CompletionItemKind: { Keyword: "keyword" } } }, + currentToken, + null, + ).length > 0 + ); + } + + if (timestampValueMatch) { + return buildTimestampSuggestions( + { languages: { CompletionItemKind: { Keyword: "keyword" } } }, + timestampValueMatch[1], + null, + ).length > 0; + } + + if (operatorMatch) { + return buildOperatorSuggestions( + { languages: { CompletionItemKind: { Operator: "operator" } } }, + `:${operatorMatch[1]}`, + null, + ).length > 0; + } + + const metaMatch = textUntilPosition.match( + /(?:^|[\s:])(?:m|metadata)\.([\w.]*?)$/, + ); + + if (metaMatch) { + return getMetadataSuggestionSegments(schemaFields, metaMatch[1]).length > 0; + } + + if (/(?:^|[\s])$/.test(textUntilPosition)) { + return true; + } + + if (currentToken) { + return ( + buildKeywordSuggestions( + { languages: { CompletionItemKind: { Keyword: "keyword" } } }, + currentToken, + null, + ).length > 0 + ); + } + + return false; +} + /** * Registers a CompletionItemProvider for the LQL language. * @param {object} monaco - The monaco-editor namespace * @param {function} getFields - Returns current schema fields array [{name, type}] + * @param {function} getSuggestedSearches - Returns saved-search suggestions [querystring] * @returns {IDisposable} The disposable for the registered provider */ -export function registerLqlCompletionProvider(monaco, getFields) { +export function registerLqlCompletionProvider( + monaco, + getFields, + getSuggestedSearches, +) { return monaco.languages.registerCompletionItemProvider("lql", { - triggerCharacters: ["."], + triggerCharacters: [".", ":", "@", ">", "<", "~"], provideCompletionItems(model, position) { const textUntilPosition = model.getValueInRange({ @@ -184,6 +388,115 @@ export function registerLqlCompletionProvider(monaco, getFields) { startColumn: word.startColumn, endColumn: word.endColumn, }; + const lineRange = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: 1, + endColumn: model.getLineMaxColumn(position.lineNumber), + }; + const operatorMatch = textUntilPosition.match( + /(?:^|\s)-?(?:(?:m|metadata)\.[\w.]+|(?:t|timestamp)|[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*):([@><~=]*)$/, + ); + const timestampValueMatch = textUntilPosition.match( + /(?:^|\s)(?:t|timestamp):([a-zA-Z@]*)$/, + ); + const fullLine = model.getLineContent(position.lineNumber); + const tokenMatch = textUntilPosition.match(/(?:^|\s)(\S+)$/); + const currentToken = tokenMatch ? tokenMatch[1] : null; + const currentTokenRange = currentToken + ? { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: position.column - currentToken.length, + endColumn: position.column, + } + : replaceRange; + const savedSearchQuerystrings = [...new Set(getSuggestedSearches())]; + const buildSavedSearchSuggestions = (querystrings, range) => + querystrings.map((querystring, index) => ({ + label: querystring, + kind: monaco.languages.CompletionItemKind.Snippet, + detail: "saved search", + insertText: querystring, + command: { + id: "lql.dismissSavedSearchSuggest", + title: "Dismiss saved search suggest", + }, + range, + sortText: `0-${String(index).padStart(3, "0")}`, + })); + + if ( + currentToken && + textUntilPosition.length > 0 && + /^\S+$/.test(textUntilPosition) && + fullLine === textUntilPosition && + !["t:", "timestamp:"].includes(currentToken) + ) { + const matchedSavedSearches = savedSearchQuerystrings.filter((querystring) => + querystring + .toLocaleLowerCase() + .startsWith(currentToken.toLocaleLowerCase()), + ); + const filteredSavedSearchQuerystrings = + matchedSavedSearches.length === 1 && + matchedSavedSearches[0].toLocaleLowerCase() === + currentToken.toLocaleLowerCase() + ? [] + : matchedSavedSearches; + const savedSearchSuggestions = buildSavedSearchSuggestions( + filteredSavedSearchQuerystrings, + lineRange, + ); + + const keywordSuggestions = buildKeywordSuggestions( + monaco, + currentToken, + lineRange, + ).map((suggestion, index) => ({ + ...suggestion, + sortText: `1-${String(index).padStart(3, "0")}`, + })); + + const suggestions = [...savedSearchSuggestions, ...keywordSuggestions]; + + if (suggestions.length > 0) { + return { suggestions }; + } + } + + if (timestampValueMatch) { + const timestampToken = timestampValueMatch[1]; + const suggestions = buildTimestampSuggestions(monaco, timestampToken, { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: position.column - timestampToken.length, + endColumn: position.column, + }); + + if (suggestions.length > 0) { + return { suggestions }; + } + } + + if (operatorMatch) { + const operatorPrefix = `:${operatorMatch[1]}`; + const operatorRange = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: position.column - operatorPrefix.length, + endColumn: position.column, + }; + const suggestions = buildOperatorSuggestions( + monaco, + operatorPrefix, + operatorRange, + ); + + if (suggestions.length > 0) { + return { suggestions }; + } + } // Check if we're in a metadata path context: m. or metadata. prefix const metaMatch = textUntilPosition.match( @@ -193,35 +506,22 @@ export function registerLqlCompletionProvider(monaco, getFields) { if (metaMatch) { const typedPath = metaMatch[1]; // e.g. "request." or "req" const fields = getFields(); + const suggestions = []; const metadataFields = fields.filter((f) => f.name.startsWith("metadata."), ); - - // Determine what prefix is already typed after m./metadata. - // e.g. if user typed "m.request.", typedPath = "request." - // We want to show fields under "metadata.request." const pathPrefix = typedPath.includes(".") ? typedPath.substring(0, typedPath.lastIndexOf(".") + 1) : ""; const fullPrefix = "metadata." + pathPrefix; - // Collect unique next segments at this level - const seen = new Set(); - const suggestions = []; - - for (const field of metadataFields) { - if (!field.name.startsWith(fullPrefix)) continue; - - const remainder = field.name.slice(fullPrefix.length); - if (!remainder) continue; - - const dotIdx = remainder.indexOf("."); - const segment = - dotIdx >= 0 ? remainder.substring(0, dotIdx) : remainder; - const isLeaf = dotIdx < 0; - - if (seen.has(segment)) continue; - seen.add(segment); + for (const segment of getMetadataSuggestionSegments(fields, typedPath)) { + const field = metadataFields.find( + (item) => + item.name === `${fullPrefix}${segment}` || + item.name.startsWith(`${fullPrefix}${segment}.`), + ); + const isLeaf = field ? field.name === `${fullPrefix}${segment}` : true; suggestions.push({ label: segment, @@ -240,7 +540,7 @@ export function registerLqlCompletionProvider(monaco, getFields) { // At start of input or after whitespace: suggest LQL keywords if (/(?:^|[\s])$/.test(textUntilPosition)) { - const suggestions = LQL_KEYWORDS.map((kw, i) => ({ + const keywordSuggestions = LQL_KEYWORDS.map((kw, i) => ({ label: kw.label, kind: monaco.languages.CompletionItemKind.Keyword, detail: kw.detail, @@ -249,12 +549,29 @@ export function registerLqlCompletionProvider(monaco, getFields) { ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined, range: replaceRange, - sortText: String(i).padStart(3, "0"), + sortText: `1-${String(i).padStart(3, "0")}`, })); + const showSavedSearches = textUntilPosition.trim().length === 0; + const savedSearchSuggestions = showSavedSearches + ? buildSavedSearchSuggestions(savedSearchQuerystrings, lineRange) + : []; + const suggestions = [...savedSearchSuggestions, ...keywordSuggestions]; return { suggestions }; } + if (currentToken) { + const suggestions = buildKeywordSuggestions( + monaco, + currentToken, + currentTokenRange, + ); + + if (suggestions.length > 0) { + return { suggestions }; + } + } + return { suggestions: [] }; }, }); 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 3c678aa88..42e1ef344 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -104,7 +104,6 @@ defmodule LogflareWeb.Source.SearchLV do lql_schema_fields_json: schema_fields_json(source), querystring: Map.get(params, "querystring", @default_qs), force_query: Map.get(params, "force", "false") == "true", - search_history: [], search_form: to_form(%{}, as: :search) ) |> maybe_assign_user_timezone(team_user, user) @@ -423,13 +422,15 @@ defmodule LogflareWeb.Source.SearchLV do hard_play(ev, socket) end - def handle_event("form_focus", %{"value" => value}, socket) do + def handle_event("form_focus", %{"value" => _value}, socket) do send(self(), :soft_pause) - source = socket.assigns.source - search_history = search_history(value, source) + suggestions = + socket.assigns.source.id + |> SavedSearches.list_saved_searches_by_source() + |> Enum.map(& &1.querystring) - {:noreply, assign(socket, :search_history, search_history)} + {:reply, %{suggestions: suggestions}, socket} end def handle_event("form_blur", %{"value" => _value}, socket) do @@ -439,14 +440,9 @@ defmodule LogflareWeb.Source.SearchLV do end def handle_event("form_update" = _ev, %{"search" => search}, %{assigns: prev_assigns} = socket) do - source = prev_assigns.source - new_qs = search["querystring"] new_chart_agg = String.to_existing_atom(search["chart_aggregate"]) new_chart_period = String.to_existing_atom(search["chart_period"]) - - search_history = search_history(new_qs, source) - socket = assign(socket, :querystring, new_qs) prev_chart_rule = @@ -465,7 +461,6 @@ defmodule LogflareWeb.Source.SearchLV do qs = Lql.encode!(lql_rules) socket - |> assign(:search_history, search_history) |> assign_querystring(qs) |> assign(:lql_rules, lql_rules) |> assign(:loading, true) @@ -473,7 +468,7 @@ defmodule LogflareWeb.Source.SearchLV do |> clear_flash() |> push_patch_with_params(%{querystring: qs, tailing?: prev_assigns.tailing?}) else - assign(socket, :search_history, search_history) + socket end {:noreply, socket} @@ -996,15 +991,6 @@ defmodule LogflareWeb.Source.SearchLV do |> put_flash(:error, error) end - defp search_history(new_qs, source) do - search_history = SavedSearches.suggest_saved_searches(new_qs, source.id) - - if Enum.count(search_history) == 1 && - hd(search_history).querystring == new_qs, - do: [], - else: search_history - end - defp soft_play( _ev, %{assigns: %{uri_params: %{"tailing?" => "false"}}} = socket From 6e73a4be87d0462cb7c6e9f7c1c2cab957618869 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 10 Mar 2026 09:42:00 +1000 Subject: [PATCH 14/44] fix aggregate autocomplete --- assets/js/lql_language.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index 42b4c285c..a68088cec 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -281,6 +281,9 @@ function getMetadataSuggestionSegments(fields, typedPath) { return suggestions; } +const completionKindStub = { Keyword: "keyword", Operator: "operator" }; +const completionItemInsertTextRuleStub = { InsertAsSnippet: "InsertAsSnippet" }; + export function hasLqlSuggestions( textUntilPosition, fullLine, @@ -310,7 +313,12 @@ export function hasLqlSuggestions( .startsWith(currentToken.toLocaleLowerCase()), ) || buildKeywordSuggestions( - { languages: { CompletionItemKind: { Keyword: "keyword" } } }, + { + languages: { + CompletionItemKind: completionKindStub, + CompletionItemInsertTextRule: completionItemInsertTextRuleStub, + }, + }, currentToken, null, ).length > 0 @@ -319,7 +327,11 @@ export function hasLqlSuggestions( if (timestampValueMatch) { return buildTimestampSuggestions( - { languages: { CompletionItemKind: { Keyword: "keyword" } } }, + { + languages: { + CompletionItemKind: completionKindStub, + }, + }, timestampValueMatch[1], null, ).length > 0; @@ -327,7 +339,11 @@ export function hasLqlSuggestions( if (operatorMatch) { return buildOperatorSuggestions( - { languages: { CompletionItemKind: { Operator: "operator" } } }, + { + languages: { + CompletionItemKind: completionKindStub, + }, + }, `:${operatorMatch[1]}`, null, ).length > 0; @@ -348,7 +364,12 @@ export function hasLqlSuggestions( if (currentToken) { return ( buildKeywordSuggestions( - { languages: { CompletionItemKind: { Keyword: "keyword" } } }, + { + languages: { + CompletionItemKind: completionKindStub, + CompletionItemInsertTextRule: completionItemInsertTextRuleStub, + }, + }, currentToken, null, ).length > 0 From 6d22b71be09cafec876e479be4fb3a118bd0711c Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Thu, 26 Mar 2026 12:01:43 +1000 Subject: [PATCH 15/44] test: SearchLV postgres backend search --- .../live/search_live/logs_search_lv_test.exs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) 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 764dab7d9..e505b74f2 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 @@ -5,8 +5,10 @@ defmodule LogflareWeb.Source.SearchLVTest do import Phoenix.LiveViewTest alias Ecto.Adapters.SQL + alias Logflare.Backends alias Logflare.Google.BigQuery.SchemaUtils alias Logflare.SingleTenant + alias Logflare.SourceSchemas alias Logflare.Sources.Source.BigQuery.Schema alias Logflare.Utils.Tasks alias LogflareWeb.Source.SearchLV @@ -1343,6 +1345,71 @@ defmodule LogflareWeb.Source.SearchLVTest do end end + describe "single tenant searching with postgres backend" do + TestUtils.setup_single_tenant(seed_user: true, backend_type: :postgres) + + setup do + start_supervised!(Logflare.SystemMetricsSup) + + user = SingleTenant.get_default_user() + source = insert(:source, user: user) + plan = SingleTenant.get_default_plan() + + assert :ok = Backends.ensure_source_sup_started(source) + + matching_message = "postgres-live-search-match-#{System.unique_integer([:positive])}" + non_matching_message = "postgres-live-search-miss-#{System.unique_integer([:positive])}" + + assert {:ok, 2} = + Backends.ingest_logs( + [ + %{"event_message" => matching_message}, + %{"event_message" => non_matching_message} + ], + source + ) + + bq_schema = TestUtils.build_bq_schema(%{"event_message" => matching_message}) + + assert {:ok, _source_schema} = + SourceSchemas.create_or_update_source_schema(source, %{ + bigquery_schema: bq_schema, + schema_flat_map: SchemaUtils.bq_schema_to_flat_typemap(bq_schema) + }) + + Cachex.clear(Logflare.SourceSchemas.Cache) + + %{ + user: user, + source: source, + plan: plan, + matching_message: matching_message + } + end + + setup [:setup_user_session] + + test "run a search against postgres backend", %{ + conn: conn, + source: source, + matching_message: matching_message + } do + {:ok, view, _html} = live(conn, Routes.live_path(conn, SearchLV, source.id)) + + %{executor_pid: search_executor_pid} = get_view_assigns(view) + allow_sandbox(search_executor_pid) + + render_change(view, :start_search, %{ + "querystring" => matching_message + }) + + view + |> TestUtils.wait_for_render("#logs-list-container li") + + assert view |> element("#logs-list-container") |> render() =~ matching_message + end + end + describe "source suggestion fields handling" do setup do user = insert(:user) From fca24e2f7aa1f50b4ae3177a72470d12e3f9526a Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 10 Mar 2026 11:08:53 +1000 Subject: [PATCH 16/44] ensure enter submits search --- assets/js/lql_editor_wrapper_hook.js | 189 ++++++++++----- assets/js/lql_language.js | 228 ++++++++++-------- assets/js/source_lv_hooks.js | 89 +++++-- .../live/search_live/form_components.ex | 48 ++-- .../live/search_live/logs_search_lv.ex | 93 ++++--- .../live/search_live/logs_search_lv_test.exs | 7 +- 6 files changed, 399 insertions(+), 255 deletions(-) diff --git a/assets/js/lql_editor_wrapper_hook.js b/assets/js/lql_editor_wrapper_hook.js index ee802dfc0..3d9177fec 100644 --- a/assets/js/lql_editor_wrapper_hook.js +++ b/assets/js/lql_editor_wrapper_hook.js @@ -20,103 +20,153 @@ const LqlEditorWrapper = { this._suggestedSearches = []; this._completionDisposable = null; this._editor = null; + this._editorDisposables = []; this._refreshSuggestionsTimer = null; - this._skipNextSavedSearchRequest = false; - - // Listen for CodeEditorHook's mount event (bubbles from inner element) - this.el.addEventListener("lme:editor_mounted", (event) => { + this._handleSubmitRequest = () => { + this.submitSearch(); + }; + this._handleEditorMounted = (event) => { const { editor } = event.detail; const standaloneEditor = editor.standalone_code_editor; + + if (this._editor === standaloneEditor && this._completionDisposable) { + return; + } + + this.disposeEditorBindings(); this._editor = standaloneEditor; const monaco = window.monaco; - // Register LQL language and set model language registerLqlLanguage(monaco); const model = standaloneEditor.getModel(); monaco.editor.setModelLanguage(model, "lql"); - // Register completion provider this._completionDisposable = registerLqlCompletionProvider( monaco, () => this._schemaFields, () => this._suggestedSearches, ); - standaloneEditor.addAction({ - id: "lql.dismissSavedSearchSuggest", - label: "Dismiss saved search suggest", - run: () => { - this._skipNextSavedSearchRequest = true; - - const suggestController = standaloneEditor.getContribution( - "editor.contrib.suggestController", - ); - - suggestController?.cancelSuggestWidget?.(); - }, - }); - - // Enter submits the form (only when suggest widget is NOT visible) standaloneEditor.addCommand( monaco.KeyCode.Enter, () => { - const form = this.el.closest("form"); - if (form) { - form.dispatchEvent( - new Event("submit", { bubbles: true, cancelable: true }), - ); - } + this.submitSearch(); }, "!suggestWidgetVisible", ); - // Sync value to hidden input on change - standaloneEditor.onDidChangeModelContent(() => { - const value = standaloneEditor.getValue(); - const hiddenInput = this.el.querySelector( - 'input[type="hidden"][name="search[querystring]"]', - ); - if (hiddenInput) { - hiddenInput.value = value; - hiddenInput.dispatchEvent(new Event("input", { bubbles: true })); - } + this._editorDisposables = [ + standaloneEditor.onDidChangeModelContent(() => { + const value = standaloneEditor.getValue(); + this.pushEvent("querystring_changed", { querystring: value }); - if (this._skipNextSavedSearchRequest) { - this._skipNextSavedSearchRequest = false; clearTimeout(this._refreshSuggestionsTimer); - return; + this._refreshSuggestionsTimer = window.setTimeout(() => { + this.refreshSuggestions(); + }, 100); + }), + standaloneEditor.onDidFocusEditorText(() => { + this.pushEvent( + "form_focus", + { value: standaloneEditor.getValue() }, + ({ suggestions = [] }) => { + this._suggestedSearches = Array.isArray(suggestions) + ? suggestions + : []; + + this.refreshSuggestions(); + }, + ); + }), + standaloneEditor.onDidBlurEditorText(() => { + this.pushEvent("form_blur", { value: standaloneEditor.getValue() }); + }), + ]; + }; + + this.el.addEventListener("lql:submit", this._handleSubmitRequest); + this.el.addEventListener("lme:editor_mounted", this._handleEditorMounted); + }, + + updated() { + this._schemaFields = parseSchemaFields(this.el.dataset.schemaFieldsJson); + this.restoreCursorToEndIfNeeded(); + }, + + restoreCursorToEndIfNeeded(attempt = 0) { + window.requestAnimationFrame(() => { + const serverQuerystring = this.el.dataset.querystring ?? ""; + const editorValue = this._editor?.getValue?.(); + const model = this._editor?.getModel?.(); + const position = this._editor?.getPosition?.(); + const suggestController = this._editor?.getContribution?.( + "editor.contrib.suggestController", + ); + + if (!this._editor || !model || !position) { + return; + } + + if (serverQuerystring !== editorValue) { + if (attempt < 6) { + window.setTimeout(() => { + this.restoreCursorToEndIfNeeded(attempt + 1); + }, 50); } - clearTimeout(this._refreshSuggestionsTimer); - this._refreshSuggestionsTimer = window.setTimeout(() => { - this.refreshSuggestions(); - }, 100); - }); + return; + } + + if (editorValue.length === 0) { + return; + } + + const endPosition = { + lineNumber: model.getLineCount(), + column: model.getLineMaxColumn(model.getLineCount()), + }; + const hasTextFocus = this._editor.hasTextFocus(); + + if (hasTextFocus && position.lineNumber === 1 && position.column === 1) { + this._editor.setPosition(endPosition); + suggestController?.cancelSuggestWidget?.(); + return; + } + + if (!hasTextFocus && attempt < 6) { + window.setTimeout(() => { + this.restoreCursorToEndIfNeeded(attempt + 1); + }, 50); + } + }); + }, - // Forward focus/blur to LiveView - standaloneEditor.onDidFocusEditorText(() => { - this.pushEvent( - "form_focus", - { value: standaloneEditor.getValue() }, - ({ suggestions = [] }) => { - this._suggestedSearches = Array.isArray(suggestions) - ? suggestions - : []; + collectRecommendedFields() { + const searchControl = + this.el.closest("#source-logs-search-control") || this.el.parentElement; + const fields = {}; - this.refreshSuggestions(); - }, - ); - }); + searchControl + ?.querySelectorAll('#recommended_fields input[name^="fields["]') + .forEach((input) => { + const match = input.name.match(/^fields\[(.+)\]$/); - standaloneEditor.onDidBlurEditorText(() => { - this.pushEvent("form_blur", { value: standaloneEditor.getValue() }); + if (match) { + fields[match[1]] = input.value; + } }); - }); + + return fields; }, - updated() { - this._schemaFields = parseSchemaFields(this.el.dataset.schemaFieldsJson); + submitSearch() { + const querystring = this._editor?.getValue?.() ?? ""; + + this.pushEvent("start_search", { + querystring, + fields: this.collectRecommendedFields(), + }); }, refreshSuggestions() { @@ -159,14 +209,23 @@ const LqlEditorWrapper = { this._editor?.getAction("editor.action.triggerSuggest").run(); }, - destroyed() { - clearTimeout(this._refreshSuggestionsTimer); + disposeEditorBindings() { + this._editorDisposables.forEach((disposable) => disposable?.dispose?.()); + this._editorDisposables = []; if (this._completionDisposable) { this._completionDisposable.dispose(); this._completionDisposable = null; } }, + + destroyed() { + clearTimeout(this._refreshSuggestionsTimer); + this.el.removeEventListener("lql:submit", this._handleSubmitRequest); + this.el.removeEventListener("lme:editor_mounted", this._handleEditorMounted); + this.disposeEditorBindings(); + this._editor = null; + }, }; export default LqlEditorWrapper; diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index a68088cec..95b8c02cb 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -14,17 +14,6 @@ export function registerLqlLanguage(monaco) { keywords: ["true", "false", "NULL"], - logLevels: [ - "emergency", - "alert", - "critical", - "error", - "warning", - "notice", - "info", - "debug", - ], - functions: [ "count", "countd", @@ -79,7 +68,7 @@ export function registerLqlLanguage(monaco) { [/(?:last|this)@\w+/, "number.date"], [/\b(?:today|yesterday|now)\b/, "number.date"], - // ISO-ish dates: 2024-01-15T00:00:00 or 2024-01-15 + // dates: 2024-01-15T00:00:00 or 2024-01-15 [ /\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?/, "number.date", @@ -102,7 +91,6 @@ export function registerLqlLanguage(monaco) { { cases: { "@keywords": "constant", - "@logLevels": "type.loglevel", "@default": "identifier", }, }, @@ -227,6 +215,13 @@ function buildKeywordSuggestions(monaco, token, range) { })); } +function hasSingleExactKeywordSuggestion(token, suggestions) { + return ( + suggestions.length === 1 && + suggestions[0].label.toLocaleLowerCase() === token.toLocaleLowerCase() + ); +} + function buildOperatorSuggestions(monaco, operatorPrefix, range) { return LQL_FILTER_OPERATORS.filter((operator) => operator.label.startsWith(operatorPrefix), @@ -298,63 +293,71 @@ export function hasLqlSuggestions( ); const tokenMatch = textUntilPosition.match(/(?:^|\s)(\S+)$/); const currentToken = tokenMatch ? tokenMatch[1] : null; + const metaMatch = textUntilPosition.match( + /(?:^|[\s:])(?:m|metadata)\.([\w.]*?)$/, + ); - if ( - currentToken && - textUntilPosition.length > 0 && - /^\S+$/.test(textUntilPosition) && - fullLine === textUntilPosition && - !["t:", "timestamp:"].includes(currentToken) - ) { + if (timestampValueMatch) { return ( - [...new Set(suggestedSearches)].some((querystring) => - querystring - .toLocaleLowerCase() - .startsWith(currentToken.toLocaleLowerCase()), - ) || - buildKeywordSuggestions( + buildTimestampSuggestions( { languages: { CompletionItemKind: completionKindStub, - CompletionItemInsertTextRule: completionItemInsertTextRuleStub, }, }, - currentToken, + timestampValueMatch[1], null, ).length > 0 ); } - if (timestampValueMatch) { - return buildTimestampSuggestions( - { - languages: { - CompletionItemKind: completionKindStub, - }, - }, - timestampValueMatch[1], - null, - ).length > 0; + if (metaMatch) { + return getMetadataSuggestionSegments(schemaFields, metaMatch[1]).length > 0; } if (operatorMatch) { - return buildOperatorSuggestions( + return ( + buildOperatorSuggestions( + { + languages: { + CompletionItemKind: completionKindStub, + }, + }, + `:${operatorMatch[1]}`, + null, + ).length > 0 + ); + } + + if ( + currentToken && + textUntilPosition.length > 0 && + /^\S+$/.test(textUntilPosition) && + fullLine === textUntilPosition && + !["t:", "timestamp:"].includes(currentToken) + ) { + const keywordSuggestions = buildKeywordSuggestions( { languages: { CompletionItemKind: completionKindStub, + CompletionItemInsertTextRule: completionItemInsertTextRuleStub, }, }, - `:${operatorMatch[1]}`, + currentToken, null, - ).length > 0; - } - - const metaMatch = textUntilPosition.match( - /(?:^|[\s:])(?:m|metadata)\.([\w.]*?)$/, - ); + ); + const hasSavedSearchMatches = [...new Set(suggestedSearches)].some( + (querystring) => + querystring + .toLocaleLowerCase() + .startsWith(currentToken.toLocaleLowerCase()), + ); - if (metaMatch) { - return getMetadataSuggestionSegments(schemaFields, metaMatch[1]).length > 0; + return ( + hasSavedSearchMatches || + (keywordSuggestions.length > 0 && + !hasSingleExactKeywordSuggestion(currentToken, keywordSuggestions)) + ); } if (/(?:^|[\s])$/.test(textUntilPosition)) { @@ -362,17 +365,20 @@ export function hasLqlSuggestions( } if (currentToken) { - return ( - buildKeywordSuggestions( - { - languages: { - CompletionItemKind: completionKindStub, - CompletionItemInsertTextRule: completionItemInsertTextRuleStub, - }, + const keywordSuggestions = buildKeywordSuggestions( + { + languages: { + CompletionItemKind: completionKindStub, + CompletionItemInsertTextRule: completionItemInsertTextRuleStub, }, - currentToken, - null, - ).length > 0 + }, + currentToken, + null, + ); + + return ( + keywordSuggestions.length > 0 && + !hasSingleExactKeywordSuggestion(currentToken, keywordSuggestions) ); } @@ -439,53 +445,10 @@ export function registerLqlCompletionProvider( kind: monaco.languages.CompletionItemKind.Snippet, detail: "saved search", insertText: querystring, - command: { - id: "lql.dismissSavedSearchSuggest", - title: "Dismiss saved search suggest", - }, range, sortText: `0-${String(index).padStart(3, "0")}`, })); - if ( - currentToken && - textUntilPosition.length > 0 && - /^\S+$/.test(textUntilPosition) && - fullLine === textUntilPosition && - !["t:", "timestamp:"].includes(currentToken) - ) { - const matchedSavedSearches = savedSearchQuerystrings.filter((querystring) => - querystring - .toLocaleLowerCase() - .startsWith(currentToken.toLocaleLowerCase()), - ); - const filteredSavedSearchQuerystrings = - matchedSavedSearches.length === 1 && - matchedSavedSearches[0].toLocaleLowerCase() === - currentToken.toLocaleLowerCase() - ? [] - : matchedSavedSearches; - const savedSearchSuggestions = buildSavedSearchSuggestions( - filteredSavedSearchQuerystrings, - lineRange, - ); - - const keywordSuggestions = buildKeywordSuggestions( - monaco, - currentToken, - lineRange, - ).map((suggestion, index) => ({ - ...suggestion, - sortText: `1-${String(index).padStart(3, "0")}`, - })); - - const suggestions = [...savedSearchSuggestions, ...keywordSuggestions]; - - if (suggestions.length > 0) { - return { suggestions }; - } - } - if (timestampValueMatch) { const timestampToken = timestampValueMatch[1]; const suggestions = buildTimestampSuggestions(monaco, timestampToken, { @@ -519,6 +482,58 @@ export function registerLqlCompletionProvider( } } + if ( + currentToken && + textUntilPosition.length > 0 && + /^\S+$/.test(textUntilPosition) && + fullLine === textUntilPosition && + !["t:", "timestamp:"].includes(currentToken) + ) { + const matchedSavedSearches = savedSearchQuerystrings.filter( + (querystring) => + querystring + .toLocaleLowerCase() + .startsWith(currentToken.toLocaleLowerCase()), + ); + const filteredSavedSearchQuerystrings = + matchedSavedSearches.length === 1 && + matchedSavedSearches[0].toLocaleLowerCase() === + currentToken.toLocaleLowerCase() + ? [] + : matchedSavedSearches; + const savedSearchSuggestions = buildSavedSearchSuggestions( + filteredSavedSearchQuerystrings, + lineRange, + ); + + const keywordSuggestions = buildKeywordSuggestions( + monaco, + currentToken, + lineRange, + ); + const filteredKeywordSuggestions = hasSingleExactKeywordSuggestion( + currentToken, + keywordSuggestions, + ) + ? [] + : keywordSuggestions; + const sortedKeywordSuggestions = filteredKeywordSuggestions.map( + (suggestion, index) => ({ + ...suggestion, + sortText: `1-${String(index).padStart(3, "0")}`, + }), + ); + + const suggestions = [ + ...savedSearchSuggestions, + ...sortedKeywordSuggestions, + ]; + + if (suggestions.length > 0) { + return { suggestions }; + } + } + // Check if we're in a metadata path context: m. or metadata. prefix const metaMatch = textUntilPosition.match( /(?:^|[\s:])(?:m|metadata)\.([\w.]*?)$/, @@ -536,13 +551,18 @@ export function registerLqlCompletionProvider( : ""; const fullPrefix = "metadata." + pathPrefix; - for (const segment of getMetadataSuggestionSegments(fields, typedPath)) { + for (const segment of getMetadataSuggestionSegments( + fields, + typedPath, + )) { const field = metadataFields.find( (item) => item.name === `${fullPrefix}${segment}` || item.name.startsWith(`${fullPrefix}${segment}.`), ); - const isLeaf = field ? field.name === `${fullPrefix}${segment}` : true; + const isLeaf = field + ? field.name === `${fullPrefix}${segment}` + : true; suggestions.push({ label: segment, @@ -588,7 +608,7 @@ export function registerLqlCompletionProvider( currentTokenRange, ); - if (suggestions.length > 0) { + if (!hasSingleExactKeywordSuggestion(currentToken, suggestions)) { return { suggestions }; } } diff --git a/assets/js/source_lv_hooks.js b/assets/js/source_lv_hooks.js index 4d367b459..e5101c1cd 100644 --- a/assets/js/source_lv_hooks.js +++ b/assets/js/source_lv_hooks.js @@ -146,11 +146,14 @@ hooks.BigQuerySqlQueryFormatter = { } hooks.SourceLogsSearch = { - updated() { + configureDateRangePicker() { const hook = this const $daterangepicker = $("#daterangepicker") + $daterangepicker.daterangepicker(datepickerConfig) - $daterangepicker.on("apply.daterangepicker", (e, picker) => { + $daterangepicker.off(".sourceLogsSearch") + + $daterangepicker.on("apply.daterangepicker.sourceLogsSearch", (e, picker) => { const tsClause = buildTsClause( picker.startDate, picker.endDate, @@ -161,43 +164,68 @@ hooks.SourceLogsSearch = { }) }) - - $daterangepicker.on("cancel.daterangepicker", (e, picker) => { + $daterangepicker.on("cancel.daterangepicker.sourceLogsSearch", () => { hook.pushEvent("soft_play", {}) }) + $daterangepicker.on("show.daterangepicker.sourceLogsSearch", () => { + hook.pushEvent("soft_pause", {}) + }) + }, + pushChartControlsUpdate() { + const chartPeriod = this.el.querySelector("#search_chart_period")?.value + const chartAggregate = this.el.querySelector("#search_chart_aggregate")?.value + + if (!chartPeriod || !chartAggregate) { + return + } + + this.pushEvent("chart_controls_update", { + chart_period: chartPeriod, + chart_aggregate: chartAggregate, + }) + }, + updated() { + this.configureDateRangePicker() + activateClipboardForSelector("#search-uri-query", { text: () => location.href, }) $("#search-uri-query").tooltip() - - $daterangepicker.on("show.daterangepicker", (e, picker) => { - hook.pushEvent("soft_pause", {}) - }) }, reconnected() { }, mounted() { const hook = this - const $daterangepicker = $("#daterangepicker") - $daterangepicker.daterangepicker(datepickerConfig) - $daterangepicker.on("apply.daterangepicker", (e, picker) => { - const tsClause = buildTsClause( - picker.startDate, - picker.endDate, - picker.chosenLabel - ) - hook.pushEvent("datetime_update", { - querystring: tsClause - }) - }) + this._handleSearchControlChange = (event) => { + if ( + event.target.id === "search_chart_period" || + event.target.id === "search_chart_aggregate" + ) { + this.pushChartControlsUpdate() + } + } + this._handleSearchControlKeydown = (event) => { + if ( + event.key !== "Enter" || + event.isComposing || + !event.target.matches('#recommended_fields input[name^="fields["]') + ) { + return + } - $daterangepicker.on("cancel.daterangepicker", (e, picker) => { - hook.pushEvent("soft_play", {}) - }) + const editorHook = this.el.querySelector("#lql-editor-hook") - $daterangepicker.on("show.daterangepicker", (e, picker) => { - hook.pushEvent("soft_pause", {}) - }) + if (!editorHook) { + return + } + + event.preventDefault() + editorHook.dispatchEvent(new CustomEvent("lql:submit")) + } + this.el.addEventListener("change", this._handleSearchControlChange) + this.el.addEventListener("keydown", this._handleSearchControlKeydown) + + this.configureDateRangePicker() activateClipboardForSelector("#search-uri-query", { text: () => location.href, @@ -226,6 +254,15 @@ hooks.SourceLogsSearch = { } }, 250) }, + destroyed() { + if (this._handleSearchControlChange) { + this.el.removeEventListener("change", this._handleSearchControlChange) + } + + if (this._handleSearchControlKeydown) { + this.el.removeEventListener("keydown", this._handleSearchControlKeydown) + } + }, }; /* Listens for a `scrollIntoView` event on the element diff --git a/lib/logflare_web/live/search_live/form_components.ex b/lib/logflare_web/live/search_live/form_components.ex index 05674caec..a9817ccf7 100644 --- a/lib/logflare_web/live/search_live/form_components.ex +++ b/lib/logflare_web/live/search_live/form_components.ex @@ -11,7 +11,6 @@ defmodule LogflareWeb.SearchLive.FormComponents do alias Logflare.Utils alias Logflare.Sources.Source - attr :form, Phoenix.HTML.Form, required: true attr :lql_rules, :list, required: true attr :chart_aggregate_enabled?, :boolean, required: true @@ -22,33 +21,29 @@ defmodule LogflareWeb.SearchLive.FormComponents do Chart period:
- {select(@form, :chart_period, ["day", "hour", "minute", "second"], - selected: Rules.get_chart_period(@lql_rules, "minute"), - class: "form-control form-control-sm" - )} +
Aggregate:
<%= if @chart_aggregate_enabled? do %> - {select( - @form, - :chart_aggregate, - ["sum", "avg", "max", "p50", "p95", "p99", "count"], - selected: Rules.get_chart_aggregate(@lql_rules, "count"), - class: "form-control form-control-sm" - )} + <% else %> - {select( - @form, - :chart_aggregate, - ["count"], - selected: "count", - class: "form-control form-control-sm", - style: "pointer-events: none;" - )} + <% end %>
@@ -152,24 +147,19 @@ defmodule LogflareWeb.SearchLive.FormComponents do <.recommended_field_inputs fields={Source.recommended_query_fields(@source)} id_prefix="search-field" />
-
+
{hidden_input(f, :querystring, value: @querystring)}
- {text_input(f, :search_timezone, - class: "d-none", - value: @search_timezone, - id: "search-timezone" - )}
- <%= submit disabled: @loading, id: "search", class: "btn btn-primary" do %> +
<.navigation_buttons tailing?={@tailing?} uri_params={@uri_params} /> @@ -177,7 +167,7 @@ defmodule LogflareWeb.SearchLive.FormComponents do <.action_buttons source={@source} user={@user} has_results?={@has_results?} />
- <.chart_controls form={f} lql_rules={@lql_rules} chart_aggregate_enabled?={search_agg_controls_enabled?(@lql_rules)} /> + <.chart_controls lql_rules={@lql_rules} chart_aggregate_enabled?={search_agg_controls_enabled?(@lql_rules)} />
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 42e1ef344..494b17c6f 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -357,7 +357,7 @@ defmodule LogflareWeb.Source.SearchLV do def handle_event( "start_search", - %{"search" => %{"querystring" => qs}} = params, + %{"querystring" => qs, "fields" => fields} = _params, %{assigns: prev_assigns} = socket ) do schema_flatmap = SourceSchemas.source_schema_flatmap_or_default(socket.assigns.source) @@ -368,8 +368,8 @@ defmodule LogflareWeb.Source.SearchLV do qs = append_fields_rules(qs, Map.get(params, "fields", %{}), schema_flatmap) socket = - assign_new_search_with_qs( - socket, + socket + |> assign_new_search_with_qs( %{querystring: qs, tailing?: prev_assigns.tailing?}, schema_flatmap ) @@ -377,6 +377,14 @@ defmodule LogflareWeb.Source.SearchLV do {:noreply, socket} end + def handle_event( + "start_search", + %{"search" => %{"querystring" => qs}} = params, + %{assigns: prev_assigns} = socket + ) do + start_search(socket, qs, Map.get(params, "fields", %{}), prev_assigns.tailing?) + end + def handle_event(direction, _, socket) when direction in ["backwards", "forwards"] do rules = socket.assigns.lql_rules @@ -439,37 +447,35 @@ defmodule LogflareWeb.Source.SearchLV do {:noreply, socket} end - def handle_event("form_update" = _ev, %{"search" => search}, %{assigns: prev_assigns} = socket) do - new_qs = search["querystring"] - new_chart_agg = String.to_existing_atom(search["chart_aggregate"]) - new_chart_period = String.to_existing_atom(search["chart_period"]) - socket = assign(socket, :querystring, new_qs) - - prev_chart_rule = - Rules.get_chart_rule(prev_assigns.lql_rules) || Rules.default_chart_rule() + def handle_event("querystring_changed", %{"querystring" => qs}, socket) do + {:noreply, assign(socket, :querystring, qs)} + end + def handle_event( + "chart_controls_update", + %{"chart_aggregate" => new_chart_agg, "chart_period" => new_chart_period}, + socket + ) do socket = - if new_chart_agg != prev_chart_rule.aggregate or - new_chart_period != prev_chart_rule.period do - lql_rules = - Lql.Rules.update_chart_rule( - prev_assigns.lql_rules, - Lql.Rules.default_chart_rule(), - %{aggregate: new_chart_agg, period: new_chart_period} - ) + maybe_update_chart_controls( + socket, + String.to_existing_atom(new_chart_agg), + String.to_existing_atom(new_chart_period) + ) - qs = Lql.encode!(lql_rules) + {:noreply, socket} + end - socket - |> assign_querystring(qs) - |> assign(:lql_rules, lql_rules) - |> assign(:loading, true) - |> assign(:chart_loading, true) - |> clear_flash() - |> push_patch_with_params(%{querystring: qs, tailing?: prev_assigns.tailing?}) - else - socket - end + def handle_event("form_update" = _ev, %{"search" => search}, socket) do + new_qs = search["querystring"] + + socket = + socket + |> assign(:querystring, new_qs) + |> maybe_update_chart_controls( + String.to_existing_atom(search["chart_aggregate"]), + String.to_existing_atom(search["chart_period"]) + ) {:noreply, socket} end @@ -598,6 +604,33 @@ defmodule LogflareWeb.Source.SearchLV do end end + defp maybe_update_chart_controls(socket, new_chart_agg, new_chart_period) do + prev_chart_rule = + Lql.Rules.get_chart_rule(socket.assigns.lql_rules) || Lql.Rules.default_chart_rule() + + if new_chart_agg != prev_chart_rule.aggregate or + new_chart_period != prev_chart_rule.period do + lql_rules = + Lql.Rules.update_chart_rule( + socket.assigns.lql_rules, + Lql.Rules.default_chart_rule(), + %{aggregate: new_chart_agg, period: new_chart_period} + ) + + qs = Lql.encode!(lql_rules) + + socket + |> assign_querystring(qs) + |> assign(:lql_rules, lql_rules) + |> assign(:loading, true) + |> assign(:chart_loading, true) + |> clear_flash() + |> push_patch_with_params(%{querystring: qs, tailing?: socket.assigns.tailing?}) + else + socket + end + end + def handle_info(:soft_pause = ev, socket) do soft_pause(ev, socket) end 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 b3a82f781..d5b99b8d4 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 @@ -1938,6 +1938,11 @@ defmodule LogflareWeb.Source.SearchLVTest do end def find_querystring(html) do - find_search_form_value(html, "#search_querystring") + {:ok, document} = Floki.parse_document(html) + + document + |> Floki.find("#lql-editor-hook") + |> Floki.attribute("data-querystring") + |> hd() end end From a4e02ef7908bd765c28ef7676bc41d201119e52f Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 10 Mar 2026 14:22:27 +1000 Subject: [PATCH 17/44] use SavedSearches.Cache --- lib/logflare_web/live/search_live/logs_search_lv.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 494b17c6f..fc764fdfa 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -435,7 +435,7 @@ defmodule LogflareWeb.Source.SearchLV do suggestions = socket.assigns.source.id - |> SavedSearches.list_saved_searches_by_source() + |> SavedSearches.Cache.list_saved_searches_by_source() |> Enum.map(& &1.querystring) {:reply, %{suggestions: suggestions}, socket} From a591794377a00e6876d1cc21b326e0cc290b3336 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Fri, 27 Mar 2026 11:49:29 +1000 Subject: [PATCH 18/44] test tidy up --- .../live/search_live/logs_search_lv_test.exs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) 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 e505b74f2..b3a82f781 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 @@ -1355,11 +1355,14 @@ defmodule LogflareWeb.Source.SearchLVTest do source = insert(:source, user: user) plan = SingleTenant.get_default_plan() - assert :ok = Backends.ensure_source_sup_started(source) - matching_message = "postgres-live-search-match-#{System.unique_integer([:positive])}" non_matching_message = "postgres-live-search-miss-#{System.unique_integer([:positive])}" + bq_schema = TestUtils.build_bq_schema(%{"event_message" => matching_message}) + insert(:source_schema, source: source, bigquery_schema: bq_schema) + + assert :ok = Backends.ensure_source_sup_started(source) + assert {:ok, 2} = Backends.ingest_logs( [ @@ -1369,16 +1372,6 @@ defmodule LogflareWeb.Source.SearchLVTest do source ) - bq_schema = TestUtils.build_bq_schema(%{"event_message" => matching_message}) - - assert {:ok, _source_schema} = - SourceSchemas.create_or_update_source_schema(source, %{ - bigquery_schema: bq_schema, - schema_flat_map: SchemaUtils.bq_schema_to_flat_typemap(bq_schema) - }) - - Cachex.clear(Logflare.SourceSchemas.Cache) - %{ user: user, source: source, From 25a78ffe9c7ae8241e08bbe0aa7cdaa8880492b1 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Tue, 10 Mar 2026 15:35:48 +1000 Subject: [PATCH 19/44] refactor: lql completions --- assets/js/lql_editor_wrapper_hook.js | 55 ++++--------- assets/js/lql_language.js | 116 --------------------------- 2 files changed, 14 insertions(+), 157 deletions(-) diff --git a/assets/js/lql_editor_wrapper_hook.js b/assets/js/lql_editor_wrapper_hook.js index 3d9177fec..c2e38f63e 100644 --- a/assets/js/lql_editor_wrapper_hook.js +++ b/assets/js/lql_editor_wrapper_hook.js @@ -1,5 +1,4 @@ import { - hasLqlSuggestions, registerLqlLanguage, registerLqlCompletionProvider, } from "./lql_language"; @@ -7,11 +6,7 @@ import { const parseSchemaFields = (schemaFieldsJson) => { if (!schemaFieldsJson) return []; - try { - return JSON.parse(schemaFieldsJson); - } catch { - return []; - } + return JSON.parse(schemaFieldsJson); }; const LqlEditorWrapper = { @@ -21,7 +16,6 @@ const LqlEditorWrapper = { this._completionDisposable = null; this._editor = null; this._editorDisposables = []; - this._refreshSuggestionsTimer = null; this._handleSubmitRequest = () => { this.submitSearch(); }; @@ -60,11 +54,6 @@ const LqlEditorWrapper = { standaloneEditor.onDidChangeModelContent(() => { const value = standaloneEditor.getValue(); this.pushEvent("querystring_changed", { querystring: value }); - - clearTimeout(this._refreshSuggestionsTimer); - this._refreshSuggestionsTimer = window.setTimeout(() => { - this.refreshSuggestions(); - }, 100); }), standaloneEditor.onDidFocusEditorText(() => { this.pushEvent( @@ -75,7 +64,14 @@ const LqlEditorWrapper = { ? suggestions : []; - this.refreshSuggestions(); + const editorValue = standaloneEditor.getValue() ?? ""; + + if ( + this._suggestedSearches.length > 0 || + editorValue.trim().length === 0 + ) { + this.refreshSuggestions(); + } }, ); }), @@ -170,43 +166,18 @@ const LqlEditorWrapper = { }, refreshSuggestions() { - const model = this._editor?.getModel(); - const position = this._editor?.getPosition(); const suggestController = this._editor?.getContribution?.( "editor.contrib.suggestController", ); suggestController?.cancelSuggestWidget?.(); - if (!model || !position) { - return; - } - - const textUntilPosition = model.getValueInRange({ - startLineNumber: position.lineNumber, - startColumn: 1, - endLineNumber: position.lineNumber, - endColumn: position.column, - }); - const fullLine = model.getLineContent(position.lineNumber); - - if ( - !hasLqlSuggestions( - textUntilPosition, - fullLine, - this._schemaFields, - this._suggestedSearches, - ) - ) { - return; - } - if (suggestController?.triggerSuggest) { suggestController.triggerSuggest(); return; } - this._editor?.getAction("editor.action.triggerSuggest").run(); + this._editor?.getAction("editor.action.triggerSuggest")?.run(); }, disposeEditorBindings() { @@ -220,9 +191,11 @@ const LqlEditorWrapper = { }, destroyed() { - clearTimeout(this._refreshSuggestionsTimer); this.el.removeEventListener("lql:submit", this._handleSubmitRequest); - this.el.removeEventListener("lme:editor_mounted", this._handleEditorMounted); + this.el.removeEventListener( + "lme:editor_mounted", + this._handleEditorMounted, + ); this.disposeEditorBindings(); this._editor = null; }, diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index 95b8c02cb..3c6529789 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -276,122 +276,6 @@ function getMetadataSuggestionSegments(fields, typedPath) { return suggestions; } -const completionKindStub = { Keyword: "keyword", Operator: "operator" }; -const completionItemInsertTextRuleStub = { InsertAsSnippet: "InsertAsSnippet" }; - -export function hasLqlSuggestions( - textUntilPosition, - fullLine, - schemaFields, - suggestedSearches, -) { - const operatorMatch = textUntilPosition.match( - /(?:^|\s)-?(?:(?:m|metadata)\.[\w.]+|(?:t|timestamp)|[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*):([@><~=]*)$/, - ); - const timestampValueMatch = textUntilPosition.match( - /(?:^|\s)(?:t|timestamp):([a-zA-Z@]*)$/, - ); - const tokenMatch = textUntilPosition.match(/(?:^|\s)(\S+)$/); - const currentToken = tokenMatch ? tokenMatch[1] : null; - const metaMatch = textUntilPosition.match( - /(?:^|[\s:])(?:m|metadata)\.([\w.]*?)$/, - ); - - if (timestampValueMatch) { - return ( - buildTimestampSuggestions( - { - languages: { - CompletionItemKind: completionKindStub, - }, - }, - timestampValueMatch[1], - null, - ).length > 0 - ); - } - - if (metaMatch) { - return getMetadataSuggestionSegments(schemaFields, metaMatch[1]).length > 0; - } - - if (operatorMatch) { - return ( - buildOperatorSuggestions( - { - languages: { - CompletionItemKind: completionKindStub, - }, - }, - `:${operatorMatch[1]}`, - null, - ).length > 0 - ); - } - - if ( - currentToken && - textUntilPosition.length > 0 && - /^\S+$/.test(textUntilPosition) && - fullLine === textUntilPosition && - !["t:", "timestamp:"].includes(currentToken) - ) { - const keywordSuggestions = buildKeywordSuggestions( - { - languages: { - CompletionItemKind: completionKindStub, - CompletionItemInsertTextRule: completionItemInsertTextRuleStub, - }, - }, - currentToken, - null, - ); - const hasSavedSearchMatches = [...new Set(suggestedSearches)].some( - (querystring) => - querystring - .toLocaleLowerCase() - .startsWith(currentToken.toLocaleLowerCase()), - ); - - return ( - hasSavedSearchMatches || - (keywordSuggestions.length > 0 && - !hasSingleExactKeywordSuggestion(currentToken, keywordSuggestions)) - ); - } - - if (/(?:^|[\s])$/.test(textUntilPosition)) { - return true; - } - - if (currentToken) { - const keywordSuggestions = buildKeywordSuggestions( - { - languages: { - CompletionItemKind: completionKindStub, - CompletionItemInsertTextRule: completionItemInsertTextRuleStub, - }, - }, - currentToken, - null, - ); - - return ( - keywordSuggestions.length > 0 && - !hasSingleExactKeywordSuggestion(currentToken, keywordSuggestions) - ); - } - - return false; -} - -/** - * Registers a CompletionItemProvider for the LQL language. - * @param {object} monaco - The monaco-editor namespace - * @param {function} getFields - Returns current schema fields array [{name, type}] - * @param {function} getSuggestedSearches - Returns saved-search suggestions [querystring] - * @returns {IDisposable} The disposable for the registered provider - */ export function registerLqlCompletionProvider( monaco, getFields, From 6cfb3c7d561a82bf1a5064645f37989a08f002e1 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Fri, 27 Mar 2026 14:13:14 +1000 Subject: [PATCH 20/44] refactor: move postgres result normalisation to PostgresAdaptor --- .../backends/adaptor/postgres_adaptor.ex | 16 ++++- lib/logflare/logs/search_operations.ex | 67 ++++++----------- .../adaptor/postgres_adaptor_test.exs | 12 ++-- test/logflare/logs/search_operations_test.exs | 71 ++++++------------- 4 files changed, 60 insertions(+), 106 deletions(-) diff --git a/lib/logflare/backends/adaptor/postgres_adaptor.ex b/lib/logflare/backends/adaptor/postgres_adaptor.ex index 4771c8ed2..2dcbadb5f 100644 --- a/lib/logflare/backends/adaptor/postgres_adaptor.ex +++ b/lib/logflare/backends/adaptor/postgres_adaptor.ex @@ -258,6 +258,14 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptor do end @spec nested_map_update(term()) :: term() + defp nested_map_update(%DateTime{} = value), do: DateTime.to_unix(value, :microsecond) + + defp nested_map_update(%NaiveDateTime{} = value) do + value |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_unix(:microsecond) + end + + defp nested_map_update(%Date{} = value), do: Date.to_iso8601(value) + defp nested_map_update(value) when is_struct(value), do: value defp nested_map_update(value) when is_map(value), @@ -267,11 +275,15 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptor do defp nested_map_update(value), do: value + defp nested_map_update({key, value}, acc) when is_struct(value) do + Map.put(acc, to_string(key), nested_map_update(value)) + end + defp nested_map_update({key, value}, acc) when is_map(value) do - Map.put(acc, key, [nested_map_update(value)]) + Map.put(acc, to_string(key), [nested_map_update(value)]) end defp nested_map_update({key, value}, acc) do - Map.put(acc, key, nested_map_update(value)) + Map.put(acc, to_string(key), nested_map_update(value)) end end diff --git a/lib/logflare/logs/search_operations.ex b/lib/logflare/logs/search_operations.ex index dba3dfb04..746180832 100644 --- a/lib/logflare/logs/search_operations.ex +++ b/lib/logflare/logs/search_operations.ex @@ -49,6 +49,8 @@ defmodule Logflare.Logs.SearchOperations do @spec do_query(SO.t()) :: SO.t() def do_query(%SO{} = so) do with {:ok, response} <- execute_backend_query(so) do + response = normalize_query_result(so, response) + so |> SearchUtils.put_result(:query_result, response) |> SearchUtils.put_result(:rows, response.rows) @@ -59,6 +61,13 @@ defmodule Logflare.Logs.SearchOperations do end end + @spec normalize_query_result(SO.t(), QueryResult.t()) :: QueryResult.t() + defp normalize_query_result(%SO{backend_type: :postgres, type: type}, %QueryResult{} = response) do + normalize_postgres_response(response, type) + end + + defp normalize_query_result(%SO{}, %QueryResult{} = response), do: response + @spec execute_backend_query(SO.t()) :: {:ok, map()} | {:error, term()} defp execute_backend_query(%SO{backend_type: :postgres} = so) do backend = postgres_backend(so) @@ -605,57 +614,21 @@ defmodule Logflare.Logs.SearchOperations do end end - @spec normalize_postgres_response([term()], :events | :aggregates) :: map() - defp normalize_postgres_response(rows, type) do - normalized_rows = Enum.map(rows, &normalize_postgres_row(&1, type)) + @spec normalize_postgres_response(QueryResult.t(), :events | :aggregates) :: QueryResult.t() + defp normalize_postgres_response(%QueryResult{} = response, :events), do: response - %{ - rows: normalized_rows, - total_rows: length(normalized_rows), - total_bytes_processed: 0 - } - end - - defp normalize_postgres_row(%{} = row, :events) do - Map.new(row, fn {key, value} -> - {to_string(key), normalize_postgres_value(key, value)} - end) - end - - defp normalize_postgres_row(%{} = row, :aggregates) do - row - |> Enum.reduce(%{}, fn {key, value}, acc -> - key = to_string(key) - key = if key == "count", do: "value", else: key - Map.put(acc, key, normalize_postgres_value(key, value)) - end) - end - - defp normalize_postgres_row(other, _type), do: other - - defp normalize_postgres_value(_key, %DateTime{} = value), - do: DateTime.to_unix(value, :microsecond) - - defp normalize_postgres_value(_key, %NaiveDateTime{} = value) do - value - |> DateTime.from_naive!("Etc/UTC") - |> DateTime.to_unix(:microsecond) - end - - defp normalize_postgres_value(_key, %Date{} = value), do: Date.to_iso8601(value) - - defp normalize_postgres_value(_key, %{} = value) do - Map.new(value, fn {nested_key, nested_value} -> - {to_string(nested_key), normalize_postgres_value(nested_key, nested_value)} - end) - end + defp normalize_postgres_response(%QueryResult{} = response, :aggregates) do + rows = + Enum.map(response.rows, fn row -> + case Map.pop(row, "count") do + {nil, _row} -> row + {count, row} -> Map.put(row, "value", count) + end + end) - defp normalize_postgres_value(_key, value) when is_list(value) do - Enum.map(value, &normalize_postgres_value(nil, &1)) + %QueryResult{response | rows: rows} end - defp normalize_postgres_value(_key, value), do: value - defp normalize_aggregate_timestamp([timestamp]), do: normalize_aggregate_timestamp(timestamp) defp normalize_aggregate_timestamp(timestamp), do: timestamp diff --git a/test/logflare/backends/adaptor/postgres_adaptor_test.exs b/test/logflare/backends/adaptor/postgres_adaptor_test.exs index ff27c26c6..a00b8a723 100644 --- a/test/logflare/backends/adaptor/postgres_adaptor_test.exs +++ b/test/logflare/backends/adaptor/postgres_adaptor_test.exs @@ -125,11 +125,7 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptorTest do "nested" => [ %{ "host" => "db-default", - "parsed" => [ - %{ - "elements" => [%{"meta" => [%{"data" => "date"}]}] - } - ] + "parsed" => [%{"elements" => [%{"meta" => [%{"data" => "date"}]}]}] } ] } @@ -148,11 +144,13 @@ defmodule Logflare.Backends.Adaptor.PostgresAdaptorTest do query = from(l in PostgresAdaptor.table_name(source), select: count(l.id)) assert {:ok, %QueryResult{rows: [1]}} = PostgresAdaptor.execute_query(backend, query, []) - # struct results are not impacted by metadata transformations + # NaiveDateTime results are converted to unix microseconds query = from(l in PostgresAdaptor.table_name(source), select: l.timestamp) - assert {:ok, %QueryResult{rows: [%NaiveDateTime{}]}} = + assert {:ok, %QueryResult{rows: [timestamp]}} = PostgresAdaptor.execute_query(backend, query, []) + + assert is_integer(timestamp) end end diff --git a/test/logflare/logs/search_operations_test.exs b/test/logflare/logs/search_operations_test.exs index c9088636f..5e20c3251 100644 --- a/test/logflare/logs/search_operations_test.exs +++ b/test/logflare/logs/search_operations_test.exs @@ -472,14 +472,24 @@ defmodule Logflare.Logs.SearchOperationsTest do [backend: backend, base_so: base_so] end - test "do_query/1 uses Postgres backend adaptor and normalizes event rows", %{ + test "do_query/1 uses Postgres backend adaptor and passes through event rows", %{ backend: backend, base_so: base_so } do - timestamp = ~U[2026-01-29 05:13:48.748909Z] - naive_timestamp = ~N[2026-01-29 05:14:48.748909] - nested_timestamp = ~N[2026-01-29 05:15:48.748909] - date = ~D[2026-01-29] + timestamp_us = DateTime.to_unix(~U[2026-01-29 05:13:48.748909Z], :microsecond) + inserted_at_us = DateTime.to_unix(~U[2026-01-29 05:14:48.748909Z], :microsecond) + seen_at_us = DateTime.to_unix(~U[2026-01-29 05:15:48.748909Z], :microsecond) + + rows = [ + %{ + "event_message" => "postgres event", + "timestamp" => timestamp_us, + "inserted_at" => inserted_at_us, + "log_date" => "2026-01-29", + "metadata" => %{"level" => "error", "seen_at" => seen_at_us}, + "tags" => ["2026-01-29", inserted_at_us] + } + ] Backends |> expect(:get_default_backend, fn user -> @@ -492,17 +502,7 @@ defmodule Logflare.Logs.SearchOperationsTest do assert opts == [query_type: :search] assert %Ecto.Query{} = query - {:ok, - [ - %{ - event_message: "postgres event", - timestamp: timestamp, - inserted_at: naive_timestamp, - log_date: date, - metadata: %{level: "error", seen_at: nested_timestamp}, - tags: [date, naive_timestamp] - } - ]} + {:ok, QueryResult.new(rows, %{total_rows: length(rows)})} end) PostgresAdaptor @@ -514,35 +514,7 @@ defmodule Logflare.Logs.SearchOperationsTest do assert result_so.sql_string == "SELECT * FROM test_table" assert result_so.sql_params == ["param"] - - assert result_so.rows == [ - %{ - "event_message" => "postgres event", - "timestamp" => DateTime.to_unix(timestamp, :microsecond), - "inserted_at" => - DateTime.to_unix( - DateTime.from_naive!(naive_timestamp, "Etc/UTC"), - :microsecond - ), - "log_date" => "2026-01-29", - "metadata" => %{ - "level" => "error", - "seen_at" => - DateTime.to_unix( - DateTime.from_naive!(nested_timestamp, "Etc/UTC"), - :microsecond - ) - }, - "tags" => [ - "2026-01-29", - DateTime.to_unix( - DateTime.from_naive!(naive_timestamp, "Etc/UTC"), - :microsecond - ) - ] - } - ] - + assert result_so.rows == rows refute result_so.error end @@ -560,19 +532,20 @@ defmodule Logflare.Logs.SearchOperationsTest do assert result_so.error == :postgres_failed end - test "do_query/1 normalizes aggregate postgres rows and process_query_result/1 adds datetime", + test "do_query/1 renames count to value for aggregates and process_query_result/1 adds datetime", %{ backend: backend, base_so: base_so } do - timestamp = ~N[2026-01-29 05:13:48.748909] + unix_timestamp = DateTime.to_unix(~U[2026-01-29 05:13:48.748909Z], :microsecond) Backends |> expect(:get_default_backend, fn _user -> backend end) PostgresAdaptor |> expect(:execute_query, fn ^backend, %Ecto.Query{}, [query_type: :search] -> - {:ok, [%{count: 2, timestamp: timestamp}]} + rows = [%{"count" => 2, "timestamp" => unix_timestamp}] + {:ok, QueryResult.new(rows, %{total_rows: length(rows)})} end) PostgresAdaptor @@ -585,8 +558,6 @@ defmodule Logflare.Logs.SearchOperationsTest do |> SearchOperations.do_query() |> SearchOperations.process_query_result() - unix_timestamp = DateTime.to_unix(DateTime.from_naive!(timestamp, "Etc/UTC"), :microsecond) - assert result_so.rows == [ %{ "value" => 2, From b987cd0837bfb7b46efb190ca4940da9cb2abab7 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Wed, 11 Mar 2026 08:21:38 +1000 Subject: [PATCH 21/44] set saved searches on mount --- assets/js/lql_editor_wrapper_hook.js | 136 ++++++++---------- .../live/search_live/form_components.ex | 19 ++- .../live/search_live/logs_search_lv.ex | 54 +++---- .../live/search_live/logs_search_lv_test.exs | 76 +++------- 4 files changed, 103 insertions(+), 182 deletions(-) diff --git a/assets/js/lql_editor_wrapper_hook.js b/assets/js/lql_editor_wrapper_hook.js index c2e38f63e..ff8f00c54 100644 --- a/assets/js/lql_editor_wrapper_hook.js +++ b/assets/js/lql_editor_wrapper_hook.js @@ -9,13 +9,25 @@ const parseSchemaFields = (schemaFieldsJson) => { return JSON.parse(schemaFieldsJson); }; +const parseSuggestedSearches = (suggestedSearchesJson) => { + if (!suggestedSearchesJson) return []; + + return JSON.parse(suggestedSearchesJson); +}; + const LqlEditorWrapper = { mounted() { this._schemaFields = parseSchemaFields(this.el.dataset.schemaFieldsJson); - this._suggestedSearches = []; + this._suggestedSearches = parseSuggestedSearches( + this.el.dataset.suggestedSearchesJson, + ); this._completionDisposable = null; this._editor = null; this._editorDisposables = []; + this._pendingServerValue = null; + this.handleEvent("set_lql_value", ({ value }) => { + this.handleServerValueEvent(value); + }); this._handleSubmitRequest = () => { this.submitSearch(); }; @@ -29,6 +41,7 @@ const LqlEditorWrapper = { this.disposeEditorBindings(); this._editor = standaloneEditor; + this.applyPendingEditorValue(); const monaco = window.monaco; @@ -56,24 +69,7 @@ const LqlEditorWrapper = { this.pushEvent("querystring_changed", { querystring: value }); }), standaloneEditor.onDidFocusEditorText(() => { - this.pushEvent( - "form_focus", - { value: standaloneEditor.getValue() }, - ({ suggestions = [] }) => { - this._suggestedSearches = Array.isArray(suggestions) - ? suggestions - : []; - - const editorValue = standaloneEditor.getValue() ?? ""; - - if ( - this._suggestedSearches.length > 0 || - editorValue.trim().length === 0 - ) { - this.refreshSuggestions(); - } - }, - ); + this.pushEvent("form_focus", { value: standaloneEditor.getValue() }); }), standaloneEditor.onDidBlurEditorText(() => { this.pushEvent("form_blur", { value: standaloneEditor.getValue() }); @@ -87,55 +83,62 @@ const LqlEditorWrapper = { updated() { this._schemaFields = parseSchemaFields(this.el.dataset.schemaFieldsJson); - this.restoreCursorToEndIfNeeded(); + this._suggestedSearches = parseSuggestedSearches( + this.el.dataset.suggestedSearchesJson, + ); }, - restoreCursorToEndIfNeeded(attempt = 0) { - window.requestAnimationFrame(() => { - const serverQuerystring = this.el.dataset.querystring ?? ""; - const editorValue = this._editor?.getValue?.(); - const model = this._editor?.getModel?.(); - const position = this._editor?.getPosition?.(); - const suggestController = this._editor?.getContribution?.( - "editor.contrib.suggestController", - ); + handleServerValueEvent(value) { + if (typeof value !== "string") { + return; + } - if (!this._editor || !model || !position) { - return; - } + if (!this._editor) { + this.setPendingEditorValue(value); + return; + } - if (serverQuerystring !== editorValue) { - if (attempt < 6) { - window.setTimeout(() => { - this.restoreCursorToEndIfNeeded(attempt + 1); - }, 50); - } + this.setEditorValue(value); + }, - return; - } + setPendingEditorValue(value) { + this._pendingServerValue = value; + }, - if (editorValue.length === 0) { - return; - } + applyPendingEditorValue() { + if (!this._editor || this._pendingServerValue === null) { + return; + } + + const value = this._pendingServerValue; + this._pendingServerValue = null; + this.setEditorValue(value); + }, + + setEditorValue(value) { + const currentValue = this._editor?.getValue?.(); + if (!this._editor || currentValue === value) { + return; + } + + const hadTextFocus = this._editor.hasTextFocus(); + const model = this._editor?.getModel?.(); + const suggestController = this._editor?.getContribution?.( + "editor.contrib.suggestController", + ); + + this._editor.setValue(value); + + if (hadTextFocus && model) { const endPosition = { lineNumber: model.getLineCount(), column: model.getLineMaxColumn(model.getLineCount()), }; - const hasTextFocus = this._editor.hasTextFocus(); - if (hasTextFocus && position.lineNumber === 1 && position.column === 1) { - this._editor.setPosition(endPosition); - suggestController?.cancelSuggestWidget?.(); - return; - } - - if (!hasTextFocus && attempt < 6) { - window.setTimeout(() => { - this.restoreCursorToEndIfNeeded(attempt + 1); - }, 50); - } - }); + this._editor.setPosition(endPosition); + suggestController?.cancelSuggestWidget(); + } }, collectRecommendedFields() { @@ -165,21 +168,6 @@ const LqlEditorWrapper = { }); }, - refreshSuggestions() { - const suggestController = this._editor?.getContribution?.( - "editor.contrib.suggestController", - ); - - suggestController?.cancelSuggestWidget?.(); - - if (suggestController?.triggerSuggest) { - suggestController.triggerSuggest(); - return; - } - - this._editor?.getAction("editor.action.triggerSuggest")?.run(); - }, - disposeEditorBindings() { this._editorDisposables.forEach((disposable) => disposable?.dispose?.()); this._editorDisposables = []; @@ -191,13 +179,7 @@ const LqlEditorWrapper = { }, destroyed() { - this.el.removeEventListener("lql:submit", this._handleSubmitRequest); - this.el.removeEventListener( - "lme:editor_mounted", - this._handleEditorMounted, - ); this.disposeEditorBindings(); - this._editor = null; }, }; diff --git a/lib/logflare_web/live/search_live/form_components.ex b/lib/logflare_web/live/search_live/form_components.ex index a9817ccf7..6a1ff3277 100644 --- a/lib/logflare_web/live/search_live/form_components.ex +++ b/lib/logflare_web/live/search_live/form_components.ex @@ -123,10 +123,9 @@ defmodule LogflareWeb.SearchLive.FormComponents do end end - attr :search_form, :any, required: true attr :querystring, :string, required: true attr :lql_schema_fields_json, :string, required: true - attr :search_timezone, :string, required: true + attr :saved_searches, :list, required: true attr :loading, :boolean, required: true attr :tailing?, :boolean, required: true attr :uri_params, :map, required: true @@ -139,17 +138,20 @@ defmodule LogflareWeb.SearchLive.FormComponents do attr :last_query_completed_at, :any, default: nil def search_controls(assigns) do + assigns = + assigns + |> assign(:saved_searches_json, JSON.encode!(assigns.saved_searches)) + ~H"""
- <.form :let={f} for={@search_form} action="#" phx-submit="start_search" phx-change="form_update" class="form-group"> +
<.recommended_field_inputs fields={Source.recommended_query_fields(@source)} id_prefix="search-field" />
-
+
- {hidden_input(f, :querystring, value: @querystring)}
@@ -170,7 +172,7 @@ defmodule LogflareWeb.SearchLive.FormComponents do <.chart_controls lql_rules={@lql_rules} chart_aggregate_enabled?={search_agg_controls_enabled?(@lql_rules)} />
- +
<.query_timing last_query_completed_at={@last_query_completed_at} />
@@ -182,15 +184,12 @@ defmodule LogflareWeb.SearchLive.FormComponents do LiveMonacoEditor.default_opts(), %{ "language" => "lql", - "theme" => "default", - "minimap" => %{"enabled" => false}, "lineNumbers" => "off", "glyphMargin" => false, "folding" => false, "lineDecorationsWidth" => 0, "lineNumbersMinChars" => 0, "wordWrap" => "off", - "scrollBeyondLastLine" => false, "scrollbar" => %{ "horizontal" => "hidden", "vertical" => "hidden", @@ -204,9 +203,7 @@ defmodule LogflareWeb.SearchLive.FormComponents do "suggest" => %{"enabled" => true, "showWords" => false}, "parameterHints" => %{"enabled" => false}, "quickSuggestions" => true, - "renderLineHighlight" => "none", "matchBrackets" => "never", - "fontSize" => 14, "fontFamily" => "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", "padding" => %{"top" => 5, "bottom" => 5}, 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 fc764fdfa..e67aa3f69 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -101,10 +101,9 @@ defmodule LogflareWeb.Source.SearchLV do uri_params: nil, uri: nil, lql_rules: [], - lql_schema_fields_json: schema_fields_json(source), + saved_searches: saved_searches(source), querystring: Map.get(params, "querystring", @default_qs), - force_query: Map.get(params, "force", "false") == "true", - search_form: to_form(%{}, as: :search) + force_query: Map.get(params, "force", "false") == "true" ) |> maybe_assign_user_timezone(team_user, user) end @@ -287,10 +286,9 @@ defmodule LogflareWeb.Source.SearchLV do )}
%{"querystring" => qs}} = params, - %{assigns: prev_assigns} = socket - ) do - start_search(socket, qs, Map.get(params, "fields", %{}), prev_assigns.tailing?) - end - def handle_event(direction, _, socket) when direction in ["backwards", "forwards"] do rules = socket.assigns.lql_rules @@ -432,13 +422,7 @@ defmodule LogflareWeb.Source.SearchLV do def handle_event("form_focus", %{"value" => _value}, socket) do send(self(), :soft_pause) - - suggestions = - socket.assigns.source.id - |> SavedSearches.Cache.list_saved_searches_by_source() - |> Enum.map(& &1.querystring) - - {:reply, %{suggestions: suggestions}, socket} + {:noreply, socket} end def handle_event("form_blur", %{"value" => _value}, socket) do @@ -466,20 +450,6 @@ defmodule LogflareWeb.Source.SearchLV do {:noreply, socket} end - def handle_event("form_update" = _ev, %{"search" => search}, socket) do - new_qs = search["querystring"] - - socket = - socket - |> assign(:querystring, new_qs) - |> maybe_update_chart_controls( - String.to_existing_atom(search["chart_aggregate"]), - String.to_existing_atom(search["chart_period"]) - ) - - {:noreply, socket} - end - def handle_event("datetime_update" = _ev, params, %{assigns: assigns} = socket) do ts_qs = Map.get(params, "querystring") period = Map.get(params, "period") @@ -525,10 +495,16 @@ defmodule LogflareWeb.Source.SearchLV do if SavedSearches.Cache.list_saved_searches_by_source(source.id) |> length() < limit do case SavedSearches.save_by_user(qs, lql_rules, source, tailing?) do {:ok, _saved_search} -> + saved_searches = + [qs | saved_searches(source)] + |> Enum.uniq() + |> Enum.sort_by(&String.downcase/1) + socket = socket |> put_flash(:info, "Search saved!") |> assign(:source, Sources.get_source_for_lv_param(source.id)) + |> assign(:saved_searches, saved_searches) {:noreply, socket} @@ -1206,7 +1182,7 @@ defmodule LogflareWeb.Source.SearchLV do defp assign_querystring(socket, qs) do socket |> assign(:querystring, qs) - |> LiveMonacoEditor.set_value(qs, to: "lql_query") + |> push_event("set_lql_value", %{value: qs}) end defp schema_fields_json(source), do: source |> lql_schema_fields() |> Jason.encode!() @@ -1223,6 +1199,12 @@ defmodule LogflareWeb.Source.SearchLV do end end + defp saved_searches(source) do + source.id + |> SavedSearches.Cache.list_saved_searches_by_source() + |> Enum.map(& &1.querystring) + end + defp format_schema_type({type, inner}), do: "#{type}[#{inner}]" defp format_schema_type(type), do: to_string(type) end 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 d5b99b8d4..a2ec98c09 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 @@ -14,12 +14,7 @@ defmodule LogflareWeb.Source.SearchLVTest do alias LogflareWeb.Source.SearchLV @endpoint LogflareWeb.Endpoint - @default_search_params %{ - "querystring" => "c:count(*) c:group_by(t::minute)", - "chart_period" => "minute", - "chart_aggregate" => "count", - "tailing?" => "false" - } + @default_querystring "c:count(*) c:group_by(t::minute)" defp setup_mocks(_ctx) do stub(GoogleApi.BigQuery.V2.Api.Jobs, :bigquery_jobs_query, fn _conn, _proj_id, opts -> @@ -649,20 +644,15 @@ defmodule LogflareWeb.Source.SearchLVTest do {:ok, TestUtils.gen_bq_response(%{"event_message" => "some error message"})} end) - render_change(view, :form_update, %{ - "search" => %{ - @default_search_params - | "querystring" => "c:count(*) c:group_by(t::minute) error crasher" - } + render_change(view, :querystring_changed, %{ + "querystring" => "c:count(*) c:group_by(t::minute) error crasher" }) view |> TestUtils.wait_for_render("#logs-list-container li") render_change(view, :start_search, %{ - "search" => %{ - "querystring" => "c:count(*) c:group_by(t::minute) error crasher" - } + "querystring" => "c:count(*) c:group_by(t::minute) error crasher" }) # wait for async search task to complete @@ -692,12 +682,7 @@ defmodule LogflareWeb.Source.SearchLVTest do {:ok, view, _html} = live(conn, Routes.live_path(conn, SearchLV, source.id)) render_change(view, :start_search, %{ - "search" => %{ - @default_search_params - | "querystring" => "c:countd(event_message) c:group_by(t::hour)", - "chart_aggregate" => "countd", - "chart_period" => "hour" - } + "querystring" => "c:countd(event_message) c:group_by(t::hour)" }) TestUtils.retry_assert(fn -> @@ -756,7 +741,7 @@ defmodule LogflareWeb.Source.SearchLVTest do TestUtils.retry_assert(fn -> render_change(view, :start_search, %{ - "search" => %{@default_search_params | "querystring" => "m.nested:test top:test"} + "querystring" => "m.nested:test top:test" }) view @@ -791,7 +776,7 @@ defmodule LogflareWeb.Source.SearchLVTest do |> TestUtils.wait_for_render("#logs-list-container li") render_change(view, :start_search, %{ - "search" => %{@default_search_params | "chart_period" => "day"} + "querystring" => @default_querystring }) # wait for async search task to complete @@ -828,12 +813,9 @@ defmodule LogflareWeb.Source.SearchLVTest do |> has_element?("#search_chart_period option[selected]", "second") # a chart period selected by the user is preserved, and search halted - render_change(view, :form_update, %{ - "search" => %{ - @default_search_params - | "querystring" => query, - "chart_period" => "day" - } + render_change(view, :chart_controls_update, %{ + "chart_aggregate" => "count", + "chart_period" => "day" }) assert view @@ -1333,7 +1315,7 @@ defmodule LogflareWeb.Source.SearchLVTest do # post-init fetching render_change(view, :start_search, %{ - "search" => %{@default_search_params | "querystring" => "somestring"} + "querystring" => "somestring" }) TestUtils.retry_assert(fn -> @@ -1431,10 +1413,7 @@ defmodule LogflareWeb.Source.SearchLVTest do view |> render_change(:start_search, %{ - "search" => %{ - @default_search_params - | "querystring" => "c:count(*) c:group_by(t::minute)" - } + "querystring" => @default_querystring }) flash = view |> element(".message .alert") |> render() @@ -1455,10 +1434,7 @@ defmodule LogflareWeb.Source.SearchLVTest do view |> render_change(:start_search, %{ - "search" => %{ - @default_search_params - | "querystring" => "c:count(*) c:group_by(t::minute) message" - } + "querystring" => "c:count(*) c:group_by(t::minute) message" }) refute view |> element(".message .alert") |> has_element?() @@ -1475,10 +1451,7 @@ defmodule LogflareWeb.Source.SearchLVTest do assert view |> render_change(:start_search, %{ - "search" => %{ - @default_search_params - | "querystring" => "c:count(*) c:group_by(t::minute) message" - } + "querystring" => "c:count(*) c:group_by(t::minute) message" }) |> Floki.parse_document!() |> Floki.find("div[role=alert]>span") @@ -1507,10 +1480,7 @@ defmodule LogflareWeb.Source.SearchLVTest do view |> render_change(:start_search, %{ - "search" => %{ - @default_search_params - | "querystring" => "c:count(*) c:group_by(t::minute)" - } + "querystring" => @default_querystring }) flash = view |> element(".message .alert") |> render() @@ -1531,10 +1501,7 @@ defmodule LogflareWeb.Source.SearchLVTest do view |> render_change(:start_search, %{ - "search" => %{ - @default_search_params - | "querystring" => "c:count(*) c:group_by(t::minute) metadata.level:error" - } + "querystring" => "c:count(*) c:group_by(t::minute) metadata.level:error" }) refute view |> element(".message .alert", "required") |> has_element?() @@ -1623,10 +1590,7 @@ defmodule LogflareWeb.Source.SearchLVTest do allow_sandbox(search_executor_pid) render_change(view, :start_search, %{ - "search" => %{ - @default_search_params - | "querystring" => "event_message:timeout c:count(*) c:group_by(t::minute)" - }, + "querystring" => "event_message:timeout c:count(*) c:group_by(t::minute)", "fields" => %{ "event_message" => "api-timeout", "metadata.request_id" => "" @@ -1677,11 +1641,7 @@ defmodule LogflareWeb.Source.SearchLVTest do view |> render_change(:start_search, %{ - "search" => %{ - @default_search_params - | "querystring" => "c:count(*) c:group_by(t::minute) message", - "tailing?" => "true" - } + "querystring" => "c:count(*) c:group_by(t::minute) message" }) refute get_view_assigns(view).tailing? From e53e043313fa8afb52be6281824953a5feb09bda Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Wed, 11 Mar 2026 15:58:48 +1000 Subject: [PATCH 22/44] refactor: handle querystring updates in updated() hook --- assets/js/lql_editor_wrapper_hook.js | 16 ++-------------- assets/js/source_lv_hooks.js | 11 +---------- .../live/search_live/logs_search_lv.ex | 14 +++++++------- 3 files changed, 10 insertions(+), 31 deletions(-) diff --git a/assets/js/lql_editor_wrapper_hook.js b/assets/js/lql_editor_wrapper_hook.js index ff8f00c54..0e18e8ab9 100644 --- a/assets/js/lql_editor_wrapper_hook.js +++ b/assets/js/lql_editor_wrapper_hook.js @@ -25,9 +25,6 @@ const LqlEditorWrapper = { this._editor = null; this._editorDisposables = []; this._pendingServerValue = null; - this.handleEvent("set_lql_value", ({ value }) => { - this.handleServerValueEvent(value); - }); this._handleSubmitRequest = () => { this.submitSearch(); }; @@ -86,25 +83,16 @@ const LqlEditorWrapper = { this._suggestedSearches = parseSuggestedSearches( this.el.dataset.suggestedSearchesJson, ); - }, - - handleServerValueEvent(value) { - if (typeof value !== "string") { - return; - } + const value = this.el.dataset.querystring ?? ""; if (!this._editor) { - this.setPendingEditorValue(value); + this._pendingServerValue = value; return; } this.setEditorValue(value); }, - setPendingEditorValue(value) { - this._pendingServerValue = value; - }, - applyPendingEditorValue() { if (!this._editor || this._pendingServerValue === null) { return; diff --git a/assets/js/source_lv_hooks.js b/assets/js/source_lv_hooks.js index e5101c1cd..3e5011f97 100644 --- a/assets/js/source_lv_hooks.js +++ b/assets/js/source_lv_hooks.js @@ -252,16 +252,7 @@ hooks.SourceLogsSearch = { const elapsed = new Date().getTime() / 1000 - lastQueryCompletedAt $("#last-query-completed-at span").text(elapsed.toFixed(1)) } - }, 250) - }, - destroyed() { - if (this._handleSearchControlChange) { - this.el.removeEventListener("change", this._handleSearchControlChange) - } - - if (this._handleSearchControlKeydown) { - this.el.removeEventListener("keydown", this._handleSearchControlKeydown) - } + }, 250); }, }; 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 e67aa3f69..66e484cc0 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -197,7 +197,7 @@ defmodule LogflareWeb.Source.SearchLV do |> assign(:chart_loading, true) |> assign(:tailing_initial?, true) |> assign(:lql_rules, lql_rules) - |> assign_querystring(qs) + |> assign(:querystring, qs) |> assign(:search_op_log_events, search_op_log_events) if connected?(socket) do @@ -214,12 +214,12 @@ defmodule LogflareWeb.Source.SearchLV do {:error, error} -> socket - |> assign_querystring(qs) + |> assign(:querystring, qs) |> error_socket(error) {:error, :field_not_found = type, suggested_querystring, error} -> socket - |> assign_querystring(qs) + |> assign(:querystring, qs) |> error_socket(type, suggested_querystring, error) end @@ -475,7 +475,7 @@ defmodule LogflareWeb.Source.SearchLV do socket |> assign(:tailing?, false) |> assign(:lql_rules, lql_list) - |> assign_querystring(qs) + |> assign(:querystring, qs) |> push_patch_with_params(%{querystring: qs, tailing?: false}) {:noreply, socket} @@ -596,7 +596,7 @@ defmodule LogflareWeb.Source.SearchLV do qs = Lql.encode!(lql_rules) socket - |> assign_querystring(qs) + |> assign(:querystring, qs) |> assign(:lql_rules, lql_rules) |> assign(:loading, true) |> assign(:chart_loading, true) @@ -763,7 +763,7 @@ defmodule LogflareWeb.Source.SearchLV do |> assign(:tailing_initial?, true) |> clear_flash() |> assign(:lql_rules, lql_rules) - |> assign_querystring(qs) + |> assign(:querystring, qs) |> push_patch_with_params(%{querystring: qs, tz: tz, tailing?: tailing?}) {:error, error} -> @@ -1071,7 +1071,7 @@ defmodule LogflareWeb.Source.SearchLV do qs = Lql.encode!(lql_rules) socket - |> assign_querystring(qs) + |> assign(:querystring, qs) |> assign(:lql_rules, lql_rules) end From 0d4d104e532a706e70a3ba91b2640eb5c9d185bf Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Wed, 11 Mar 2026 16:25:02 +1000 Subject: [PATCH 23/44] fix: test --- lib/logflare_web/live/search_live/logs_search_lv.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 66e484cc0..5aeabe66e 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -355,7 +355,7 @@ defmodule LogflareWeb.Source.SearchLV do def handle_event( "start_search", - %{"querystring" => qs, "fields" => fields} = _params, + %{"querystring" => qs} = params, %{assigns: prev_assigns} = socket ) do schema_flatmap = SourceSchemas.source_schema_flatmap_or_default(socket.assigns.source) From f7f0608bd1b70d348ed0dd3fc38696b9eb4e479f Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Wed, 11 Mar 2026 16:49:17 +1000 Subject: [PATCH 24/44] ui: style suggested fields, source show field to be consistent with monaco query field --- lib/logflare_web/live/search_live/form_components.ex | 8 +++++++- lib/logflare_web/templates/source/show.html.heex | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/logflare_web/live/search_live/form_components.ex b/lib/logflare_web/live/search_live/form_components.ex index 6a1ff3277..594567dcc 100644 --- a/lib/logflare_web/live/search_live/form_components.ex +++ b/lib/logflare_web/live/search_live/form_components.ex @@ -87,7 +87,12 @@ defmodule LogflareWeb.SearchLive.FormComponents do required
- + @@ -204,6 +209,7 @@ defmodule LogflareWeb.SearchLive.FormComponents do "parameterHints" => %{"enabled" => false}, "quickSuggestions" => true, "matchBrackets" => "never", + "tabIndex" => 0, "fontFamily" => "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", "padding" => %{"top" => 5, "bottom" => 5}, diff --git a/lib/logflare_web/templates/source/show.html.heex b/lib/logflare_web/templates/source/show.html.heex index 317a8291a..8e5e84a6c 100644 --- a/lib/logflare_web/templates/source/show.html.heex +++ b/lib/logflare_web/templates/source/show.html.heex @@ -65,7 +65,11 @@
- {text_input(f, :querystring, placeholder: "404", class: "form-control tw-mt-0", autofocus: true)} + {text_input(f, :querystring, + placeholder: "404", + class: "form-control tw-mt-0 tw-border-[#282c34] tw-bg-[#282c34] tw-font-mono tw-text-[#c4cad6] placeholder:tw-text-[#8c92a3] focus:tw-border-[#3e4451] focus:tw-bg-[#282c34] focus:tw-text-[#c4cad6]", + autofocus: true + )}
{hidden_input(f, :tailing?, value: "true")} From 40ccfdf505393b8d9959161976118382977ce6e0 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Thu, 12 Mar 2026 10:43:58 +1000 Subject: [PATCH 25/44] test: SearchLV query field has saved searches, schema fields --- .../live/search_live/logs_search_lv_test.exs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) 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 a2ec98c09..1b1bde3a8 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 @@ -7,6 +7,7 @@ defmodule LogflareWeb.Source.SearchLVTest do alias Ecto.Adapters.SQL alias Logflare.Backends alias Logflare.Google.BigQuery.SchemaUtils + alias Logflare.Google.BigQuery.SchemaUtils alias Logflare.SingleTenant alias Logflare.SourceSchemas alias Logflare.Sources.Source.BigQuery.Schema @@ -501,6 +502,69 @@ defmodule LogflareWeb.Source.SearchLVTest do assert querystring =~ "c:count(*) c:group_by(t::minute)" end + test "query field has schema fields and saved searches", %{ + conn: conn, + source: source + } do + query_a = "metadata.level:error" + query_b = "tags:active" + + insert(:saved_search, source: source, querystring: query_a) + insert(:saved_search, source: source, querystring: query_b) + + bq_schema = + TestUtils.build_bq_schema(%{ + "message" => "string", + "metadata" => %{ + "flags" => [true], + "store" => %{"zip" => 123} + } + }) + + schema_flat_map = SchemaUtils.bq_schema_to_flat_typemap(bq_schema) + + source_schema = Logflare.SourceSchemas.get_source_schema_by(source_id: source.id) + + {:ok, _source_schema} = + Logflare.SourceSchemas.update_source_schema(source_schema, %{ + bigquery_schema: bq_schema, + schema_flat_map: schema_flat_map + }) + + _ = Logflare.SavedSearches.Cache.bust_by(source_id: source.id) + Cachex.clear(Logflare.SourceSchemas.Cache) + + {:ok, view, _html} = live(conn, Routes.live_path(conn, SearchLV, source.id)) + %{executor_pid: search_executor_pid} = get_view_assigns(view) + Ecto.Adapters.SQL.Sandbox.allow(Logflare.Repo, self(), search_executor_pid) + + html = + view + |> TestUtils.wait_for_render("#lql-editor-hook") + |> render() + + {:ok, document} = Floki.parse_document(html) + + [schema_fields_json] = + document + |> Floki.find("#lql-editor-hook") + |> Floki.attribute("data-schema-fields-json") + + [saved_searches_json] = + document + |> Floki.find("#lql-editor-hook") + |> Floki.attribute("data-suggested-searches-json") + + assert {:ok, schema_fields} = Jason.decode(schema_fields_json) + assert {:ok, saved_searches} = Jason.decode(saved_searches_json) + + assert %{"name" => "metadata.flags", "type" => "list[boolean]"} in schema_fields + assert %{"name" => "metadata.store.zip", "type" => "integer"} in schema_fields + + assert query_a in saved_searches + assert query_b in saved_searches + end + test "empty results message", %{conn: conn, source: source} do pid = self() From 0b224b47f9aad44fabcf2f70492f8f8b6fd0b899 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Thu, 12 Mar 2026 14:15:46 +1000 Subject: [PATCH 26/44] ui: consistency for source search fields --- lib/logflare_web/live/search_live/form_components.ex | 2 +- lib/logflare_web/templates/source/show.html.heex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/logflare_web/live/search_live/form_components.ex b/lib/logflare_web/live/search_live/form_components.ex index 594567dcc..c445a0816 100644 --- a/lib/logflare_web/live/search_live/form_components.ex +++ b/lib/logflare_web/live/search_live/form_components.ex @@ -90,7 +90,7 @@ defmodule LogflareWeb.SearchLive.FormComponents do diff --git a/lib/logflare_web/templates/source/show.html.heex b/lib/logflare_web/templates/source/show.html.heex index 8e5e84a6c..4a40751cf 100644 --- a/lib/logflare_web/templates/source/show.html.heex +++ b/lib/logflare_web/templates/source/show.html.heex @@ -67,7 +67,7 @@
{text_input(f, :querystring, placeholder: "404", - class: "form-control tw-mt-0 tw-border-[#282c34] tw-bg-[#282c34] tw-font-mono tw-text-[#c4cad6] placeholder:tw-text-[#8c92a3] focus:tw-border-[#3e4451] focus:tw-bg-[#282c34] focus:tw-text-[#c4cad6]", + class: "form-control tw-mt-0 tw-border-[#282c34] tw-text-sm tw-h-8 tw-min-h-8 tw-max-h-8 tw-py-[3px] tw-bg-[#282c34] tw-font-mono tw-text-[#c4cad6] placeholder:tw-text-[#8c92a3] focus:tw-border-[#3e4451] focus:tw-bg-[#282c34] focus:tw-text-[#c4cad6]", autofocus: true )}
From 39c5410edc5844adc1b443d2c60dd8d34c4c2af0 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 16 Mar 2026 10:31:11 +1000 Subject: [PATCH 27/44] refactor: handle error in Lql editor mount --- assets/js/lql_editor_wrapper_hook.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/js/lql_editor_wrapper_hook.js b/assets/js/lql_editor_wrapper_hook.js index 0e18e8ab9..775d7614f 100644 --- a/assets/js/lql_editor_wrapper_hook.js +++ b/assets/js/lql_editor_wrapper_hook.js @@ -42,7 +42,12 @@ const LqlEditorWrapper = { const monaco = window.monaco; - registerLqlLanguage(monaco); + try { + registerLqlLanguage(monaco); + } catch (_) { + console.log("Failed to register LQL language", _); + } + const model = standaloneEditor.getModel(); monaco.editor.setModelLanguage(model, "lql"); From d71d18ad050665607e269e4d6cfaa98f3fa89725 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 16 Mar 2026 11:09:19 +1000 Subject: [PATCH 28/44] refactor: simplify lql schema fields passed to client --- assets/js/lql_editor_wrapper_hook.js | 2 +- assets/js/lql_language.js | 28 +++++++++---------- .../live/search_live/form_components.ex | 18 +++++++++++- .../live/search_live/logs_search_lv.ex | 18 ------------ .../live/search_live/logs_search_lv_test.exs | 4 +-- 5 files changed, 34 insertions(+), 36 deletions(-) diff --git a/assets/js/lql_editor_wrapper_hook.js b/assets/js/lql_editor_wrapper_hook.js index 775d7614f..9273a6b83 100644 --- a/assets/js/lql_editor_wrapper_hook.js +++ b/assets/js/lql_editor_wrapper_hook.js @@ -4,7 +4,7 @@ import { } from "./lql_language"; const parseSchemaFields = (schemaFieldsJson) => { - if (!schemaFieldsJson) return []; + if (!schemaFieldsJson) return {}; return JSON.parse(schemaFieldsJson); }; diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index 3c6529789..b2dfa4d65 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -250,8 +250,12 @@ function buildTimestampSuggestions(monaco, token, range) { })); } +function getMetadataFieldNames(fields) { + return Object.keys(fields).filter((name) => name.startsWith("metadata.")); +} + function getMetadataSuggestionSegments(fields, typedPath) { - const metadataFields = fields.filter((f) => f.name.startsWith("metadata.")); + const metadataFields = getMetadataFieldNames(fields); const pathPrefix = typedPath.includes(".") ? typedPath.substring(0, typedPath.lastIndexOf(".") + 1) : ""; @@ -260,9 +264,9 @@ function getMetadataSuggestionSegments(fields, typedPath) { const suggestions = []; for (const field of metadataFields) { - if (!field.name.startsWith(fullPrefix)) continue; + if (!field.startsWith(fullPrefix)) continue; - const remainder = field.name.slice(fullPrefix.length); + const remainder = field.slice(fullPrefix.length); if (!remainder) continue; const dotIdx = remainder.indexOf("."); @@ -427,9 +431,7 @@ export function registerLqlCompletionProvider( const typedPath = metaMatch[1]; // e.g. "request." or "req" const fields = getFields(); const suggestions = []; - const metadataFields = fields.filter((f) => - f.name.startsWith("metadata."), - ); + const metadataFieldNames = getMetadataFieldNames(fields); const pathPrefix = typedPath.includes(".") ? typedPath.substring(0, typedPath.lastIndexOf(".") + 1) : ""; @@ -439,21 +441,19 @@ export function registerLqlCompletionProvider( fields, typedPath, )) { - const field = metadataFields.find( - (item) => - item.name === `${fullPrefix}${segment}` || - item.name.startsWith(`${fullPrefix}${segment}.`), + const fieldName = metadataFieldNames.find( + (name) => + name === `${fullPrefix}${segment}` || + name.startsWith(`${fullPrefix}${segment}.`), ); - const isLeaf = field - ? field.name === `${fullPrefix}${segment}` - : true; + const isLeaf = fieldName ? fieldName === `${fullPrefix}${segment}` : true; suggestions.push({ label: segment, kind: isLeaf ? monaco.languages.CompletionItemKind.Field : monaco.languages.CompletionItemKind.Module, - detail: isLeaf ? field.type : "namespace", + detail: isLeaf ? fields[fieldName] : "namespace", insertText: segment, range: replaceRange, sortText: segment.padStart(50), diff --git a/lib/logflare_web/live/search_live/form_components.ex b/lib/logflare_web/live/search_live/form_components.ex index c445a0816..29ebf5913 100644 --- a/lib/logflare_web/live/search_live/form_components.ex +++ b/lib/logflare_web/live/search_live/form_components.ex @@ -8,6 +8,7 @@ defmodule LogflareWeb.SearchLive.FormComponents do use Phoenix.Component alias Logflare.Lql.Rules + alias Logflare.SourceSchemas alias Logflare.Utils alias Logflare.Sources.Source @@ -129,7 +130,6 @@ defmodule LogflareWeb.SearchLive.FormComponents do end attr :querystring, :string, required: true - attr :lql_schema_fields_json, :string, required: true attr :saved_searches, :list, required: true attr :loading, :boolean, required: true attr :tailing?, :boolean, required: true @@ -146,6 +146,7 @@ defmodule LogflareWeb.SearchLive.FormComponents do assigns = assigns |> assign(:saved_searches_json, JSON.encode!(assigns.saved_searches)) + |> assign(:lql_schema_fields_json, assigns.source |> lql_schema_fields() |> Jason.encode!()) ~H"""
@@ -225,6 +226,21 @@ defmodule LogflareWeb.SearchLive.FormComponents do |> Kernel.in([:integer, :float]) end + defp lql_schema_fields(source) do + case SourceSchemas.Cache.get_source_schema_by(source_id: source.id) do + %{schema_flat_map: flat_map} when is_map(flat_map) -> + for {name, type} <- flat_map, into: %{} do + {name, format_schema_type(type)} + end + + _ -> + %{} + end + end + + defp format_schema_type({type, inner}), do: "#{type}[#{inner}]" + defp format_schema_type(type), do: to_string(type) + attr :tailing?, :boolean, required: true attr :play_event, :string, values: ["soft_play", "hard_play"] 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 5aeabe66e..2b382104b 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -287,7 +287,6 @@ defmodule LogflareWeb.Source.SearchLV do
push_event("set_lql_value", %{value: qs}) end - defp schema_fields_json(source), do: source |> lql_schema_fields() |> Jason.encode!() - - defp lql_schema_fields(source) do - case SourceSchemas.Cache.get_source_schema_by(source_id: source.id) do - %{schema_flat_map: flat_map} when is_map(flat_map) -> - Enum.map(flat_map, fn {name, type} -> - %{name: name, type: format_schema_type(type)} - end) - - _ -> - [] - end - end - defp saved_searches(source) do source.id |> SavedSearches.Cache.list_saved_searches_by_source() |> Enum.map(& &1.querystring) end - - defp format_schema_type({type, inner}), do: "#{type}[#{inner}]" - defp format_schema_type(type), do: to_string(type) end 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 1b1bde3a8..9465b5d24 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 @@ -558,8 +558,8 @@ defmodule LogflareWeb.Source.SearchLVTest do assert {:ok, schema_fields} = Jason.decode(schema_fields_json) assert {:ok, saved_searches} = Jason.decode(saved_searches_json) - assert %{"name" => "metadata.flags", "type" => "list[boolean]"} in schema_fields - assert %{"name" => "metadata.store.zip", "type" => "integer"} in schema_fields + assert schema_fields["metadata.flags"] == "list[boolean]" + assert schema_fields["metadata.store.zip"] == "integer" assert query_a in saved_searches assert query_b in saved_searches From 5fece1edf3a4925a9f56ad00f7047958c99c8199 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 16 Mar 2026 11:15:47 +1000 Subject: [PATCH 29/44] ui: condolidate lql group_by completions into single suggestion --- assets/js/lql_language.js | 87 +++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index b2dfa4d65..1e35b7720 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -139,24 +139,9 @@ const LQL_KEYWORDS = [ insertText: "c:countd($0)", }, { - label: "c:group_by(t::minute)", - detail: "chart group by minute", - insertText: "c:group_by(t::minute)", - }, - { - label: "c:group_by(t::second)", - detail: "chart group by second", - insertText: "c:group_by(t::second)", - }, - { - label: "c:group_by(t::hour)", - detail: "chart group by hour", - insertText: "c:group_by(t::hour)", - }, - { - label: "c:group_by(t::day)", - detail: "chart group by day", - insertText: "c:group_by(t::day)", + label: "c:group_by", + detail: "chart group by", + insertText: "c:group_by($0)", }, { label: "c:avg()", detail: "chart average", insertText: "c:avg($0)" }, { label: "c:sum()", detail: "chart sum", insertText: "c:sum($0)" }, @@ -178,6 +163,13 @@ const LQL_TIMESTAMP_KEYWORDS = [ { label: "this@", detail: "current time period", insertText: "this@" }, ]; +const LQL_GROUP_BY_KEYWORDS = [ + { label: "t::second", detail: "group by second", insertText: "t::second" }, + { label: "t::minute", detail: "group by minute", insertText: "t::minute" }, + { label: "t::hour", detail: "group by hour", insertText: "t::hour" }, + { label: "t::day", detail: "group by day", insertText: "t::day" }, +]; + const LQL_FILTER_OPERATORS = [ { label: ":", detail: "exact match", insertText: ":" }, { label: ":..", detail: "range operator", insertText: ":.." }, @@ -190,6 +182,17 @@ const LQL_FILTER_OPERATORS = [ { label: ":@>~", detail: "array includes regex match", insertText: ":@>~" }, ]; +function getKeywordSuggestionCommand(kw) { + return kw.insertText === "m." || + kw.insertText === "metadata." || + kw.insertText === "c:group_by($0)" + ? { + id: "editor.action.triggerSuggest", + title: "Trigger suggest", + } + : undefined; +} + function buildKeywordSuggestions(monaco, token, range) { return LQL_KEYWORDS.filter( (kw) => @@ -203,13 +206,7 @@ function buildKeywordSuggestions(monaco, token, range) { insertTextRules: kw.insertText.includes("$0") ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined, - command: - kw.insertText === "m." || kw.insertText === "metadata." - ? { - id: "editor.action.triggerSuggest", - title: "Trigger suggest", - } - : undefined, + command: getKeywordSuggestionCommand(kw), range, sortText: String(i).padStart(3, "0"), })); @@ -250,6 +247,21 @@ function buildTimestampSuggestions(monaco, token, range) { })); } +function buildGroupBySuggestions(monaco, token, range) { + return LQL_GROUP_BY_KEYWORDS.filter( + (kw) => + kw.label.toLocaleLowerCase().startsWith(token.toLocaleLowerCase()) || + kw.insertText.toLocaleLowerCase().startsWith(token.toLocaleLowerCase()), + ).map((kw, i) => ({ + label: kw.label, + kind: monaco.languages.CompletionItemKind.Keyword, + detail: kw.detail, + insertText: kw.insertText, + range, + sortText: String(i).padStart(3, "0"), + })); +} + function getMetadataFieldNames(fields) { return Object.keys(fields).filter((name) => name.startsWith("metadata.")); } @@ -286,7 +298,7 @@ export function registerLqlCompletionProvider( getSuggestedSearches, ) { return monaco.languages.registerCompletionItemProvider("lql", { - triggerCharacters: [".", ":", "@", ">", "<", "~"], + triggerCharacters: [".", ":", "@", ">", "<", "~", "("], provideCompletionItems(model, position) { const textUntilPosition = model.getValueInRange({ @@ -312,6 +324,9 @@ export function registerLqlCompletionProvider( const operatorMatch = textUntilPosition.match( /(?:^|\s)-?(?:(?:m|metadata)\.[\w.]+|(?:t|timestamp)|[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*):([@><~=]*)$/, ); + const groupByMatch = textUntilPosition.match( + /(?:^|\s)(?:c|chart):group_by\(([^)]*)$/, + ); const timestampValueMatch = textUntilPosition.match( /(?:^|\s)(?:t|timestamp):([a-zA-Z@]*)$/, ); @@ -370,6 +385,25 @@ export function registerLqlCompletionProvider( } } + if (groupByMatch) { + const groupByToken = groupByMatch[1]; + const groupByRange = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: position.column - groupByToken.length, + endColumn: position.column, + }; + const suggestions = buildGroupBySuggestions( + monaco, + groupByToken, + groupByRange, + ); + + if (suggestions.length > 0) { + return { suggestions }; + } + } + if ( currentToken && textUntilPosition.length > 0 && @@ -473,6 +507,7 @@ export function registerLqlCompletionProvider( insertTextRules: kw.insertText.includes("$0") ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined, + command: getKeywordSuggestionCommand(kw), range: replaceRange, sortText: `1-${String(i).padStart(3, "0")}`, })); From 2e74eca4387237df35e6bdcea5a083728b4d399c Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 16 Mar 2026 11:25:01 +1000 Subject: [PATCH 30/44] ui: place cursor inside c:count completion --- assets/js/lql_language.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index 1e35b7720..83a5ee1e8 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -132,7 +132,7 @@ const LQL_KEYWORDS = [ { label: "m.", detail: "metadata field", insertText: "m." }, { label: "metadata.", detail: "metadata field", insertText: "metadata." }, { label: "s:*", detail: "select all fields", insertText: "s:*" }, - { label: "c:count(*)", detail: "chart count", insertText: "c:count(*)" }, + { label: "c:count()", detail: "chart count", insertText: "c:count($0)" }, { label: "c:countd()", detail: "chart distinct count", From cf96056b0e3563430de8471e6ee7cc060c144ef4 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 16 Mar 2026 11:25:01 +1000 Subject: [PATCH 31/44] ui: drop `*` from lql select completion --- assets/js/lql_language.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index 83a5ee1e8..3ba795e0a 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -131,7 +131,7 @@ const LQL_KEYWORDS = [ { label: "timestamp:", detail: "timestamp filter", insertText: "timestamp:" }, { label: "m.", detail: "metadata field", insertText: "m." }, { label: "metadata.", detail: "metadata field", insertText: "metadata." }, - { label: "s:*", detail: "select all fields", insertText: "s:*" }, + { label: "s:", detail: "select all fields", insertText: "s:" }, { label: "c:count()", detail: "chart count", insertText: "c:count($0)" }, { label: "c:countd()", From c8c98643554d886b404fb8a81e80be466bea7949 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 16 Mar 2026 11:26:04 +1000 Subject: [PATCH 32/44] ui: consolidate lql completion metadata and timestamp into m: and t: --- assets/js/lql_language.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index 3ba795e0a..7daf47ecb 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -127,11 +127,9 @@ export function registerLqlLanguage(monaco) { } const LQL_KEYWORDS = [ - { label: "t:", detail: "timestamp filter", insertText: "t:" }, - { label: "timestamp:", detail: "timestamp filter", insertText: "timestamp:" }, - { label: "m.", detail: "metadata field", insertText: "m." }, - { label: "metadata.", detail: "metadata field", insertText: "metadata." }, - { label: "s:", detail: "select all fields", insertText: "s:" }, + { label: "t:", detail: "alias timestamp:", insertText: "t:" }, + { label: "m.", detail: "alias metadata.", insertText: "m." }, + { label: "s:", detail: "alias select:", insertText: "s:" }, { label: "c:count()", detail: "chart count", insertText: "c:count($0)" }, { label: "c:countd()", @@ -149,8 +147,6 @@ const LQL_KEYWORDS = [ { label: "c:p50()", detail: "chart p50", insertText: "c:p50($0)" }, { label: "c:p95()", detail: "chart p95", insertText: "c:p95($0)" }, { label: "c:p99()", detail: "chart p99", insertText: "c:p99($0)" }, - { label: "s:", detail: "select fields", insertText: "s:" }, - { label: "select:", detail: "select fields", insertText: "select:" }, { label: "f:", detail: "from source", insertText: "f:" }, { label: "from:", detail: "from source", insertText: "from:" }, ]; @@ -184,7 +180,7 @@ const LQL_FILTER_OPERATORS = [ function getKeywordSuggestionCommand(kw) { return kw.insertText === "m." || - kw.insertText === "metadata." || + kw.insertText === "t:" || kw.insertText === "c:group_by($0)" ? { id: "editor.action.triggerSuggest", From 5898ba382e7d89b633801da881ad4b2fdc490a59 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 16 Mar 2026 11:37:33 +1000 Subject: [PATCH 33/44] ui: lql search only suggests saved searchs for first char --- assets/js/lql_language.js | 48 ++++++++++++--------------------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index 7daf47ecb..0b17c023c 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -423,32 +423,8 @@ export function registerLqlCompletionProvider( filteredSavedSearchQuerystrings, lineRange, ); - - const keywordSuggestions = buildKeywordSuggestions( - monaco, - currentToken, - lineRange, - ); - const filteredKeywordSuggestions = hasSingleExactKeywordSuggestion( - currentToken, - keywordSuggestions, - ) - ? [] - : keywordSuggestions; - const sortedKeywordSuggestions = filteredKeywordSuggestions.map( - (suggestion, index) => ({ - ...suggestion, - sortText: `1-${String(index).padStart(3, "0")}`, - }), - ); - - const suggestions = [ - ...savedSearchSuggestions, - ...sortedKeywordSuggestions, - ]; - - if (suggestions.length > 0) { - return { suggestions }; + if (savedSearchSuggestions.length > 0) { + return { suggestions: savedSearchSuggestions }; } } @@ -476,7 +452,9 @@ export function registerLqlCompletionProvider( name === `${fullPrefix}${segment}` || name.startsWith(`${fullPrefix}${segment}.`), ); - const isLeaf = fieldName ? fieldName === `${fullPrefix}${segment}` : true; + const isLeaf = fieldName + ? fieldName === `${fullPrefix}${segment}` + : true; suggestions.push({ label: segment, @@ -495,6 +473,15 @@ export function registerLqlCompletionProvider( // At start of input or after whitespace: suggest LQL keywords if (/(?:^|[\s])$/.test(textUntilPosition)) { + if (textUntilPosition.trim().length === 0) { + return { + suggestions: buildSavedSearchSuggestions( + savedSearchQuerystrings, + lineRange, + ), + }; + } + const keywordSuggestions = LQL_KEYWORDS.map((kw, i) => ({ label: kw.label, kind: monaco.languages.CompletionItemKind.Keyword, @@ -507,13 +494,8 @@ export function registerLqlCompletionProvider( range: replaceRange, sortText: `1-${String(i).padStart(3, "0")}`, })); - const showSavedSearches = textUntilPosition.trim().length === 0; - const savedSearchSuggestions = showSavedSearches - ? buildSavedSearchSuggestions(savedSearchQuerystrings, lineRange) - : []; - const suggestions = [...savedSearchSuggestions, ...keywordSuggestions]; - return { suggestions }; + return { suggestions: keywordSuggestions }; } if (currentToken) { From 39281f487113908da6017e4a29101c320d97f5be Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Fri, 27 Mar 2026 16:36:05 +1000 Subject: [PATCH 34/44] test: refute non matching message --- .../logflare_web/live/search_live/logs_search_lv_test.exs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 b3a82f781..a1d9a9ae3 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 @@ -8,7 +8,6 @@ defmodule LogflareWeb.Source.SearchLVTest do alias Logflare.Backends alias Logflare.Google.BigQuery.SchemaUtils alias Logflare.SingleTenant - alias Logflare.SourceSchemas alias Logflare.Sources.Source.BigQuery.Schema alias Logflare.Utils.Tasks alias LogflareWeb.Source.SearchLV @@ -1376,7 +1375,8 @@ defmodule LogflareWeb.Source.SearchLVTest do user: user, source: source, plan: plan, - matching_message: matching_message + matching_message: matching_message, + non_matching_message: non_matching_message } end @@ -1385,7 +1385,8 @@ defmodule LogflareWeb.Source.SearchLVTest do test "run a search against postgres backend", %{ conn: conn, source: source, - matching_message: matching_message + matching_message: matching_message, + non_matching_message: non_matching_message } do {:ok, view, _html} = live(conn, Routes.live_path(conn, SearchLV, source.id)) @@ -1400,6 +1401,7 @@ defmodule LogflareWeb.Source.SearchLVTest do |> TestUtils.wait_for_render("#logs-list-container li") assert view |> element("#logs-list-container") |> render() =~ matching_message + refute view |> element("#logs-list-container") |> render() =~ non_matching_message end end From 40674a700cc1c6008c5731ffdbc6292ead71bd69 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 16 Mar 2026 13:30:49 +1000 Subject: [PATCH 35/44] test: add test coverage for lql_language.js --- .github/workflows/elixir-ci.yml | 20 + assets/js/lql_language.js | 432 ++- assets/js/test/lql_language.test.js | 354 ++ assets/package-lock.json | 5454 +++++++++------------------ assets/package.json | 4 +- 5 files changed, 2374 insertions(+), 3890 deletions(-) create mode 100644 assets/js/test/lql_language.test.js diff --git a/.github/workflows/elixir-ci.yml b/.github/workflows/elixir-ci.yml index 923fc8047..98a0432af 100644 --- a/.github/workflows/elixir-ci.yml +++ b/.github/workflows/elixir-ci.yml @@ -8,6 +8,7 @@ on: pull_request: branches: ["**"] paths: + - "assets/**" - "lib/**" - "config/**" - "*.exs" @@ -163,3 +164,22 @@ jobs: - name: Run ${{ matrix.commands.name }} run: ${{ matrix.commands.run }} + + frontend: + name: Frontend Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: npm + cache-dependency-path: assets/package-lock.json + + - name: Install frontend dependencies + run: npm ci --prefix assets + + - name: Run frontend unit tests + run: npm test --prefix assets diff --git a/assets/js/lql_language.js b/assets/js/lql_language.js index 0b17c023c..73985dec7 100644 --- a/assets/js/lql_language.js +++ b/assets/js/lql_language.js @@ -288,6 +288,219 @@ function getMetadataSuggestionSegments(fields, typedPath) { return suggestions; } +export function getLqlCompletionItems({ + fields = {}, + kinds, + lineMaxColumn, + position, + savedSearches = [], + snippetInsertTextRule, + textUntilPosition, + word, + fullLine = textUntilPosition, +}) { + const replaceRange = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }; + const lineRange = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: 1, + endColumn: lineMaxColumn, + }; + const operatorMatch = textUntilPosition.match( + /(?:^|\s)-?(?:(?:m|metadata)\.[\w.]+|(?:t|timestamp)|[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*):([@><~=]*)$/, + ); + const groupByMatch = textUntilPosition.match( + /(?:^|\s)(?:c|chart):group_by\(([^)]*)$/, + ); + const timestampValueMatch = textUntilPosition.match( + /(?:^|\s)(?:t|timestamp):([a-zA-Z@]*)$/, + ); + const tokenMatch = textUntilPosition.match(/(?:^|\s)(\S+)$/); + const currentToken = tokenMatch ? tokenMatch[1] : null; + const currentTokenRange = currentToken + ? { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: position.column - currentToken.length, + endColumn: position.column, + } + : replaceRange; + const savedSearchQuerystrings = [...new Set(savedSearches)]; + const completionApi = { + languages: { + CompletionItemInsertTextRule: { + InsertAsSnippet: snippetInsertTextRule, + }, + CompletionItemKind: kinds, + }, + }; + const buildSavedSearchSuggestions = (querystrings, range) => + querystrings.map((querystring, index) => ({ + label: querystring, + kind: kinds.Snippet, + detail: "saved search", + insertText: querystring, + range, + sortText: `0-${String(index).padStart(3, "0")}`, + })); + + if (timestampValueMatch) { + const timestampToken = timestampValueMatch[1]; + const suggestions = buildTimestampSuggestions( + completionApi, + timestampToken, + { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: position.column - timestampToken.length, + endColumn: position.column, + }, + ); + + if (suggestions.length > 0) { + return suggestions; + } + } + + if (operatorMatch) { + const operatorPrefix = `:${operatorMatch[1]}`; + const operatorRange = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: position.column - operatorPrefix.length, + endColumn: position.column, + }; + const suggestions = buildOperatorSuggestions( + completionApi, + operatorPrefix, + operatorRange, + ); + + if (suggestions.length > 0) { + return suggestions; + } + } + + if (groupByMatch) { + const groupByToken = groupByMatch[1]; + const groupByRange = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: position.column - groupByToken.length, + endColumn: position.column, + }; + const suggestions = buildGroupBySuggestions( + completionApi, + groupByToken, + groupByRange, + ); + + if (suggestions.length > 0) { + return suggestions; + } + } + + if ( + currentToken && + textUntilPosition.length > 0 && + /^\S+$/.test(textUntilPosition) && + fullLine === textUntilPosition && + !["t:", "timestamp:"].includes(currentToken) + ) { + const matchedSavedSearches = savedSearchQuerystrings.filter((querystring) => + querystring + .toLocaleLowerCase() + .startsWith(currentToken.toLocaleLowerCase()), + ); + const filteredSavedSearchQuerystrings = + matchedSavedSearches.length === 1 && + matchedSavedSearches[0].toLocaleLowerCase() === + currentToken.toLocaleLowerCase() + ? [] + : matchedSavedSearches; + const savedSearchSuggestions = buildSavedSearchSuggestions( + filteredSavedSearchQuerystrings, + lineRange, + ); + + if (savedSearchSuggestions.length > 0) { + return savedSearchSuggestions; + } + } + + const metaMatch = textUntilPosition.match( + /(?:^|[\s:])(?:m|metadata)\.([\w.]*?)$/, + ); + + if (metaMatch) { + const typedPath = metaMatch[1]; + const suggestions = []; + const metadataFieldNames = getMetadataFieldNames(fields); + const pathPrefix = typedPath.includes(".") + ? typedPath.substring(0, typedPath.lastIndexOf(".") + 1) + : ""; + const fullPrefix = "metadata." + pathPrefix; + + for (const segment of getMetadataSuggestionSegments(fields, typedPath)) { + const fieldName = metadataFieldNames.find( + (name) => + name === `${fullPrefix}${segment}` || + name.startsWith(`${fullPrefix}${segment}.`), + ); + const isLeaf = fieldName ? fieldName === `${fullPrefix}${segment}` : true; + + suggestions.push({ + label: segment, + kind: isLeaf ? kinds.Field : kinds.Module, + detail: isLeaf ? fields[fieldName] : "namespace", + insertText: segment, + range: replaceRange, + sortText: segment.padStart(50), + }); + } + + return suggestions; + } + + if (/(?:^|[\s])$/.test(textUntilPosition)) { + if (textUntilPosition.trim().length === 0) { + return buildSavedSearchSuggestions(savedSearchQuerystrings, lineRange); + } + + return LQL_KEYWORDS.map((kw, i) => ({ + label: kw.label, + kind: kinds.Keyword, + detail: kw.detail, + insertText: kw.insertText, + insertTextRules: kw.insertText.includes("$0") + ? snippetInsertTextRule + : undefined, + command: getKeywordSuggestionCommand(kw), + range: replaceRange, + sortText: `1-${String(i).padStart(3, "0")}`, + })); + } + + if (currentToken) { + const suggestions = buildKeywordSuggestions( + completionApi, + currentToken, + currentTokenRange, + ); + + if (!hasSingleExactKeywordSuggestion(currentToken, suggestions)) { + return suggestions; + } + } + + return []; +} + export function registerLqlCompletionProvider( monaco, getFields, @@ -304,213 +517,20 @@ export function registerLqlCompletionProvider( endColumn: position.column, }); - const word = model.getWordUntilPosition(position); - const replaceRange = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, + return { + suggestions: getLqlCompletionItems({ + fields: getFields(), + fullLine: model.getLineContent(position.lineNumber), + kinds: monaco.languages.CompletionItemKind, + lineMaxColumn: model.getLineMaxColumn(position.lineNumber), + position, + savedSearches: getSuggestedSearches(), + snippetInsertTextRule: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + textUntilPosition, + word: model.getWordUntilPosition(position), + }), }; - const lineRange = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: 1, - endColumn: model.getLineMaxColumn(position.lineNumber), - }; - const operatorMatch = textUntilPosition.match( - /(?:^|\s)-?(?:(?:m|metadata)\.[\w.]+|(?:t|timestamp)|[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*):([@><~=]*)$/, - ); - const groupByMatch = textUntilPosition.match( - /(?:^|\s)(?:c|chart):group_by\(([^)]*)$/, - ); - const timestampValueMatch = textUntilPosition.match( - /(?:^|\s)(?:t|timestamp):([a-zA-Z@]*)$/, - ); - const fullLine = model.getLineContent(position.lineNumber); - const tokenMatch = textUntilPosition.match(/(?:^|\s)(\S+)$/); - const currentToken = tokenMatch ? tokenMatch[1] : null; - const currentTokenRange = currentToken - ? { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: position.column - currentToken.length, - endColumn: position.column, - } - : replaceRange; - const savedSearchQuerystrings = [...new Set(getSuggestedSearches())]; - const buildSavedSearchSuggestions = (querystrings, range) => - querystrings.map((querystring, index) => ({ - label: querystring, - kind: monaco.languages.CompletionItemKind.Snippet, - detail: "saved search", - insertText: querystring, - range, - sortText: `0-${String(index).padStart(3, "0")}`, - })); - - if (timestampValueMatch) { - const timestampToken = timestampValueMatch[1]; - const suggestions = buildTimestampSuggestions(monaco, timestampToken, { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: position.column - timestampToken.length, - endColumn: position.column, - }); - - if (suggestions.length > 0) { - return { suggestions }; - } - } - - if (operatorMatch) { - const operatorPrefix = `:${operatorMatch[1]}`; - const operatorRange = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: position.column - operatorPrefix.length, - endColumn: position.column, - }; - const suggestions = buildOperatorSuggestions( - monaco, - operatorPrefix, - operatorRange, - ); - - if (suggestions.length > 0) { - return { suggestions }; - } - } - - if (groupByMatch) { - const groupByToken = groupByMatch[1]; - const groupByRange = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: position.column - groupByToken.length, - endColumn: position.column, - }; - const suggestions = buildGroupBySuggestions( - monaco, - groupByToken, - groupByRange, - ); - - if (suggestions.length > 0) { - return { suggestions }; - } - } - - if ( - currentToken && - textUntilPosition.length > 0 && - /^\S+$/.test(textUntilPosition) && - fullLine === textUntilPosition && - !["t:", "timestamp:"].includes(currentToken) - ) { - const matchedSavedSearches = savedSearchQuerystrings.filter( - (querystring) => - querystring - .toLocaleLowerCase() - .startsWith(currentToken.toLocaleLowerCase()), - ); - const filteredSavedSearchQuerystrings = - matchedSavedSearches.length === 1 && - matchedSavedSearches[0].toLocaleLowerCase() === - currentToken.toLocaleLowerCase() - ? [] - : matchedSavedSearches; - const savedSearchSuggestions = buildSavedSearchSuggestions( - filteredSavedSearchQuerystrings, - lineRange, - ); - if (savedSearchSuggestions.length > 0) { - return { suggestions: savedSearchSuggestions }; - } - } - - // Check if we're in a metadata path context: m. or metadata. prefix - const metaMatch = textUntilPosition.match( - /(?:^|[\s:])(?:m|metadata)\.([\w.]*?)$/, - ); - - if (metaMatch) { - const typedPath = metaMatch[1]; // e.g. "request." or "req" - const fields = getFields(); - const suggestions = []; - const metadataFieldNames = getMetadataFieldNames(fields); - const pathPrefix = typedPath.includes(".") - ? typedPath.substring(0, typedPath.lastIndexOf(".") + 1) - : ""; - const fullPrefix = "metadata." + pathPrefix; - - for (const segment of getMetadataSuggestionSegments( - fields, - typedPath, - )) { - const fieldName = metadataFieldNames.find( - (name) => - name === `${fullPrefix}${segment}` || - name.startsWith(`${fullPrefix}${segment}.`), - ); - const isLeaf = fieldName - ? fieldName === `${fullPrefix}${segment}` - : true; - - suggestions.push({ - label: segment, - kind: isLeaf - ? monaco.languages.CompletionItemKind.Field - : monaco.languages.CompletionItemKind.Module, - detail: isLeaf ? fields[fieldName] : "namespace", - insertText: segment, - range: replaceRange, - sortText: segment.padStart(50), - }); - } - - return { suggestions }; - } - - // At start of input or after whitespace: suggest LQL keywords - if (/(?:^|[\s])$/.test(textUntilPosition)) { - if (textUntilPosition.trim().length === 0) { - return { - suggestions: buildSavedSearchSuggestions( - savedSearchQuerystrings, - lineRange, - ), - }; - } - - const keywordSuggestions = LQL_KEYWORDS.map((kw, i) => ({ - label: kw.label, - kind: monaco.languages.CompletionItemKind.Keyword, - detail: kw.detail, - insertText: kw.insertText, - insertTextRules: kw.insertText.includes("$0") - ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet - : undefined, - command: getKeywordSuggestionCommand(kw), - range: replaceRange, - sortText: `1-${String(i).padStart(3, "0")}`, - })); - - return { suggestions: keywordSuggestions }; - } - - if (currentToken) { - const suggestions = buildKeywordSuggestions( - monaco, - currentToken, - currentTokenRange, - ); - - if (!hasSingleExactKeywordSuggestion(currentToken, suggestions)) { - return { suggestions }; - } - } - - return { suggestions: [] }; }, }); } diff --git a/assets/js/test/lql_language.test.js b/assets/js/test/lql_language.test.js new file mode 100644 index 000000000..c6f99a03a --- /dev/null +++ b/assets/js/test/lql_language.test.js @@ -0,0 +1,354 @@ +import { describe, expect, it, vi } from "vitest"; + +import { getLqlCompletionItems, registerLqlLanguage } from "../lql_language.js"; + +const COMPLETION_KINDS = { + Field: "field", + Keyword: "keyword", + Module: "module", + Operator: "operator", + Snippet: "snippet", +}; + +const SNIPPET_INSERT_TEXT_RULE = "insert-as-snippet"; + +function getWord(line, column) { + let start = column - 1; + let end = column - 1; + + while (start > 0 && /\w/.test(line[start - 1])) start -= 1; + while (end < line.length && /\w/.test(line[end])) end += 1; + + return { + startColumn: start + 1, + endColumn: end + 1, + }; +} + +function getSuggestions( + line, + { fields = {}, savedSearches = [] } = {}, + column = line.length + 1, +) { + return getLqlCompletionItems({ + fields, + fullLine: line, + kinds: COMPLETION_KINDS, + lineMaxColumn: line.length + 1, + position: { + column, + lineNumber: 1, + }, + savedSearches, + snippetInsertTextRule: SNIPPET_INSERT_TEXT_RULE, + textUntilPosition: line.slice(0, column - 1), + word: getWord(line, column), + }); +} + +function buildMonacoLanguageApi(registeredLanguages = []) { + const register = vi.fn(); + const setMonarchTokensProvider = vi.fn(); + const setLanguageConfiguration = vi.fn(); + + return { + monaco: { + languages: { + getLanguages: () => registeredLanguages, + register, + setMonarchTokensProvider, + setLanguageConfiguration, + }, + }, + register, + setMonarchTokensProvider, + setLanguageConfiguration, + }; +} + +describe("getLqlCompletionItems", () => { + it("suggests only matching saved searches for the first token", () => { + const suggestions = getSuggestions( + "c", + { + savedSearches: [ + "c:count(*) c:group_by(t::hour)", + "c:count(*) c:group_by(t::day)", + ], + }, + 2, + ); + + expect(suggestions).toHaveLength(2); + expect(suggestions.map((suggestion) => suggestion.detail)).toEqual([ + "saved search", + "saved search", + ]); + }); + + it("suggests LQL keywords after whitespace", () => { + const suggestions = getSuggestions("foo "); + const labels = suggestions.map((suggestion) => suggestion.label); + + expect(labels).toContain("t:"); + expect(labels).toContain("m."); + expect(labels).toContain("s:"); + expect(labels).toContain("c:group_by"); + }); + + it("suggests timestamp values after the shorthand prefix", () => { + const suggestions = getSuggestions("t:"); + + expect(suggestions.map((suggestion) => suggestion.label)).toEqual([ + "today", + "yesterday", + "now", + "last@", + "this@", + ]); + }); + + it("suggests group_by values inside c:group_by(", () => { + const suggestions = getSuggestions("c:group_by("); + + expect(suggestions.map((suggestion) => suggestion.label)).toEqual([ + "t::second", + "t::minute", + "t::hour", + "t::day", + ]); + }); + + it("suggests filter operators after a field name and colon", () => { + const suggestions = getSuggestions("m.status:"); + + const labels = suggestions.map((suggestion) => suggestion.label); + expect(labels).toContain(":"); + expect(labels).toContain(":>"); + expect(labels).toContain(":>="); + expect(labels).toContain(":<"); + expect(labels).toContain(":<="); + expect(labels).toContain(":~"); + expect(labels).toContain(":@>"); + expect(labels).toContain(":@>~"); + expect(labels).toContain(":.."); + expect(suggestions.map((suggestion) => suggestion.kind)).toEqual( + suggestions.map(() => COMPLETION_KINDS.Operator), + ); + }); + + it("narrows operator suggestions by typed prefix", () => { + const suggestions = getSuggestions("m.status:>"); + + expect(suggestions.map((suggestion) => suggestion.label)).toEqual([ + ":>", + ":>=", + ]); + }); + + it("suggests operators for negated fields", () => { + const suggestions = getSuggestions("-m.status:"); + + expect(suggestions.map((suggestion) => suggestion.label)).toContain(":"); + expect(suggestions.map((suggestion) => suggestion.label)).toContain(":~"); + }); + + it("filters keywords by prefix when typing mid-token", () => { + const suggestions = getSuggestions("foo c:c"); + + const labels = suggestions.map((suggestion) => suggestion.label); + expect(labels).toContain("c:count()"); + expect(labels).toContain("c:countd()"); + expect(labels).not.toContain("c:group_by"); + }); + + it("excludes saved search when it is the only match and exactly equals input", () => { + const suggestions = getSuggestions("m.status:500", { + savedSearches: ["m.status:500", "m.status:404"], + }); + + expect(suggestions).toHaveLength(0); + }); + + it("keeps saved searches when multiple match even if one is exact", () => { + const suggestions = getSuggestions("m.status:500", { + savedSearches: ["m.status:500", "m.status:500 c:count()"], + }); + + expect(suggestions.map((suggestion) => suggestion.label)).toEqual([ + "m.status:500", + "m.status:500 c:count()", + ]); + }); + + it("returns no suggestions for unrecognized tokens", () => { + const suggestions = getSuggestions("foo xyzzy"); + + expect(suggestions).toHaveLength(0); + }); + + it("marks snippet keywords with InsertAsSnippet rule", () => { + const suggestions = getSuggestions("foo "); + const countSuggestion = suggestions.find( + (suggestion) => suggestion.label === "c:count()", + ); + + expect(countSuggestion.insertTextRules).toBe(SNIPPET_INSERT_TEXT_RULE); + expect(countSuggestion.insertText).toBe("c:count($0)"); + }); + + it("sets re-suggest command on m. and t: keywords", () => { + const suggestions = getSuggestions("foo "); + const mDot = suggestions.find((suggestion) => suggestion.label === "m."); + const tColon = suggestions.find((suggestion) => suggestion.label === "t:"); + const selectColon = suggestions.find( + (suggestion) => suggestion.label === "s:", + ); + + expect(mDot.command).toEqual({ + id: "editor.action.triggerSuggest", + title: "Trigger suggest", + }); + expect(tColon.command).toEqual({ + id: "editor.action.triggerSuggest", + title: "Trigger suggest", + }); + expect(selectColon.command).toBeUndefined(); + }); + + it("sets re-suggest command on c:group_by", () => { + const suggestions = getSuggestions("foo c:g"); + const groupBySuggestion = suggestions.find( + (suggestion) => suggestion.label === "c:group_by", + ); + + expect(groupBySuggestion.command).toEqual({ + id: "editor.action.triggerSuggest", + title: "Trigger suggest", + }); + }); + + it("suggests metadata segments and field types from the schema map", () => { + const fields = { + "metadata.request.id": "string", + "metadata.request.method": "string", + "metadata.status": "integer", + }; + + const rootSuggestions = getSuggestions("m.", { fields }); + const nestedSuggestions = getSuggestions("m.request.", { fields }); + + expect(rootSuggestions).toEqual([ + expect.objectContaining({ + detail: "namespace", + kind: "module", + label: "request", + }), + expect.objectContaining({ + detail: "integer", + kind: "field", + label: "status", + }), + ]); + expect(nestedSuggestions).toEqual([ + expect.objectContaining({ + detail: "string", + kind: "field", + label: "id", + }), + expect.objectContaining({ + detail: "string", + kind: "field", + label: "method", + }), + ]); + }); + + it("returns no suggestions for an exact single keyword match", () => { + const suggestions = getSuggestions("foo c:group_by"); + + expect(suggestions).toEqual([]); + }); + + it("returns no suggestions for unknown metadata paths", () => { + const fields = { + "metadata.request.id": "string", + "metadata.status": "integer", + }; + + expect(getSuggestions("m.request.zzz.", { fields })).toEqual([]); + }); + + it("returns deduplicated saved searches for blank input using the full line range", () => { + const suggestions = getSuggestions("", { + savedSearches: [ + "m.status:500 c:count()", + "m.status:500 c:count()", + "t:last@1h m.level:error", + ], + }); + + expect(suggestions).toEqual([ + expect.objectContaining({ + label: "m.status:500 c:count()", + detail: "saved search", + range: { + startLineNumber: 1, + endLineNumber: 1, + startColumn: 1, + endColumn: 1, + }, + }), + expect.objectContaining({ + label: "t:last@1h m.level:error", + detail: "saved search", + range: { + startLineNumber: 1, + endLineNumber: 1, + startColumn: 1, + endColumn: 1, + }, + }), + ]); + }); +}); + +describe("registerLqlLanguage", () => { + it("registers the language configuration", () => { + const { + monaco, + register, + setMonarchTokensProvider, + setLanguageConfiguration, + } = buildMonacoLanguageApi(); + + registerLqlLanguage(monaco); + + expect(register).toHaveBeenCalledWith({ id: "lql" }); + expect(setMonarchTokensProvider).toHaveBeenCalledWith( + "lql", + expect.objectContaining({ + ignoreCase: true, + tokenizer: expect.any(Object), + }), + ); + expect(setLanguageConfiguration).toHaveBeenCalledWith("lql", { + wordPattern: /[a-zA-Z_]\w*/, + }); + }); + + it("returns early when lql is already registered", () => { + const { + monaco, + register, + setMonarchTokensProvider, + setLanguageConfiguration, + } = buildMonacoLanguageApi([{ id: "lql" }]); + + registerLqlLanguage(monaco); + + expect(register).not.toHaveBeenCalled(); + expect(setMonarchTokensProvider).not.toHaveBeenCalled(); + expect(setLanguageConfiguration).not.toHaveBeenCalled(); + }); +}); diff --git a/assets/package-lock.json b/assets/package-lock.json index 69f59aec8..8771250f3 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -1,6 +1,6 @@ { "name": "assets", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -37,21 +37,28 @@ "playwright": "^1.57.0", "postcss": "^8.4.31", "sass": "^1.58.3", - "tailwindcss": "^3.4.10" + "tailwindcss": "^3.4.10", + "vitest": "^3.2.4" } }, "../deps/phoenix": { - "version": "0.0.1" + "version": "1.7.21", + "license": "MIT" }, "../deps/phoenix_html": { - "version": "0.0.1" + "version": "4.3.0" }, "../deps/phoenix_live_react": { - "version": "0.0.1" + "version": "0.4.2", + "license": "MIT" }, "../deps/phoenix_live_view": { - "version": "0.0.1", + "version": "1.0.18", + "license": "MIT", "dependencies": { + "morphdom": "2.7.7" + }, + "devDependencies": { "@babel/cli": "7.27.0", "@babel/core": "7.26.10", "@babel/preset-env": "7.26.9", @@ -67,15 +74,13 @@ "jest-environment-jsdom": "^29.7.0", "jest-monocart-coverage": "^1.1.1", "monocart-reporter": "^2.9.17", - "morphdom": "2.7.7", "phoenix": "1.7.21" } }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -85,16 +90,13 @@ }, "node_modules/@babel/runtime": { "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@bufbuild/protobuf": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz", - "integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==", "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)", "peer": true @@ -169,8 +171,6 @@ }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", "cpu": [ "arm64" ], @@ -526,18 +526,16 @@ }, "node_modules/@fortawesome/fontawesome-free": { "version": "5.15.4", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", - "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", "hasInstallScript": true, + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -552,9 +550,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -566,49 +563,38 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.31", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -619,18 +605,16 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -641,8 +625,6 @@ }, "node_modules/@parcel/watcher": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -699,8 +681,6 @@ }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -951,9 +931,8 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -961,8 +940,6 @@ }, "node_modules/@popperjs/core": { "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", "funding": { "type": "opencollective", @@ -971,8 +948,6 @@ }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -997,8 +972,6 @@ }, "node_modules/@reduxjs/toolkit/node_modules/immer": { "version": "11.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", - "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", "license": "MIT", "funding": { "type": "opencollective", @@ -1007,16 +980,14 @@ }, "node_modules/@restart/context": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", - "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", + "license": "MIT", "peerDependencies": { "react": ">=16.3.2" } }, "node_modules/@restart/hooks": { "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.9.tgz", - "integrity": "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==", + "license": "MIT", "dependencies": { "dequal": "^2.0.2" }, @@ -1024,326 +995,761 @@ "react": ">=16.8.0" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/invariant": { - "version": "2.2.35", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", - "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/react": { - "version": "19.2.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", - "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", - "dependencies": { - "@types/react": "*" - } + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", - "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==" + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], "dev": true, - "engines": { - "node": ">=8" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/autoprefixer": { - "version": "10.4.13", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", - "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", "dependencies": { - "browserslist": "^4.21.4", - "caniuse-lite": "^1.0.30001426", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "node_modules/@types/d3-array": { + "version": "3.2.2", + "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" + "node_modules/@types/d3-color": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" } }, - "node_modules/bootstrap": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", - "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "peerDependencies": { - "jquery": "1.9.1 - 3", - "popper.js": "^1.16.1" + "node_modules/@types/d3-path": { + "version": "3.1.1", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" } }, - "node_modules/bootstrap-select": { - "version": "1.13.18", - "resolved": "https://registry.npmjs.org/bootstrap-select/-/bootstrap-select-1.13.18.tgz", - "integrity": "sha512-V1IzK4rxBq5FrJtkzSH6RmFLFBsjx50byFbfAf8jYyXROWs7ZpprGjdHeoyq2HSsHyjJhMMwjsQhRoYAfxCGow==", - "peerDependencies": { - "bootstrap": ">=3.0.0", - "jquery": "1.9.1 - 3" + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" } }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/@types/d3-time": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", "dev": true, + "license": "MIT" + }, + "node_modules/@types/invariant": { + "version": "2.2.35", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.9", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "csstype": "^3.2.2" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/@types/react-transition-group": { + "version": "4.4.5", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "license": "MIT" + }, + "node_modules/@types/warning": { + "version": "3.0.0", + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "node_modules/@vitest/mocker": { + "version": "3.2.4", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, - "bin": { - "browserslist": "cli.js" + "funding": { + "url": "https://opencollective.com/vitest" }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/buffer-builder": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", - "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.13", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bootstrap": { + "version": "4.6.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" + } + }, + "node_modules/bootstrap-select": { + "version": "1.13.18", + "license": "MIT", + "peerDependencies": { + "bootstrap": ">=3.0.0", + "jquery": "1.9.1 - 3" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-builder": { + "version": "0.2.0", "dev": true, "license": "MIT/X11", "peer": true }, + "node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/caniuse-lite": { "version": "1.0.30001457", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz", - "integrity": "sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==", "dev": true, "funding": [ { @@ -1354,13 +1760,28 @@ "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" } - ] + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1372,10 +1793,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "funding": [ { @@ -1383,6 +1810,7 @@ "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1401,13 +1829,11 @@ }, "node_modules/classnames": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + "license": "MIT" }, "node_modules/clipboard": { "version": "2.0.11", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", - "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "license": "MIT", "dependencies": { "good-listener": "^1.2.2", "select": "^1.1.2", @@ -1416,8 +1842,6 @@ }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -1425,9 +1849,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1437,38 +1860,32 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/colorjs.io": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", - "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1480,9 +1897,8 @@ }, "node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -1492,14 +1908,10 @@ }, "node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/d3-color": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { "node": ">=12" @@ -1507,8 +1919,6 @@ }, "node_modules/d3-ease": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "engines": { "node": ">=12" @@ -1516,20 +1926,14 @@ }, "node_modules/d3-format": { "version": "1.4.5", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", - "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", "license": "BSD-3-Clause" }, "node_modules/d3-time": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", - "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", "license": "BSD-3-Clause" }, "node_modules/d3-time-format": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", - "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", "license": "BSD-3-Clause", "dependencies": { "d3-time": "1 - 2" @@ -1537,8 +1941,6 @@ }, "node_modules/d3-timer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "engines": { "node": ">=12" @@ -1546,8 +1948,7 @@ }, "node_modules/date-fns": { "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "license": "MIT", "engines": { "node": ">=0.11" }, @@ -1556,29 +1957,47 @@ "url": "https://opencollective.com/date-fns" } }, - "node_modules/decimal.js-light": { + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/delegate": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + "license": "MIT" }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/detect-libc": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -1591,15 +2010,13 @@ }, "node_modules/didyoumean": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/dir-glob": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -1609,14 +2026,12 @@ }, "node_modules/dlv": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -1624,26 +2039,26 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.4.311", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz", - "integrity": "sha512-RoDlZufvrtr2Nx3Yx5MB8jX3aHIxm8nRWPJm3yVvyHmyKaRvn90RjzB6hNnt0AkhS3IInJdyRfQb4mWhPvUjVw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" }, "node_modules/es-toolkit": { "version": "1.44.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", - "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", "license": "MIT", "workspaces": [ "docs", @@ -1652,8 +2067,6 @@ }, "node_modules/esbuild": { "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1693,9 +2106,8 @@ }, "node_modules/esbuild-plugin-copy": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esbuild-plugin-copy/-/esbuild-plugin-copy-2.0.2.tgz", - "integrity": "sha512-HlDgkHXagBCwaoB8tlQFeH08/i5a2ey6Pc26annV1YcG5CkAHzzRzmCwp3wdi5KHI//HVUgipS+Zsy2tQmn9gQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.2", "fs-extra": "^10.0.1", @@ -1707,8 +2119,6 @@ }, "node_modules/esbuild-sass-plugin": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-3.3.1.tgz", - "integrity": "sha512-SnO1ls+d52n6j8gRRpjexXI8MsHEaumS0IdDHaYM29Y6gakzZYMls6i9ql9+AWMSQk/eryndmUpXEgT34QrX1A==", "dev": true, "license": "MIT", "dependencies": { @@ -1723,24 +2133,36 @@ }, "node_modules/escalade": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1754,18 +2176,16 @@ }, "node_modules/fastq": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1775,9 +2195,8 @@ }, "node_modules/foreground-child": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, + "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -1791,9 +2210,8 @@ }, "node_modules/fraction.js": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -1804,9 +2222,8 @@ }, "node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1818,16 +2235,13 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1838,8 +2252,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -1848,9 +2260,8 @@ }, "node_modules/glob": { "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -1868,9 +2279,8 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1880,9 +2290,8 @@ }, "node_modules/globby": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -1900,31 +2309,26 @@ }, "node_modules/good-listener": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "license": "MIT", "dependencies": { "delegate": "^3.1.2" } }, "node_modules/graceful-fs": { "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1936,30 +2340,25 @@ }, "node_modules/highlight.js": { "version": "11.7.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz", - "integrity": "sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==", + "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" } }, "node_modules/ignore": { "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + "license": "MIT" }, "node_modules/immer": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", "funding": { "type": "opencollective", @@ -1968,15 +2367,13 @@ }, "node_modules/immutable": { "version": "5.1.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", - "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1984,14 +2381,11 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/internmap": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "license": "ISC", "engines": { "node": ">=12" @@ -1999,17 +2393,15 @@ }, "node_modules/invariant": { "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -2019,8 +2411,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -2035,27 +2425,24 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2065,24 +2452,21 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -2095,34 +2479,28 @@ }, "node_modules/jiti": { "version": "1.21.6", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } }, "node_modules/jquery": { "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", "license": "MIT" }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "license": "MIT" }, "node_modules/json-formatter-js": { "version": "2.3.4", - "resolved": "https://registry.npmjs.org/json-formatter-js/-/json-formatter-js-2.3.4.tgz", - "integrity": "sha512-gmAzYRtPRmYzeAT4T7+t3NhTF89JOAIioCVDddl9YDb3ls3kWcskirafw/MZGJaRhEU6fRimGJHl7CC7gaAI2Q==" + "license": "MIT" }, "node_modules/jsonfile": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -2132,44 +2510,38 @@ }, "node_modules/lie": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } }, "node_modules/lilconfig": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/localforage": { "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", "dependencies": { "lie": "3.1.1" } }, "node_modules/lodash": { "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -2177,34 +2549,43 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/luxon": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", - "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==", + "license": "MIT", "engines": { "node": ">=12" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -2215,9 +2596,8 @@ }, "node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" }, @@ -2230,26 +2610,28 @@ }, "node_modules/minipass": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/moment": { "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "license": "MIT", "engines": { "node": "*" } }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -2258,8 +2640,6 @@ }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -2267,6 +2647,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -2276,97 +2657,84 @@ }, "node_modules/node-addon-api": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, "license": "MIT", "optional": true }, "node_modules/node-releases": { "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/normalize-range": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-hash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -2380,13 +2748,25 @@ }, "node_modules/path-type": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/phoenix": { "resolved": "../deps/phoenix", "link": true @@ -2404,16 +2784,14 @@ "link": true }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -2423,26 +2801,22 @@ }, "node_modules/pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/pirates": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/playwright": { "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2460,8 +2834,6 @@ }, "node_modules/playwright-core": { "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2473,9 +2845,7 @@ }, "node_modules/popper.js": { "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", "peer": true, "funding": { "type": "opencollective", @@ -2483,9 +2853,7 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.5.8", "dev": true, "funding": [ { @@ -2501,10 +2869,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -2512,9 +2881,8 @@ }, "node_modules/postcss-import": { "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -2529,9 +2897,8 @@ }, "node_modules/postcss-js": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, + "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" }, @@ -2548,9 +2915,8 @@ }, "node_modules/postcss-load-config": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", "dev": true, + "license": "MIT", "dependencies": { "lilconfig": "^2.0.5", "yaml": "^2.1.1" @@ -2577,18 +2943,16 @@ }, "node_modules/postcss-load-config/node_modules/yaml": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", "dev": true, + "license": "ISC", "engines": { "node": ">= 14" } }, "node_modules/postcss-nested": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", "dev": true, + "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.11" }, @@ -2605,9 +2969,8 @@ }, "node_modules/postcss-selector-parser": { "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2618,14 +2981,12 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -2634,8 +2995,7 @@ }, "node_modules/prop-types-extra": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", - "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "license": "MIT", "dependencies": { "react-is": "^16.3.2", "warning": "^4.0.0" @@ -2646,8 +3006,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -2662,12 +3020,11 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/react": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -2678,8 +3035,7 @@ }, "node_modules/react-bootstrap": { "version": "1.6.6", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.6.tgz", - "integrity": "sha512-pSzYyJT5u4rc8+5myM8Vid2JG52L8AmYSkpznReH/GM4+FhLqEnxUa0+6HRTaGwjdEixQNGchwY+b3xCdYWrDA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.14.0", "@restart/context": "^2.1.4", @@ -2706,8 +3062,6 @@ }, "node_modules/react-dom": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -2719,18 +3073,14 @@ }, "node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "license": "MIT" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + "license": "MIT" }, "node_modules/react-overlays": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", - "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.8", @@ -2749,8 +3099,6 @@ }, "node_modules/react-redux": { "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", "dependencies": { "@types/use-sync-external-store": "^0.0.6", @@ -2772,8 +3120,6 @@ }, "node_modules/react-spinners": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.17.0.tgz", - "integrity": "sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ==", "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -2782,8 +3128,6 @@ }, "node_modules/react-transition-group": { "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", @@ -2798,18 +3142,16 @@ }, "node_modules/read-cache": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^2.3.0" } }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -2819,8 +3161,6 @@ }, "node_modules/recharts": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", - "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", "license": "MIT", "workspaces": [ "www" @@ -2849,14 +3189,10 @@ }, "node_modules/redux": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "license": "MIT", "peerDependencies": { "redux": "^5.0.0" @@ -2864,14 +3200,10 @@ }, "node_modules/reselect": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "license": "MIT" }, "node_modules/resolve": { "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { @@ -2891,18 +3223,58 @@ }, "node_modules/reusify": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.59.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -2918,14 +3290,13 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/rxjs": { "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -2935,15 +3306,11 @@ }, "node_modules/safe-identifier": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", - "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", "dev": true, "license": "ISC" }, "node_modules/sass": { "version": "1.84.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz", - "integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==", "dev": true, "license": "MIT", "dependencies": { @@ -2963,8 +3330,6 @@ }, "node_modules/sass-embedded": { "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.4.tgz", - "integrity": "sha512-Hf2burRA/y5PGxsg6jB9UpoK/xZ6g/pgrkOcdl6j+rRg1Zj8XhGKZ1MTysZGtTPUUmiiErqzkP5+Kzp95yv9GQ==", "dev": true, "license": "MIT", "peer": true, @@ -3099,8 +3464,6 @@ }, "node_modules/sass-embedded-darwin-arm64": { "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.83.4.tgz", - "integrity": "sha512-rp2ywymWc3nymnSnAFG5R/8hvxWCsuhK3wOnD10IDlmNB7o4rzKby1c+2ZfpQGowlYGWsWWTgz8FW2qzmZsQRw==", "cpu": [ "arm64" ], @@ -3369,8 +3732,6 @@ }, "node_modules/sass-embedded/node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "peer": true, @@ -3384,3237 +3745,964 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/sass/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", - "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sync-child-process": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", - "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "sync-message-port": "^1.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/sync-message-port": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", - "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", - "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", - "dev": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "peer": true - }, - "node_modules/uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "dependencies": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0" - } - }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist-lint": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/varint": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", - "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/victory-vendor": { - "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, - "node_modules/victory-vendor/node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/victory-vendor/node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/victory-vendor/node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/victory-vendor/node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/victory-vendor/node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/victory-vendor/node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - } - }, - "dependencies": { - "@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true - }, - "@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==" - }, - "@bufbuild/protobuf": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz", - "integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==", - "dev": true, - "peer": true - }, - "@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "dev": true, - "optional": true - }, - "@fortawesome/fontawesome-free": { - "version": "5.15.4", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", - "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==" - }, - "@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "requires": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, - "optional": true, - "requires": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1", - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - } - }, - "@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "dev": true, - "optional": true - }, - "@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "dev": true, - "optional": true - }, - "@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "dev": true, - "optional": true - }, - "@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "dev": true, - "optional": true - }, - "@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "dev": true, - "optional": true - }, - "@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "dev": true, - "optional": true - }, - "@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "dev": true, - "optional": true - }, - "@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "dev": true, - "optional": true - }, - "@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "dev": true, - "optional": true - }, - "@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "dev": true, - "optional": true - }, - "@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "dev": true, - "optional": true - }, - "@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "dev": true, - "optional": true - }, - "@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "dev": true, - "optional": true - }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true - }, - "@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" - }, - "@reduxjs/toolkit": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", - "requires": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "dependencies": { - "immer": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", - "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==" - } - } - }, - "@restart/context": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", - "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", - "requires": {} - }, - "@restart/hooks": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.9.tgz", - "integrity": "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==", - "requires": { - "dequal": "^2.0.2" - } - }, - "@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" - }, - "@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" - }, - "@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==" - }, - "@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" - }, - "@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" - }, - "@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "requires": { - "@types/d3-color": "*" - } - }, - "@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" - }, - "@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "requires": { - "@types/d3-time": "*" - } - }, - "@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", - "requires": { - "@types/d3-path": "*" - } - }, - "@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" - }, - "@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" - }, - "@types/invariant": { - "version": "2.2.35", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", - "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" - }, - "@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" - }, - "@types/react": { - "version": "19.2.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", - "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", - "requires": { - "csstype": "^3.2.2" - } - }, - "@types/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", - "requires": { - "@types/react": "*" - } - }, - "@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" - }, - "@types/warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", - "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==" - }, - "ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "autoprefixer": { - "version": "10.4.13", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", - "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "caniuse-lite": "^1.0.30001426", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "bootstrap": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", - "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", - "requires": {} - }, - "bootstrap-select": { - "version": "1.13.18", - "resolved": "https://registry.npmjs.org/bootstrap-select/-/bootstrap-select-1.13.18.tgz", - "integrity": "sha512-V1IzK4rxBq5FrJtkzSH6RmFLFBsjx50byFbfAf8jYyXROWs7ZpprGjdHeoyq2HSsHyjJhMMwjsQhRoYAfxCGow==", - "requires": {} - }, - "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "requires": { - "fill-range": "^7.1.1" - } - }, - "browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" - } - }, - "buffer-builder": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", - "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", - "dev": true, - "peer": true - }, - "camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001457", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz", - "integrity": "sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "classnames": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" - }, - "clipboard": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", - "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", - "requires": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" - } - }, - "clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "colorjs.io": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", - "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "dev": true, - "peer": true - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" - }, - "d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" - }, - "d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" - }, - "d3-format": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", - "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" - }, - "d3-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", - "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" - }, - "d3-time-format": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", - "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", - "requires": { - "d3-time": "1 - 2" - } - }, - "d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" - }, - "date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" - }, - "decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" - }, - "delegate": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" - }, - "dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "optional": true - }, - "didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "requires": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.311", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz", - "integrity": "sha512-RoDlZufvrtr2Nx3Yx5MB8jX3aHIxm8nRWPJm3yVvyHmyKaRvn90RjzB6hNnt0AkhS3IInJdyRfQb4mWhPvUjVw==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "es-toolkit": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", - "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==" - }, - "esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, - "esbuild-plugin-copy": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esbuild-plugin-copy/-/esbuild-plugin-copy-2.0.2.tgz", - "integrity": "sha512-HlDgkHXagBCwaoB8tlQFeH08/i5a2ey6Pc26annV1YcG5CkAHzzRzmCwp3wdi5KHI//HVUgipS+Zsy2tQmn9gQ==", - "dev": true, - "requires": { - "chalk": "^4.1.2", - "fs-extra": "^10.0.1", - "globby": "^11.0.3" - } - }, - "esbuild-sass-plugin": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-3.3.1.tgz", - "integrity": "sha512-SnO1ls+d52n6j8gRRpjexXI8MsHEaumS0IdDHaYM29Y6gakzZYMls6i9ql9+AWMSQk/eryndmUpXEgT34QrX1A==", - "dev": true, - "requires": { - "resolve": "^1.22.8", - "safe-identifier": "^0.4.2", - "sass": "^1.71.1" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==" - }, - "fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - } - }, - "fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "dev": true - }, - "fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true - }, - "glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "good-listener": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", - "requires": { - "delegate": "^3.1.2" - } - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "requires": { - "function-bind": "^1.1.2" - } - }, - "highlight.js": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz", - "integrity": "sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==" - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==" - }, - "immutable": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", - "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "^1.0.0" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "requires": { - "hasown": "^2.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" - } - }, - "jiti": { - "version": "1.21.6", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "dev": true - }, - "jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "json-formatter-js": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/json-formatter-js/-/json-formatter-js-2.3.4.tgz", - "integrity": "sha512-gmAzYRtPRmYzeAT4T7+t3NhTF89JOAIioCVDddl9YDb3ls3kWcskirafw/MZGJaRhEU6fRimGJHl7CC7gaAI2Q==" - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "requires": { - "immediate": "~3.0.5" - } - }, - "lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "requires": { - "lie": "3.1.1" - } - }, - "lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, - "luxon": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", - "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "requires": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.2" - } - }, - "minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true - }, - "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" - }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true - }, - "node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "optional": true - }, - "node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "requires": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "phoenix": { - "version": "file:../deps/phoenix" - }, - "phoenix_html": { - "version": "file:../deps/phoenix_html" - }, - "phoenix_live_react": { - "version": "file:../deps/phoenix_live_react" - }, - "phoenix_live_view": { - "version": "file:../deps/phoenix_live_view", - "requires": { - "@babel/cli": "7.27.0", - "@babel/core": "7.26.10", - "@babel/preset-env": "7.26.9", - "@eslint/js": "^9.24.0", - "@playwright/test": "^1.51.1", - "@stylistic/eslint-plugin-js": "^4.2.0", - "css.escape": "^1.5.1", - "eslint": "9.24.0", - "eslint-plugin-jest": "28.11.0", - "eslint-plugin-playwright": "^2.2.0", - "globals": "^16.0.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-monocart-coverage": "^1.1.1", - "monocart-reporter": "^2.9.17", - "morphdom": "2.7.7", - "phoenix": "1.7.21" - } - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true - }, - "pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true - }, - "playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "dev": true, - "requires": { - "fsevents": "2.3.2", - "playwright-core": "1.57.0" - } - }, - "playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "dev": true - }, - "popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "peer": true - }, - "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - } - }, - "postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "requires": { - "camelcase-css": "^2.0.1" - } - }, - "postcss-load-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", "dev": true, - "requires": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" - }, + "license": "MIT", "dependencies": { - "yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", - "dev": true - } + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.1", "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.11" + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "node_modules/scheduler": { + "version": "0.23.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" } }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "node_modules/select": { + "version": "1.1.2", + "license": "MIT" }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "prop-types-extra": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", - "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", - "requires": { - "react-is": "^16.3.2", - "warning": "^4.0.0" + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" }, - "react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "requires": { - "loose-envify": "^1.1.0" + "node_modules/signal-exit": { + "version": "4.0.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "react-bootstrap": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.6.tgz", - "integrity": "sha512-pSzYyJT5u4rc8+5myM8Vid2JG52L8AmYSkpznReH/GM4+FhLqEnxUa0+6HRTaGwjdEixQNGchwY+b3xCdYWrDA==", - "requires": { - "@babel/runtime": "^7.14.0", - "@restart/context": "^2.1.4", - "@restart/hooks": "^0.4.7", - "@types/invariant": "^2.2.33", - "@types/prop-types": "^15.7.3", - "@types/react": ">=16.14.8", - "@types/react-transition-group": "^4.4.1", - "@types/warning": "^3.0.0", - "classnames": "^2.3.1", - "dom-helpers": "^5.2.1", - "invariant": "^2.2.4", - "prop-types": "^15.7.2", - "prop-types-extra": "^1.1.0", - "react-overlays": "^5.1.2", - "react-transition-group": "^4.4.1", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" }, - "react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + "node_modules/std-env": { + "version": "3.10.0", + "dev": true, + "license": "MIT" }, - "react-overlays": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", - "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", - "requires": { - "@babel/runtime": "^7.13.8", - "@popperjs/core": "^2.11.6", - "@restart/hooks": "^0.4.7", - "@types/warning": "^3.0.0", - "dom-helpers": "^5.2.0", - "prop-types": "^15.7.2", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" + "node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "requires": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "react-spinners": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.17.0.tgz", - "integrity": "sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ==", - "requires": {} - }, - "react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "requires": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", "dev": true, - "requires": { - "pify": "^2.3.0" + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/strip-ansi": { + "version": "7.1.2", "dev": true, - "requires": { - "picomatch": "^2.2.1" + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "recharts": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", - "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", - "requires": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", - "clsx": "^2.1.1", - "decimal.js-light": "^2.5.1", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "immer": "^10.1.1", - "react-redux": "8.x.x || 9.x.x", - "reselect": "5.1.1", - "tiny-invariant": "^1.3.3", - "use-sync-external-store": "^1.2.2", - "victory-vendor": "^37.0.2" + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "redux": { + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "redux-thunk": { + "node_modules/strip-literal": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "requires": {} - }, - "reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" - }, - "resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, - "requires": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "node_modules/sucrase": { + "version": "3.32.0", "dev": true, - "requires": { - "queue-microtask": "^1.2.2" + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" } }, - "rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "1.1.12", "dev": true, - "peer": true, - "requires": { - "tslib": "^2.1.0" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "safe-identifier": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", - "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", - "dev": true - }, - "sass": { - "version": "1.84.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz", - "integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==", + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", "dev": true, - "requires": { - "@parcel/watcher": "^2.4.1", - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, + "license": "ISC", "dependencies": { - "chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "requires": { - "readdirp": "^4.0.1" - } - }, - "readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", - "dev": true - } + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "sass-embedded": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.4.tgz", - "integrity": "sha512-Hf2burRA/y5PGxsg6jB9UpoK/xZ6g/pgrkOcdl6j+rRg1Zj8XhGKZ1MTysZGtTPUUmiiErqzkP5+Kzp95yv9GQ==", + "node_modules/sucrase/node_modules/minimatch": { + "version": "3.1.5", "dev": true, - "peer": true, - "requires": { - "@bufbuild/protobuf": "^2.0.0", - "buffer-builder": "^0.2.0", - "colorjs.io": "^0.5.0", - "immutable": "^5.0.2", - "rxjs": "^7.4.0", - "sass-embedded-android-arm": "1.83.4", - "sass-embedded-android-arm64": "1.83.4", - "sass-embedded-android-ia32": "1.83.4", - "sass-embedded-android-riscv64": "1.83.4", - "sass-embedded-android-x64": "1.83.4", - "sass-embedded-darwin-arm64": "1.83.4", - "sass-embedded-darwin-x64": "1.83.4", - "sass-embedded-linux-arm": "1.83.4", - "sass-embedded-linux-arm64": "1.83.4", - "sass-embedded-linux-ia32": "1.83.4", - "sass-embedded-linux-musl-arm": "1.83.4", - "sass-embedded-linux-musl-arm64": "1.83.4", - "sass-embedded-linux-musl-ia32": "1.83.4", - "sass-embedded-linux-musl-riscv64": "1.83.4", - "sass-embedded-linux-musl-x64": "1.83.4", - "sass-embedded-linux-riscv64": "1.83.4", - "sass-embedded-linux-x64": "1.83.4", - "sass-embedded-win32-arm64": "1.83.4", - "sass-embedded-win32-ia32": "1.83.4", - "sass-embedded-win32-x64": "1.83.4", - "supports-color": "^8.1.1", - "sync-child-process": "^1.0.2", - "varint": "^6.0.0" - }, + "license": "ISC", "dependencies": { - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "sass-embedded-android-arm": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.83.4.tgz", - "integrity": "sha512-9Z4pJAOgEkXa3VDY/o+U6l5XvV0mZTJcSl0l/mSPHihjAHSpLYnOW6+KOWeM8dxqrsqTYcd6COzhanI/a++5Gw==", + "node_modules/supports-color": { + "version": "7.2.0", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "sass-embedded-android-arm64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.83.4.tgz", - "integrity": "sha512-tgX4FzmbVqnQmD67ZxQDvI+qFNABrboOQgwsG05E5bA/US42zGajW9AxpECJYiMXVOHmg+d81ICbjb0fsVHskw==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "sass-embedded-android-ia32": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.83.4.tgz", - "integrity": "sha512-RsFOziFqPcfZXdFRULC4Ayzy9aK6R6FwQ411broCjlOBX+b0gurjRadkue3cfUEUR5mmy0KeCbp7zVKPLTK+5Q==", + "node_modules/sync-child-process": { + "version": "1.0.2", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "peer": true, + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } }, - "sass-embedded-android-riscv64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.83.4.tgz", - "integrity": "sha512-EHwh0nmQarBBrMRU928eTZkFGx19k/XW2YwbPR4gBVdWLkbTgCA5aGe8hTE6/1zStyx++3nDGvTZ78+b/VvvLg==", + "node_modules/sync-message-port": { + "version": "1.1.3", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.0.0" + } }, - "sass-embedded-android-x64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.83.4.tgz", - "integrity": "sha512-0PgQNuPWYy1jEOEPDVsV89KfqOsMLIp9CSbjBY7jRcwRhyVAcigqrUG6bDeNtojHUYKA1kU+Eh/85WxOHUOgBw==", + "node_modules/tailwindcss": { + "version": "3.4.10", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } }, - "sass-embedded-darwin-arm64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.83.4.tgz", - "integrity": "sha512-rp2ywymWc3nymnSnAFG5R/8hvxWCsuhK3wOnD10IDlmNB7o4rzKby1c+2ZfpQGowlYGWsWWTgz8FW2qzmZsQRw==", + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", "dev": true, - "optional": true, - "peer": true + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } }, - "sass-embedded-darwin-x64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.83.4.tgz", - "integrity": "sha512-kLkN2lXz9PCgGfDS8Ev5YVcl/V2173L6379en/CaFuJJi7WiyPgBymW7hOmfCt4uO4R1y7CP2Uc08DRtZsBlAA==", + "node_modules/thenify": { + "version": "3.3.1", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } }, - "sass-embedded-linux-arm": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.83.4.tgz", - "integrity": "sha512-nL90ryxX2lNmFucr9jYUyHHx21AoAgdCL1O5Ltx2rKg2xTdytAGHYo2MT5S0LIeKLa/yKP/hjuSvrbICYNDvtA==", + "node_modules/thenify-all": { + "version": "1.6.0", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } }, - "sass-embedded-linux-arm64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.83.4.tgz", - "integrity": "sha512-E0zjsZX2HgESwyqw31EHtI39DKa7RgK7nvIhIRco1d0QEw227WnoR9pjH3M/ZQy4gQj3GKilOFHM5Krs/omeIA==", - "dev": true, - "optional": true, - "peer": true + "node_modules/tiny-emitter": { + "version": "2.1.0", + "license": "MIT" }, - "sass-embedded-linux-ia32": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.83.4.tgz", - "integrity": "sha512-ew5HpchSzgAYbQoriRh8QhlWn5Kw2nQ2jHoV9YLwGKe3fwwOWA0KDedssvDv7FWnY/FCqXyymhLd6Bxae4Xquw==", + "node_modules/tiny-invariant": { + "version": "1.3.3", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, - "sass-embedded-linux-musl-arm": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.83.4.tgz", - "integrity": "sha512-0RrJRwMrmm+gG0VOB5b5Cjs7Sd+lhqpQJa6EJNEaZHljJokEfpE5GejZsGMRMIQLxEvVphZnnxl6sonCGFE/QQ==", + "node_modules/tinyexec": { + "version": "0.3.2", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, - "sass-embedded-linux-musl-arm64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.83.4.tgz", - "integrity": "sha512-IzMgalf6MZOxgp4AVCgsaWAFDP/IVWOrgVXxkyhw29fyAEoSWBJH4k87wyPhEtxSuzVHLxKNbc8k3UzdWmlBFg==", + "node_modules/tinyglobby": { + "version": "0.2.15", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } }, - "sass-embedded-linux-musl-ia32": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.83.4.tgz", - "integrity": "sha512-LLb4lYbcxPzX4UaJymYXC+WwokxUlfTJEFUv5VF0OTuSsHAGNRs/rslPtzVBTvMeG9TtlOQDhku1F7G6iaDotA==", + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } }, - "sass-embedded-linux-musl-riscv64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.83.4.tgz", - "integrity": "sha512-zoKlPzD5Z13HKin1UGR74QkEy+kZEk2AkGX5RelRG494mi+IWwRuWCppXIovor9+BQb9eDWPYPoMVahwN5F7VA==", + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "sass-embedded-linux-musl-x64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.83.4.tgz", - "integrity": "sha512-hB8+/PYhfEf2zTIcidO5Bpof9trK6WJjZ4T8g2MrxQh8REVtdPcgIkoxczRynqybf9+fbqbUwzXtiUao2GV+vQ==", + "node_modules/tinypool": { + "version": "1.1.1", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } }, - "sass-embedded-linux-riscv64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.83.4.tgz", - "integrity": "sha512-83fL4n+oeDJ0Y4KjASmZ9jHS1Vl9ESVQYHMhJE0i4xDi/P3BNarm2rsKljq/QtrwGpbqwn8ujzOu7DsNCMDSHA==", + "node_modules/tinyrainbow": { + "version": "2.0.0", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, - "sass-embedded-linux-x64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.83.4.tgz", - "integrity": "sha512-NlnGdvCmTD5PK+LKXlK3sAuxOgbRIEoZfnHvxd157imCm/s2SYF/R28D0DAAjEViyI8DovIWghgbcqwuertXsA==", + "node_modules/tinyspy": { + "version": "4.0.4", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, - "sass-embedded-win32-arm64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.83.4.tgz", - "integrity": "sha512-J2BFKrEaeSrVazU2qTjyQdAk+MvbzJeTuCET0uAJEXSKtvQ3AzxvzndS7LqkDPbF32eXAHLw8GVpwcBwKbB3Uw==", + "node_modules/to-regex-range": { + "version": "5.0.1", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } }, - "sass-embedded-win32-ia32": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.83.4.tgz", - "integrity": "sha512-uPAe9T/5sANFhJS5dcfAOhOJy8/l2TRYG4r+UO3Wp4yhqbN7bggPvY9c7zMYS0OC8tU/bCvfYUDFHYMCl91FgA==", + "node_modules/ts-interface-checker": { + "version": "0.1.13", "dev": true, - "optional": true, - "peer": true + "license": "Apache-2.0" }, - "sass-embedded-win32-x64": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.83.4.tgz", - "integrity": "sha512-C9fkDY0jKITdJFij4UbfPFswxoXN9O/Dr79v17fJnstVwtUojzVJWKHUXvF0Zg2LIR7TCc4ju3adejKFxj7ueA==", + "node_modules/tslib": { + "version": "2.8.1", "dev": true, - "optional": true, + "license": "0BSD", "peer": true }, - "scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "requires": { - "loose-envify": "^1.1.0" + "node_modules/uncontrollable": { + "version": "7.2.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" } }, - "select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" - }, - "shebang-command": { + "node_modules/universalify": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "requires": { - "shebang-regex": "^3.0.0" + "license": "MIT", + "engines": { + "node": ">= 10.0.0" } }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", - "dev": true + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } }, - "source-map-js": { + "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true + "dev": true, + "license": "MIT" }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/varint": { + "version": "6.0.0", "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "license": "MIT", + "peer": true + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "node_modules/victory-vendor/node_modules/d3-array": { + "version": "3.2.4", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" }, + "engines": { + "node": ">=12" + } + }, + "node_modules/victory-vendor/node_modules/d3-interpolate": { + "version": "3.0.1", + "license": "ISC", "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" } }, - "strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" + "node_modules/victory-vendor/node_modules/d3-path": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" + "node_modules/victory-vendor/node_modules/d3-scale": { + "version": "4.0.2", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" }, + "engines": { + "node": ">=12" + } + }, + "node_modules/victory-vendor/node_modules/d3-shape": { + "version": "3.2.0", + "license": "ISC", "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - } + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" } }, - "sucrase": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", - "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" + "node_modules/victory-vendor/node_modules/d3-time": { + "version": "3.1.0", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" }, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "tsx": { + "optional": true }, - "minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } + "yaml": { + "optional": true } } }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "sync-child-process": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", - "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "node_modules/vite-node": { + "version": "3.2.4", "dev": true, - "peer": true, - "requires": { - "sync-message-port": "^1.0.0" + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "sync-message-port": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", - "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", - "dev": true, - "peer": true - }, - "tailwindcss": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", - "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", "dev": true, - "requires": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "license": "MIT", + "engines": { + "node": ">=12.0.0" }, - "dependencies": { - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true } } }, - "thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, - "tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" - }, - "tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", "dev": true, - "peer": true - }, - "uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "requires": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "requires": {} - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "varint": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", - "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "node_modules/vitest": { + "version": "3.2.4", "dev": true, - "peer": true - }, - "victory-vendor": { - "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", - "requires": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - }, + "license": "MIT", "dependencies": { - "d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "requires": { - "internmap": "1 - 2" - } + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true }, - "d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "requires": { - "d3-color": "1 - 3" - } + "@types/node": { + "optional": true }, - "d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + "@vitest/browser": { + "optional": true }, - "d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "requires": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - } + "@vitest/ui": { + "optional": true }, - "d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "requires": { - "d3-path": "^3.1.0" - } + "happy-dom": { + "optional": true }, - "d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "requires": { - "d3-array": "2 - 3" - } + "jsdom": { + "optional": true } } }, - "warning": { + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/warning": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "requires": { + "license": "MIT", + "dependencies": { "loose-envify": "^1.0.0" } }, - "which": { + "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "requires": { + "license": "ISC", + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, - "wrap-ansi": { + "node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" }, - "dependencies": { - "ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true - } + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" } } } diff --git a/assets/package.json b/assets/package.json index 3ce61a8e5..ad986c5d9 100644 --- a/assets/package.json +++ b/assets/package.json @@ -3,6 +3,7 @@ "license": "MIT", "scripts": { "deploy": "node ./build.mjs", + "test": "vitest run", "watch": "node ./build.mjs watch" }, "dependencies": { @@ -37,6 +38,7 @@ "playwright": "^1.57.0", "postcss": "^8.4.31", "sass": "^1.58.3", - "tailwindcss": "^3.4.10" + "tailwindcss": "^3.4.10", + "vitest": "^3.2.4" } } From 31491d6054b56d2a1404eab836688e5370703259 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Fri, 27 Mar 2026 16:41:02 +1000 Subject: [PATCH 36/44] test: full list of aggregate types --- test/logflare/logs/search_operations_test.exs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/logflare/logs/search_operations_test.exs b/test/logflare/logs/search_operations_test.exs index 5e20c3251..291144b45 100644 --- a/test/logflare/logs/search_operations_test.exs +++ b/test/logflare/logs/search_operations_test.exs @@ -245,7 +245,7 @@ defmodule Logflare.Logs.SearchOperationsTest do end test "generates expected SQL for all aggregate types", %{base_so: base_so} do - for agg <- [:count, :avg, :sum, :max] do + for agg <- [:count, :avg, :sum, :max, :countd, :p50, :p95, :p99] do chart_rule = %ChartRule{ path: "event_message", aggregate: agg, @@ -266,6 +266,10 @@ defmodule Logflare.Logs.SearchOperationsTest do expected_aggregate_sql = case agg do :count -> ~s|count(t0."timestamp")| + :countd -> ~s|count(distinct t0."event_message")| + :p50 -> ~s|percentile_cont(| + :p95 -> ~s|percentile_cont(| + :p99 -> ~s|percentile_cont(| _ -> ~s|#{agg}(t0."event_message")| end From 02c132711196fa6be97325a6ce75aa42edb9f3ae Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Thu, 19 Mar 2026 14:44:54 +1000 Subject: [PATCH 37/44] refactor: move SourceSchema fetch to LiveView --- .../live/search_live/form_components.ex | 21 +++++++++---------- .../live/search_live/logs_search_lv.ex | 12 +++++++++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/logflare_web/live/search_live/form_components.ex b/lib/logflare_web/live/search_live/form_components.ex index 29ebf5913..dfeeb3589 100644 --- a/lib/logflare_web/live/search_live/form_components.ex +++ b/lib/logflare_web/live/search_live/form_components.ex @@ -8,7 +8,6 @@ defmodule LogflareWeb.SearchLive.FormComponents do use Phoenix.Component alias Logflare.Lql.Rules - alias Logflare.SourceSchemas alias Logflare.Utils alias Logflare.Sources.Source @@ -141,12 +140,16 @@ defmodule LogflareWeb.SearchLive.FormComponents do attr :has_results?, :boolean attr :source, Logflare.Sources.Source, required: true attr :last_query_completed_at, :any, default: nil + attr :lql_schema_flat_map, :map, required: true def search_controls(assigns) do assigns = assigns |> assign(:saved_searches_json, JSON.encode!(assigns.saved_searches)) - |> assign(:lql_schema_fields_json, assigns.source |> lql_schema_fields() |> Jason.encode!()) + |> assign( + :lql_schema_fields_json, + assigns.lql_schema_flat_map |> lql_schema_fields() |> Jason.encode!() + ) ~H"""
@@ -226,18 +229,14 @@ defmodule LogflareWeb.SearchLive.FormComponents do |> Kernel.in([:integer, :float]) end - defp lql_schema_fields(source) do - case SourceSchemas.Cache.get_source_schema_by(source_id: source.id) do - %{schema_flat_map: flat_map} when is_map(flat_map) -> - for {name, type} <- flat_map, into: %{} do - {name, format_schema_type(type)} - end - - _ -> - %{} + defp lql_schema_fields(flat_map) when is_map(flat_map) do + for {name, type} <- flat_map, into: %{} do + {name, format_schema_type(type)} end end + defp lql_schema_fields(_), do: %{} + defp format_schema_type({type, inner}), do: "#{type}[#{inner}]" defp format_schema_type(type), do: to_string(type) 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 2b382104b..f71526d28 100644 --- a/lib/logflare_web/live/search_live/logs_search_lv.ex +++ b/lib/logflare_web/live/search_live/logs_search_lv.ex @@ -296,6 +296,7 @@ defmodule LogflareWeb.Source.SearchLV do has_results?={[@search_op_log_events, @search_op_log_aggregates] |> Enum.any?()} source={@source} last_query_completed_at={@last_query_completed_at} + lql_schema_flat_map={lql_schema_flat_map(@source)} />
@@ -1184,6 +1185,17 @@ defmodule LogflareWeb.Source.SearchLV do |> push_event("set_lql_value", %{value: qs}) end + @spec lql_schema_flat_map(Logflare.Sources.Source.t()) :: map() + defp lql_schema_flat_map(source) do + case SourceSchemas.Cache.get_source_schema_by(source_id: source.id) do + %{schema_flat_map: flat_map} when is_map(flat_map) -> + flat_map + + _ -> + %{} + end + end + defp saved_searches(source) do source.id |> SavedSearches.Cache.list_saved_searches_by_source() From 7d731f9e6bf8db3a275fc9829c6bb66e6a6d34f2 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Mon, 30 Mar 2026 08:22:41 +1000 Subject: [PATCH 38/44] bump ci From da2c2eff474a00dd9e4dd3acdebc6f18b37cb54f Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Wed, 1 Apr 2026 11:45:12 +1000 Subject: [PATCH 39/44] test: put_sql_string matches on backend_type --- lib/logflare/logs/search_operations.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/logflare/logs/search_operations.ex b/lib/logflare/logs/search_operations.ex index 746180832..4cd4266bc 100644 --- a/lib/logflare/logs/search_operations.ex +++ b/lib/logflare/logs/search_operations.ex @@ -90,7 +90,7 @@ defmodule Logflare.Logs.SearchOperations do defp put_sql_string(%{sql_string: sql_string} = so, _response) when is_binary(sql_string), do: so - defp put_sql_string(so, %QueryResult{ + defp put_sql_string(%SO{backend_type: :bigquery} = so, %QueryResult{ query_string: query_string, bq_params: bq_params }) do @@ -101,7 +101,7 @@ defmodule Logflare.Logs.SearchOperations do } end - defp put_sql_string(%SO{} = so, _response) do + defp put_sql_string(%SO{backend_type: :postgres} = so, _response) do case PostgresAdaptor.ecto_to_sql(so.query, []) do {:ok, {query_string, params}} -> %{so | sql_string: query_string, sql_params: params} {:error, _reason} -> so From 4f4a147b21b9794a15cd6ca9d7ab8aa06a22e849 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Thu, 2 Apr 2026 09:10:56 +1000 Subject: [PATCH 40/44] ci: disable static asset gzip encoding in test --- config/test.exs | 1 + lib/logflare_web/endpoint.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index 27b4b30ed..a51bb53ae 100644 --- a/config/test.exs +++ b/config/test.exs @@ -4,6 +4,7 @@ config :logflare, env: :test, cache_stats: true, sql_sandbox: true, + static_gzip: false, encryption_key_default: "Q+IS7ogkzRxsj+zAIB1u6jNFquxkFzSrBZXItN27K/Q=" config :logflare, LogflareWeb.Endpoint, diff --git a/lib/logflare_web/endpoint.ex b/lib/logflare_web/endpoint.ex index e69a82233..a1f00b402 100644 --- a/lib/logflare_web/endpoint.ex +++ b/lib/logflare_web/endpoint.ex @@ -22,7 +22,7 @@ defmodule LogflareWeb.Endpoint do plug(Plug.Static, at: "/", from: :logflare, - gzip: !code_reloading?, + gzip: !code_reloading? and Application.compile_env(:logflare, :static_gzip, true), only: ~w(css fonts images js favicon.ico robots.txt worker.js manifest.json), only_matching: ~w(manifest) ) From 76bfa2797ac7fba926f4bfbb65321a6474550f06 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Thu, 2 Apr 2026 14:24:09 +1000 Subject: [PATCH 41/44] test: e2e integration test for search chart controls --- test/e2e/features/logs_search_test.exs | 163 +++++++++++++------------ 1 file changed, 86 insertions(+), 77 deletions(-) diff --git a/test/e2e/features/logs_search_test.exs b/test/e2e/features/logs_search_test.exs index 76054f965..11282773e 100644 --- a/test/e2e/features/logs_search_test.exs +++ b/test/e2e/features/logs_search_test.exs @@ -16,7 +16,7 @@ defmodule E2e.Features.LogsSearchTest do setup do user = SingleTenant.get_default_user() - source = insert(:source, user: user, suggested_keys: "event_message") + source = insert(:source, user: user) matching_message = "featuresearchmatch#{System.unique_integer([:positive])}" non_matching_message = "featuresearchmiss#{System.unique_integer([:positive])}" @@ -52,8 +52,8 @@ defmodule E2e.Features.LogsSearchTest do |> visit( ~p"/sources/#{source.id}/search?#{%{querystring: "event_message:#{matching_message}"}}" ) - |> assert_has("#logs-list-container", text: matching_message, timeout: 10_000) - |> refute_has("#logs-list-container", text: non_matching_message, timeout: 10_000) + |> assert_has("#logs-list-container", text: matching_message) + |> refute_has("#logs-list-container", text: non_matching_message) end test "cancelling the datepicker resumes tailing", %{ @@ -64,10 +64,11 @@ defmodule E2e.Features.LogsSearchTest do |> visit(~p"/auth/login/single_tenant") |> assert_path(~p"/dashboard") |> visit(~p"/sources/#{source.id}/search") - |> assert_has(".live-pause", text: "Live", timeout: 10_000) - |> open_datepicker() - |> click(".daterangepicker .cancelBtn") - |> assert_has(".live-pause", text: "Pause", timeout: 10_000) + |> assert_has(".live-pause", text: "Pause") + |> click("#daterangepicker") + |> wait_for_selector(".daterangepicker", state: "attached") + |> click_date_range_cancel() + |> assert_has(".live-pause", text: "Pause") end test "applying a preset date range updates the search query", %{ @@ -79,101 +80,109 @@ defmodule E2e.Features.LogsSearchTest do |> visit(~p"/auth/login/single_tenant") |> assert_path(~p"/dashboard") |> visit(~p"/sources/#{source.id}/search") - |> open_datepicker() - |> click(".daterangepicker .ranges li", "Last 15 Minutes") - |> assert_has(".live-pause", text: "Live", timeout: 10_000) + |> click("span", "DateTime") + |> click_date_range_preset("Last 15 Minutes") + |> assert_has(".live-pause", text: "Live") querystring = - wait_for_editor_querystring(conn, fn querystring -> - String.contains?(querystring, "t:last@15") - end) + wait_for_editor_querystring(conn, "t:last@15") assert querystring =~ "t:last@15" end + + test "changing chart period updates the search query", %{ + conn: conn, + source: source + } do + conn = + conn + |> visit(~p"/auth/login/single_tenant") + |> assert_path(~p"/dashboard") + |> visit(~p"/sources/#{source.id}/search") + |> wait_for_selector("#source-logs-search-list") + + wait_for_editor_querystring(conn, "") + + conn + |> unwrap(fn %{frame_id: frame_id} -> + {:ok, _} = + PlaywrightEx.Frame.select_option(frame_id, + selector: "#search_chart_period", + options: [%{label: "hour"}], + timeout: 5_000 + ) + end) + + querystring = + wait_for_editor_querystring(conn, "t::hour") + + assert querystring =~ "c:group_by(t::hour)" + end end - defp current_editor_querystring(conn) do - ref = make_ref() + def wait_for_selector(conn, selector, opts \\ []) do + opts = opts |> Keyword.merge(selector: selector, timeout: 10_000) conn |> unwrap(fn %{frame_id: frame_id} -> - {:ok, querystring} = - Frame.evaluate( - frame_id, - expression: ~S|document.querySelector("#lql-editor-hook")?.dataset.querystring ?? ""|, - timeout: 5_000 - ) - - send(self(), {ref, querystring}) + Frame.wait_for_selector(frame_id, opts) end) - - assert_receive {^ref, querystring} - querystring end - defp open_datepicker(conn, timeout_ms \\ 10_000) do - deadline = System.monotonic_time(:millisecond) + timeout_ms - do_open_datepicker(conn, deadline) + defp click_date_range_preset(conn, preset) do + trigger_click_event(conn, ~s|.daterangepicker .ranges li[data-range-key="#{preset}"]|) end - defp wait_for_editor_querystring(conn, predicate, timeout_ms \\ 10_000) do - deadline = System.monotonic_time(:millisecond) + timeout_ms - do_wait_for_editor_querystring(conn, predicate, deadline) + defp click_date_range_cancel(conn) do + trigger_click_event(conn, ".daterangepicker .cancelBtn") end - defp do_open_datepicker(conn, deadline) do - conn = click(conn, "#daterangepicker") - - case wait_for_selector(conn, ".daterangepicker", 500, state: "attached") do - :ok -> - conn - - :error -> - if System.monotonic_time(:millisecond) < deadline do - Process.sleep(100) - do_open_datepicker(conn, deadline) - else - flunk("Timed out waiting for the datepicker to open") - end - end + defp trigger_click_event(conn, selector) do + conn + |> unwrap(fn %{frame_id: frame_id} -> + {:ok, _event} = + Frame.dispatch_event(frame_id, + selector: selector, + type: "click", + event_init: %{bubbles: true, cancelable: true}, + timeout: 5_000 + ) + end) end - defp wait_for_selector(conn, selector, timeout_ms, opts) do + defp wait_for_editor_querystring(conn, expected_fragment, timeout_ms \\ 10_000) do ref = make_ref() conn |> unwrap(fn %{frame_id: frame_id} -> - result = - case Frame.wait_for_selector( - frame_id, - Keyword.merge(opts, selector: selector, timeout: timeout_ms) - ) do - {:ok, _} -> :ok - {:error, _} -> :error - end - - send(self(), {ref, result}) - end) - - assert_receive {^ref, result} - result - end - - defp do_wait_for_editor_querystring(conn, predicate, deadline) do - querystring = current_editor_querystring(conn) + {:ok, _} = + Frame.wait_for_function(frame_id, + expression: """ + ({ expectedFragment }) => { + const querystring = + document.querySelector("#lql-editor-hook")?.dataset.querystring ?? "" + + if (expectedFragment === "") return querystring !== "" + + return querystring.includes(expectedFragment) + } + """, + is_function: true, + arg: %{expectedFragment: expected_fragment}, + timeout: timeout_ms + ) - cond do - predicate.(querystring) -> - querystring + {:ok, querystring} = + Frame.evaluate( + frame_id, + expression: ~S|document.querySelector("#lql-editor-hook")?.dataset.querystring ?? ""|, + timeout: 5_000 + ) - System.monotonic_time(:millisecond) < deadline -> - Process.sleep(100) - do_wait_for_editor_querystring(conn, predicate, deadline) + send(self(), {ref, querystring}) + end) - true -> - flunk( - "Timed out waiting for editor querystring update, last querystring: #{inspect(querystring)}" - ) - end + assert_receive {^ref, querystring} + querystring end end From cd80605fc7d1ce702a9ffc43091f856d8fd74896 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Thu, 2 Apr 2026 16:15:39 +1000 Subject: [PATCH 42/44] chore: bump playwright_ex --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index df55b7f6a..1752e2489 100644 --- a/mix.lock +++ b/mix.lock @@ -129,7 +129,7 @@ "phoenix_test": {:hex, :phoenix_test, "0.9.1", "ac58a4d341c594ac57ce52a6ce643200084fad419a91b72896a44881fe84809c", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:lazy_html, "~> 0.1.7", [hex: :lazy_html, repo: "hexpm", optional: false]}, {:mime, ">= 1.0.0", [hex: :mime, repo: "hexpm", optional: true]}, {:phoenix, ">= 1.7.10", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "ed453394c0f8987aa58a06e2302e7dd4bc53cd2d25eff5a18c4a5775241ebe61"}, "phoenix_test_playwright": {:hex, :phoenix_test_playwright, "0.11.1", "02a03006d148993da544a1593034026938a367aba1906c678ccbd764ceaaf7cb", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_ecto, "~> 4.5", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_test, "~> 0.8", [hex: :phoenix_test, repo: "hexpm", optional: false]}, {:playwright_ex, "~> 0.4", [hex: :playwright_ex, repo: "hexpm", optional: false]}, {:websockex, "~> 0.4", [hex: :websockex, repo: "hexpm", optional: true]}], "hexpm", "a792fbc8c5c34f53a77c6933fce0a3e05471bda0c6bf4c5ed23e7dca5acf6da7"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "playwright_ex": {:hex, :playwright_ex, "0.4.0", "26e824ca7257932758410aee5df5e37edf1b156e7941e3d1e3a8fae2c6f604df", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:websockex, "~> 0.4", [hex: :websockex, repo: "hexpm", optional: true]}], "hexpm", "e08cf88b1fcab0087676e3a9da2a847ac4d31040fb6f24282910b78e3e99500b"}, + "playwright_ex": {:hex, :playwright_ex, "0.5.0", "87db628ad39afc0eaf864a9f5919ffc1a224ea267ef7c813507839f85fd432e2", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:websockex, "~> 0.4", [hex: :websockex, repo: "hexpm", optional: true]}], "hexpm", "afe8db4f6fd9bb6cc0f3df2f45edef337b86fc69947aecaa390428e3a7a86d16"}, "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_caisson": {:hex, :plug_caisson, "0.2.1", "aa6a45a4f0e674459b8881d742cc0e8c7d5d0e008a29fe84dc10ab95d6fcfa74", [:mix], [{:brotli, "~> 0.3.2", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "661887bca916122c31717842fa6496c5a4d92c22b5dbef6bd6973d28188939cc"}, "plug_cowboy": {:hex, :plug_cowboy, "2.8.0", "07789e9c03539ee51bb14a07839cc95aa96999fd8846ebfd28c97f0b50c7b612", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9cbfaaf17463334ca31aed38ea7e08a68ee37cabc077b1e9be6d2fb68e0171d0"}, From 5414c166e52ab44b0c4658e31646cd3d4b2ccf61 Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Fri, 3 Apr 2026 11:35:30 +1000 Subject: [PATCH 43/44] formatting --- test/logflare/logs/search_operations_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/logflare/logs/search_operations_test.exs b/test/logflare/logs/search_operations_test.exs index 8b03c7e92..2dbdbee2c 100644 --- a/test/logflare/logs/search_operations_test.exs +++ b/test/logflare/logs/search_operations_test.exs @@ -428,7 +428,6 @@ defmodule Logflare.Logs.SearchOperationsTest do assert [{%DateTime{}, _}, {%DateTime{}, _}] = params end - test "aggregate query uses filters and timestamp", %{base_so: base_so} do min = ~U[2026-01-29 04:13:48.748909Z] max = ~U[2026-01-29 06:13:48.748909Z] From ba4b3396e1aa293cc0f3b4a71de7c21a5b18d98b Mon Sep 17 00:00:00 2001 From: Matt Stubbs Date: Thu, 9 Apr 2026 15:31:39 +1000 Subject: [PATCH 44/44] fix: bad merge --- test/logflare/logs/search_operations_test.exs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/test/logflare/logs/search_operations_test.exs b/test/logflare/logs/search_operations_test.exs index 2dbdbee2c..636adea94 100644 --- a/test/logflare/logs/search_operations_test.exs +++ b/test/logflare/logs/search_operations_test.exs @@ -36,27 +36,6 @@ defmodule Logflare.Logs.SearchOperationsTest do [user: insert(:user)] end - @postgres_search_attrs %{ - source: nil, - querystring: "", - query: nil, - chart_data_shape_id: nil, - tailing?: false, - tailing_initial?: nil, - partition_by: :timestamp, - type: :events, - backend_type: :postgres, - lql_rules: [], - lql_ts_filters: [], - lql_meta_and_msg_filters: [] - } - - setup do - insert(:plan) - - [user: insert(:user)] - end - describe "unnesting metadata if present" do setup %{user: user} do source = insert(:source, user: user, bq_table_id: "1")