Packages
kino
0.1.2
0.19.0
0.18.0
0.17.0
0.16.1
0.16.0
0.15.3
0.15.2
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.2
0.13.1
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.0
0.6.2
0.6.1
0.6.0
retired
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Interactive widgets for Livebook
Current section
Files
Jump to
Current section
Files
lib/kino/ets.ex
defmodule Kino.ETS do
@moduledoc """
A widget for interactively viewing an ETS table.
## Examples
tid = :ets.new(:users, [:set, :public])
Kino.ETS.start(tid)
Kino.ETS.start(:elixir_config)
"""
use GenServer, restart: :temporary
defstruct [:pid]
@type t :: %__MODULE__{pid: pid()}
@typedoc false
@type state :: %{
parent_monitor_ref: reference(),
tid: :ets.tid()
}
@doc """
Starts a widget process representing the given ETS table.
Note that private tables cannot be read by an arbitrary process,
so the given table must have either public or protected access.
"""
@spec start(:ets.tid()) :: t()
def start(tid) do
case :ets.info(tid, :protection) do
:private ->
raise ArgumentError,
"the given table must be either public or protected, but a private one was given"
:undefined ->
raise ArgumentError,
"the given table identifier #{inspect(tid)} does not refer to an existing ETS table"
_ ->
:ok
end
parent = self()
opts = [tid: tid, parent: parent]
{:ok, pid} = DynamicSupervisor.start_child(Kino.WidgetSupervisor, {__MODULE__, opts})
%__MODULE__{pid: pid}
end
@doc false
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
@impl true
def init(opts) do
tid = Keyword.fetch!(opts, :tid)
parent = Keyword.fetch!(opts, :parent)
parent_monitor_ref = Process.monitor(parent)
{:ok, %{parent_monitor_ref: parent_monitor_ref, tid: tid}}
end
@impl true
def handle_info({:connect, pid}, state) do
table_name = :ets.info(state.tid, :name)
name = "ETS #{inspect(table_name)}"
columns =
case :ets.match_object(state.tid, :_, 1) do
{[record], _} -> columns_structure_from_record(record)
:"$end_of_table" -> []
end
send(
pid,
{:connect_reply,
%{name: name, columns: columns, features: [:refetch, :pagination, :sorting]}}
)
{:noreply, state}
end
def handle_info({:get_rows, pid, rows_spec}, state) do
records = get_records(state.tid, rows_spec)
rows = Enum.map(records, &record_to_row/1)
total_rows = :ets.info(state.tid, :size)
columns =
case records do
[] -> :initial
[record | _] -> columns_structure_from_record(record)
end
send(pid, {:rows, %{rows: rows, total_rows: total_rows, columns: columns}})
{:noreply, state}
end
def handle_info({:DOWN, ref, :process, _object, _reason}, %{parent_monitor_ref: ref} = state) do
{:stop, :shutdown, state}
end
defp columns_structure_from_record(record) do
record
|> Tuple.to_list()
|> Enum.with_index()
|> Enum.map(fn {_, idx} ->
%{key: idx, label: "#{idx + 1}"}
end)
end
defp get_records(tid, rows_spec) do
query = :ets.table(tid)
order_by_pos = (rows_spec[:order_by] || 0) + 1
order =
case rows_spec.order do
:asc -> :ascending
:desc -> :descending
end
query = :qlc.keysort(order_by_pos, query, order: order)
cursor = :qlc.cursor(query)
if rows_spec.offset > 0 do
:qlc.next_answers(cursor, rows_spec.offset)
end
records = :qlc.next_answers(cursor, rows_spec.limit)
:qlc.delete_cursor(cursor)
records
end
defp record_to_row(record) do
fields =
record
|> Tuple.to_list()
|> Enum.with_index()
|> Map.new(fn {val, idx} -> {idx, inspect(val)} end)
# Note: id is opaque to the client, and we don't need it for now
%{id: nil, fields: fields}
end
end