Packages
kino
0.2.3
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.new(tid)
Kino.ETS.new(:elixir_config)
"""
use GenServer, restart: :temporary
alias Kino.Utils.Table
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 new(:ets.tid()) :: t()
def new(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
# TODO: remove in v0.3.0
@deprecated "Use Kino.ETS.new/1 instead"
def start(tid), do: new(tid)
@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], _} -> Table.columns_for_records([record])
:"$end_of_table" -> []
end
send(
pid,
{:connect_reply, %{name: name, columns: columns, features: [:refetch, :pagination]}}
)
{: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
records -> Table.columns_for_records(records)
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 get_records(tid, rows_spec) do
query = :ets.table(tid)
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