mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-12-09 05:05:55 +08:00
Remove old output formats (#876)
* Remove old outputs * Remove other occurrences
This commit is contained in:
parent
fa67c3c567
commit
c57e5448b7
22 changed files with 94 additions and 1914 deletions
|
|
@ -19,7 +19,6 @@ import ScrollOnUpdate from "./scroll_on_update";
|
|||
import VirtualizedLines from "./virtualized_lines";
|
||||
import UserForm from "./user_form";
|
||||
import EditorSettings from "./editor_settings";
|
||||
import VegaLite from "./vega_lite";
|
||||
import Timer from "./timer";
|
||||
import MarkdownRenderer from "./markdown_renderer";
|
||||
import Highlight from "./highlight";
|
||||
|
|
@ -40,7 +39,6 @@ const hooks = {
|
|||
VirtualizedLines,
|
||||
UserForm,
|
||||
EditorSettings,
|
||||
VegaLite,
|
||||
Timer,
|
||||
MarkdownRenderer,
|
||||
Highlight,
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
import { getAttributeOrThrow } from "../lib/attribute";
|
||||
import { throttle } from "../lib/utils";
|
||||
|
||||
/**
|
||||
* Dynamically imports the vega-related modules.
|
||||
*/
|
||||
function importVega() {
|
||||
return import(
|
||||
/* webpackChunkName: "vega" */
|
||||
"./vega"
|
||||
);
|
||||
}
|
||||
|
||||
// See https://github.com/vega/vega-lite/blob/b61b13c2cbd4ecde0448544aff6cdaea721fd22a/src/compile/data/assemble.ts#L228-L231
|
||||
const DEFAULT_DATASET_NAME = "source_0";
|
||||
|
||||
/**
|
||||
* A hook used to render graphics according to the given
|
||||
* Vega-Lite specification.
|
||||
*
|
||||
* The hook expects a `vega_lite:<id>:init` event with `{ spec }` payload,
|
||||
* where `spec` is the graphic definition as an object.
|
||||
*
|
||||
* Later `vega_lite:<id>:push` events may be sent with `{ data, dataset, window }` payload,
|
||||
* to dynamically update the underlying data.
|
||||
*
|
||||
* Configuration:
|
||||
*
|
||||
* * `data-id` - plot id
|
||||
*
|
||||
*/
|
||||
const VegaLite = {
|
||||
mounted() {
|
||||
this.props = getProps(this);
|
||||
this.state = {
|
||||
container: null,
|
||||
viewPromise: null,
|
||||
};
|
||||
|
||||
this.state.container = document.createElement("div");
|
||||
this.el.appendChild(this.state.container);
|
||||
|
||||
this.handleEvent(`vega_lite:${this.props.id}:init`, ({ spec }) => {
|
||||
if (!spec.data) {
|
||||
spec.data = { values: [] };
|
||||
}
|
||||
|
||||
this.state.viewPromise = importVega()
|
||||
.then(({ vegaEmbed }) => {
|
||||
return vegaEmbed(this.state.container, spec, {});
|
||||
})
|
||||
.then((result) => result.view)
|
||||
.catch((error) => {
|
||||
const message = `Failed to render the given Vega-Lite specification, got the following error:\n\n ${error.message}\n\nMake sure to check for typos.`;
|
||||
|
||||
this.state.container.innerHTML = `
|
||||
<div class="text-red-600 whitespace-pre-wrap">${message}</div>
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
const throttledResize = throttle((view) => view.resize(), 1_000);
|
||||
|
||||
this.handleEvent(
|
||||
`vega_lite:${this.props.id}:push`,
|
||||
({ data, dataset, window }) => {
|
||||
dataset = dataset || DEFAULT_DATASET_NAME;
|
||||
|
||||
this.state.viewPromise.then((view) => {
|
||||
const currentData = view.data(dataset);
|
||||
buildChangeset(currentData, data, window).then((changeset) => {
|
||||
// Schedule resize after the run finishes
|
||||
throttledResize(view);
|
||||
view.change(dataset, changeset).run();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.props = getProps(this);
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
if (this.state.viewPromise) {
|
||||
this.state.viewPromise.then((view) => view.finalize());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getProps(hook) {
|
||||
return {
|
||||
id: getAttributeOrThrow(hook.el, "data-id"),
|
||||
};
|
||||
}
|
||||
|
||||
function buildChangeset(currentData, newData, window) {
|
||||
return importVega().then(({ vega }) => {
|
||||
if (window === 0) {
|
||||
return vega.changeset().remove(currentData);
|
||||
} else if (window) {
|
||||
const toInsert = newData.slice(-window);
|
||||
const freeSpace = Math.max(window - toInsert.length, 0);
|
||||
const toRemove = currentData.slice(0, -freeSpace);
|
||||
|
||||
return vega.changeset().remove(toRemove).insert(toInsert);
|
||||
} else {
|
||||
return vega.changeset().insert(newData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default VegaLite;
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import * as vega from "vega";
|
||||
import vegaEmbed from "vega-embed";
|
||||
|
||||
export { vega, vegaEmbed };
|
||||
1323
assets/package-lock.json
generated
1323
assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"deploy": "NODE_ENV=production webpack --mode production",
|
||||
"watch": "webpack --mode development --watch",
|
||||
|
|
@ -38,10 +37,7 @@
|
|||
"topbar": "^1.0.1",
|
||||
"unified": "^10.1.0",
|
||||
"unist-util-remove-position": "^4.0.1",
|
||||
"unist-util-visit": "^4.0.0",
|
||||
"vega": "^5.20.2",
|
||||
"vega-embed": "^6.18.1",
|
||||
"vega-lite": "^5.1.0"
|
||||
"unist-util-visit": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.0",
|
||||
|
|
|
|||
|
|
@ -170,10 +170,6 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
[delimiter, "output\n", text, "\n", delimiter]
|
||||
end
|
||||
|
||||
defp render_output({:vega_lite_static, spec}, _ctx) do
|
||||
["```", "vega-lite\n", Jason.encode!(spec), "\n", "```"]
|
||||
end
|
||||
|
||||
defp render_output(
|
||||
{:js, %{export: %{info_string: info_string, key: key}, ref: ref}},
|
||||
ctx
|
||||
|
|
|
|||
|
|
@ -187,16 +187,6 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
take_outputs(ast, [{:text, output} | outputs])
|
||||
end
|
||||
|
||||
defp take_outputs(
|
||||
[{"pre", _, [{"code", [{"class", "vega-lite"}], [output], %{}}], %{}} | ast],
|
||||
outputs
|
||||
) do
|
||||
case Jason.decode(output) do
|
||||
{:ok, spec} -> take_outputs(ast, [{:vega_lite_static, spec} | outputs])
|
||||
_ -> take_outputs(ast, outputs)
|
||||
end
|
||||
end
|
||||
|
||||
defp take_outputs(ast, outputs), do: {outputs, ast}
|
||||
|
||||
# Builds a notebook from the list of elements obtained in the previous step.
|
||||
|
|
|
|||
|
|
@ -34,16 +34,8 @@ defmodule Livebook.Notebook.Cell.Elixir do
|
|||
| {:markdown, binary()}
|
||||
# A raw image in the given format
|
||||
| {:image, content :: binary(), mime_type :: binary()}
|
||||
# Vega-Lite graphic
|
||||
| {:vega_lite_static, spec :: map()}
|
||||
# Vega-Lite graphic with dynamic data
|
||||
| {:vega_lite_dynamic, widget_process :: pid()}
|
||||
# JavaScript powered output
|
||||
| {:js, info :: map()}
|
||||
# Interactive data table
|
||||
| {:table_dynamic, widget_process :: pid()}
|
||||
# Dynamic wrapper for static output
|
||||
| {:frame_dynamic, widget_process :: pid()}
|
||||
# Outputs placeholder
|
||||
| {:frame, outputs :: list(output()), info :: map()}
|
||||
# An input field
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ install the [Kino](https://github.com/livebook-dev/kino) library:
|
|||
```elixir
|
||||
Mix.install(
|
||||
[
|
||||
{:kino, "~> 0.4.1"}
|
||||
{:kino, github: "livebook-dev/kino"}
|
||||
],
|
||||
consolidate_protocols: false
|
||||
)
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ instance, otherwise the command below will fail.
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.4.1"}
|
||||
{:kino, github: "livebook-dev/kino"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ directly, but it is required to render VegaLite:
|
|||
```elixir
|
||||
Mix.install([
|
||||
{:vega_lite, "~> 0.1.2"},
|
||||
{:kino, "~> 0.4.1"}
|
||||
{:kino, github: "livebook-dev/kino"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ and interact with them.
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.4.1"},
|
||||
{:kino, github: "livebook-dev/kino"},
|
||||
{:vega_lite, "~> 0.1.2"}
|
||||
])
|
||||
```
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ the visualization and interactions.
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.4.1"}
|
||||
{:kino, github: "livebook-dev/kino"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ so let's add `:vega_lite` and `:kino` for that.
|
|||
```elixir
|
||||
Mix.install([
|
||||
{:vega_lite, "~> 0.1.2"},
|
||||
{:kino, "~> 0.4.1"}
|
||||
{:kino, github: "livebook-dev/kino"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
defmodule LivebookWeb.Output do
|
||||
use Phoenix.Component
|
||||
|
||||
alias LivebookWeb.Output
|
||||
|
||||
@doc """
|
||||
Renders a list of cell outputs.
|
||||
"""
|
||||
|
|
@ -34,68 +36,31 @@ defmodule LivebookWeb.Output do
|
|||
|
||||
defp render_output({:stdout, text}, %{id: id}) do
|
||||
text = if(text == :__pruned__, do: nil, else: text)
|
||||
live_component(LivebookWeb.Output.StdoutComponent, id: id, text: text, follow: true)
|
||||
live_component(Output.StdoutComponent, id: id, text: text, follow: true)
|
||||
end
|
||||
|
||||
defp render_output({:text, text}, %{id: id}) do
|
||||
assigns = %{id: id, text: text}
|
||||
|
||||
~H"""
|
||||
<LivebookWeb.Output.TextComponent.render id={@id} content={@text} follow={false} />
|
||||
<Output.TextComponent.render id={@id} content={@text} follow={false} />
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output({:markdown, markdown}, %{id: id}) do
|
||||
live_component(LivebookWeb.Output.MarkdownComponent, id: id, content: markdown)
|
||||
live_component(Output.MarkdownComponent, id: id, content: markdown)
|
||||
end
|
||||
|
||||
defp render_output({:image, content, mime_type}, %{id: id}) do
|
||||
assigns = %{id: id, content: content, mime_type: mime_type}
|
||||
|
||||
~H"""
|
||||
<LivebookWeb.Output.ImageComponent.render content={@content} mime_type={@mime_type} />
|
||||
<Output.ImageComponent.render content={@content} mime_type={@mime_type} />
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output({:vega_lite_static, spec}, %{id: id}) do
|
||||
live_component(LivebookWeb.Output.VegaLiteStaticComponent, id: id, spec: spec)
|
||||
end
|
||||
|
||||
defp render_output({:vega_lite_dynamic, pid}, %{id: id, socket: socket}) do
|
||||
live_render(socket, LivebookWeb.Output.VegaLiteDynamicLive,
|
||||
id: id,
|
||||
session: %{"id" => id, "pid" => pid}
|
||||
)
|
||||
end
|
||||
|
||||
defp render_output({:js, info}, %{id: id, session_id: session_id}) do
|
||||
live_component(LivebookWeb.Output.JSComponent, id: id, info: info, session_id: session_id)
|
||||
end
|
||||
|
||||
defp render_output({:table_dynamic, pid}, %{id: id, socket: socket}) do
|
||||
live_render(socket, LivebookWeb.Output.TableDynamicLive,
|
||||
id: id,
|
||||
session: %{"id" => id, "pid" => pid}
|
||||
)
|
||||
end
|
||||
|
||||
defp render_output({:frame_dynamic, pid}, %{
|
||||
id: id,
|
||||
socket: socket,
|
||||
session_id: session_id,
|
||||
input_values: input_values,
|
||||
cell_validity_status: cell_validity_status
|
||||
}) do
|
||||
live_render(socket, LivebookWeb.Output.FrameDynamicLive,
|
||||
id: id,
|
||||
session: %{
|
||||
"id" => id,
|
||||
"pid" => pid,
|
||||
"session_id" => session_id,
|
||||
"input_values" => input_values,
|
||||
"cell_validity_status" => cell_validity_status
|
||||
}
|
||||
)
|
||||
live_component(Output.JSComponent, id: id, info: info, session_id: session_id)
|
||||
end
|
||||
|
||||
defp render_output({:frame, outputs, _info}, %{
|
||||
|
|
@ -103,7 +68,7 @@ defmodule LivebookWeb.Output do
|
|||
input_values: input_values,
|
||||
session_id: session_id
|
||||
}) do
|
||||
live_component(LivebookWeb.Output.FrameComponent,
|
||||
live_component(Output.FrameComponent,
|
||||
id: id,
|
||||
outputs: outputs,
|
||||
session_id: session_id,
|
||||
|
|
@ -112,19 +77,11 @@ defmodule LivebookWeb.Output do
|
|||
end
|
||||
|
||||
defp render_output({:input, attrs}, %{id: id, input_values: input_values}) do
|
||||
live_component(LivebookWeb.Output.InputComponent,
|
||||
id: id,
|
||||
attrs: attrs,
|
||||
input_values: input_values
|
||||
)
|
||||
live_component(Output.InputComponent, id: id, attrs: attrs, input_values: input_values)
|
||||
end
|
||||
|
||||
defp render_output({:control, attrs}, %{id: id, input_values: input_values}) do
|
||||
live_component(LivebookWeb.Output.ControlComponent,
|
||||
id: id,
|
||||
attrs: attrs,
|
||||
input_values: input_values
|
||||
)
|
||||
live_component(Output.ControlComponent, id: id, attrs: attrs, input_values: input_values)
|
||||
end
|
||||
|
||||
defp render_output({:error, formatted, :runtime_restart_required}, %{
|
||||
|
|
@ -158,6 +115,20 @@ defmodule LivebookWeb.Output do
|
|||
render_error_message_output(formatted)
|
||||
end
|
||||
|
||||
# TODO: remove on Livebook v0.7
|
||||
defp render_output(output, %{})
|
||||
when elem(output, 0) in [
|
||||
:vega_lite_static,
|
||||
:vega_lite_dynamic,
|
||||
:table_dynamic,
|
||||
:frame_dynamic
|
||||
] do
|
||||
render_error_message_output("""
|
||||
Legacy output format: #{inspect(output)}. Please update Kino to
|
||||
the latest version.
|
||||
""")
|
||||
end
|
||||
|
||||
defp render_output(output, %{}) do
|
||||
render_error_message_output("""
|
||||
Unknown output format: #{inspect(output)}. If you're using Kino,
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
defmodule LivebookWeb.Output.FrameDynamicLive do
|
||||
use LivebookWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(
|
||||
_params,
|
||||
%{
|
||||
"pid" => pid,
|
||||
"id" => id,
|
||||
"session_id" => session_id,
|
||||
"input_values" => input_values,
|
||||
"cell_validity_status" => cell_validity_status
|
||||
},
|
||||
socket
|
||||
) do
|
||||
if connected?(socket) do
|
||||
send(pid, {:connect, self()})
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
id: id,
|
||||
output: nil,
|
||||
session_id: session_id,
|
||||
input_values: input_values,
|
||||
cell_validity_status: cell_validity_status
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<%= if @output do %>
|
||||
<LivebookWeb.Output.outputs
|
||||
outputs={[{"#{@id}-output", @output}]}
|
||||
socket={@socket}
|
||||
session_id={@session_id}
|
||||
runtime={nil}
|
||||
input_values={@input_values}
|
||||
cell_validity_status={@cell_validity_status} />
|
||||
<% else %>
|
||||
<div class="text-gray-300">
|
||||
Empty output frame
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:connect_reply, %{output: output}}, socket) do
|
||||
{:noreply, assign(socket, output: output)}
|
||||
end
|
||||
|
||||
def handle_info({:render, %{output: output}}, socket) do
|
||||
{:noreply, assign(socket, output: output)}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
defmodule LivebookWeb.Output.TableDynamicLive do
|
||||
use LivebookWeb, :live_view
|
||||
|
||||
@limit 10
|
||||
@loading_delay_ms 100
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"pid" => pid, "id" => id}, socket) do
|
||||
if connected?(socket) do
|
||||
send(pid, {:connect, self()})
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
id: id,
|
||||
pid: pid,
|
||||
loading: true,
|
||||
show_loading_timer: nil,
|
||||
# Data specification
|
||||
page: 1,
|
||||
limit: @limit,
|
||||
order_by: nil,
|
||||
order: :asc,
|
||||
# Fetched data
|
||||
name: "Table",
|
||||
features: [],
|
||||
columns: [],
|
||||
rows: [],
|
||||
total_rows: 0
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(%{loading: true} = assigns) do
|
||||
~H"""
|
||||
<div class="max-w-2xl w-full animate-pulse">
|
||||
<div class="flex-1 space-y-4">
|
||||
<div class="h-4 bg-gray-200 rounded-lg w-3/4"></div>
|
||||
<div class="h-4 bg-gray-200 rounded-lg"></div>
|
||||
<div class="h-4 bg-gray-200 rounded-lg w-5/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mb-4 flex items-center space-x-3">
|
||||
<h3 class="font-semibold text-gray-800">
|
||||
<%= @name %>
|
||||
</h3>
|
||||
<div class="grow"></div>
|
||||
<!-- Actions -->
|
||||
<div class="flex space-x-2">
|
||||
<%= if :refetch in @features do %>
|
||||
<span class="tooltip left" data-tooltip="Refetch">
|
||||
<button class="icon-button" aria-label="refresh" phx-click="refetch">
|
||||
<.remix_icon icon="refresh-line" class="text-xl" />
|
||||
</button>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<%= if :pagination in @features and @total_rows > 0 do %>
|
||||
<div class="flex space-x-2">
|
||||
<button class="flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300"
|
||||
phx-click="prev"
|
||||
disabled={@page == 1}>
|
||||
<.remix_icon icon="arrow-left-s-line" class="text-xl" />
|
||||
<span>Prev</span>
|
||||
</button>
|
||||
<div class="flex items-center px-3 py-1 rounded-lg border border-gray-300 font-medium text-sm text-gray-400">
|
||||
<span><%= @page %> of <%= max_page(@total_rows, @limit) %></span>
|
||||
</div>
|
||||
<button class="flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300"
|
||||
phx-click="next"
|
||||
disabled={@page == max_page(@total_rows, @limit)}>
|
||||
<span>Next</span>
|
||||
<.remix_icon icon="arrow-right-s-line" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if @columns == [] do %>
|
||||
<!-- In case we don't have information about table structure yet -->
|
||||
<p class="text-gray-700">
|
||||
No data
|
||||
</p>
|
||||
<% else %>
|
||||
<!-- Data table -->
|
||||
<div class="shadow-xl-center rounded-lg max-w-full overflow-y-auto tiny-scrollbar">
|
||||
<table class="w-full">
|
||||
<thead class="text-left">
|
||||
<tr class="border-b border-gray-200 whitespace-nowrap">
|
||||
<%= for {column, idx} <- Enum.with_index(@columns) do %>
|
||||
<th class={"py-3 px-6 text-gray-700 font-semibold #{if(:sorting in @features, do: "cursor-pointer", else: "pointer-events-none")}"}
|
||||
phx-click="column_click"
|
||||
phx-value-column_idx={idx}>
|
||||
<div class="flex items-center space-x-1">
|
||||
<span><%= column.label %></span>
|
||||
<span class={unless(@order_by == column.key, do: "invisible")}>
|
||||
<.remix_icon icon={order_icon(@order)} class="text-xl align-middle leading-none" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-gray-500">
|
||||
<%= for row <- @rows do %>
|
||||
<tr class="border-b border-gray-200 last:border-b-0 hover:bg-gray-50 whitespace-nowrap">
|
||||
<%= for column <- @columns do %>
|
||||
<td class="py-3 px-6">
|
||||
<%= to_string(row.fields[column.key]) %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
defp order_icon(:asc), do: "arrow-up-s-line"
|
||||
defp order_icon(:desc), do: "arrow-down-s-line"
|
||||
|
||||
defp max_page(total_rows, limit) do
|
||||
ceil(total_rows / limit)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("refetch", %{}, socket) do
|
||||
{:noreply, request_rows(socket)}
|
||||
end
|
||||
|
||||
def handle_event("prev", %{}, socket) do
|
||||
{:noreply, assign(socket, :page, socket.assigns.page - 1) |> request_rows()}
|
||||
end
|
||||
|
||||
def handle_event("next", %{}, socket) do
|
||||
{:noreply, assign(socket, :page, socket.assigns.page + 1) |> request_rows()}
|
||||
end
|
||||
|
||||
def handle_event("column_click", %{"column_idx" => idx}, socket) do
|
||||
idx = String.to_integer(idx)
|
||||
%{key: key} = Enum.at(socket.assigns.columns, idx)
|
||||
|
||||
{order_by, order} =
|
||||
case {socket.assigns.order_by, socket.assigns.order} do
|
||||
{^key, :asc} -> {key, :desc}
|
||||
{^key, :desc} -> {nil, :asc}
|
||||
_ -> {key, :asc}
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, order_by: order_by, order: order) |> request_rows()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:connect_reply, %{name: name, columns: columns, features: features}}, socket) do
|
||||
{:noreply, assign(socket, name: name, columns: columns, features: features) |> request_rows()}
|
||||
end
|
||||
|
||||
def handle_info({:rows, %{rows: rows, total_rows: total_rows, columns: columns}}, socket) do
|
||||
columns =
|
||||
case columns do
|
||||
:initial -> socket.assigns.columns
|
||||
columns when is_list(columns) -> columns
|
||||
end
|
||||
|
||||
if socket.assigns.show_loading_timer do
|
||||
Process.cancel_timer(socket.assigns.show_loading_timer)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
loading: false,
|
||||
show_loading_timer: nil,
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
total_rows: total_rows
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info(:show_loading, socket) do
|
||||
{:noreply, assign(socket, loading: true, show_loading_timer: nil)}
|
||||
end
|
||||
|
||||
defp request_rows(socket) do
|
||||
rows_spec = %{
|
||||
offset: (socket.assigns.page - 1) * socket.assigns.limit,
|
||||
limit: socket.assigns.limit,
|
||||
order_by: socket.assigns.order_by,
|
||||
order: socket.assigns.order
|
||||
}
|
||||
|
||||
send(socket.assigns.pid, {:get_rows, self(), rows_spec})
|
||||
|
||||
show_loading_timer = Process.send_after(self(), :show_loading, @loading_delay_ms)
|
||||
assign(socket, show_loading_timer: show_loading_timer)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
defmodule LivebookWeb.Output.VegaLiteDynamicLive do
|
||||
use LivebookWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"pid" => pid, "id" => id}, socket) do
|
||||
if connected?(socket) do
|
||||
send(pid, {:connect, self()})
|
||||
end
|
||||
|
||||
{:ok, assign(socket, id: id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={"vega-lite-#{@id}"} phx-hook="VegaLite" phx-update="ignore" data-id={@id}>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:connect_reply, %{spec: spec}}, socket) do
|
||||
{:noreply, push_event(socket, "vega_lite:#{socket.assigns.id}:init", %{"spec" => spec})}
|
||||
end
|
||||
|
||||
def handle_info({:push, %{data: data, dataset: dataset, window: window}}, socket) do
|
||||
{:noreply,
|
||||
push_event(socket, "vega_lite:#{socket.assigns.id}:push", %{
|
||||
"data" => data,
|
||||
"dataset" => dataset,
|
||||
"window" => window
|
||||
})}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
defmodule LivebookWeb.Output.VegaLiteStaticComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, id: assigns.id)
|
||||
{:ok, push_event(socket, "vega_lite:#{socket.assigns.id}:init", %{"spec" => assigns.spec})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={"vega-lite-#{@id}"} phx-hook="VegaLite" phx-update="ignore" data-id={@id}>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -531,12 +531,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
IO.puts("hey")\
|
||||
""",
|
||||
outputs: [
|
||||
{0, {:stdout, "hey"}},
|
||||
{1,
|
||||
{:vega_lite_static,
|
||||
%{
|
||||
"$schema" => "https://vega.github.io/schema/vega-lite/v5.json"
|
||||
}}}
|
||||
{0, {:stdout, "hey"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -657,7 +652,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
| source: """
|
||||
IO.puts("hey")\
|
||||
""",
|
||||
outputs: [{0, {:table_dynamic, self()}}]
|
||||
outputs: [{0, {:markdown, "some **Markdown**"}}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -621,107 +621,6 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
assert %Notebook{name: "My Notebook", autosave_interval_s: 10} = notebook
|
||||
end
|
||||
|
||||
test "imports notebook with valid vega-lite output" do
|
||||
markdown = """
|
||||
# My Notebook
|
||||
|
||||
## Section 1
|
||||
|
||||
```elixir
|
||||
Vl.new(width: 500, height: 200)
|
||||
|> Vl.data_from_series(in: [1, 2, 3, 4, 5], out: [1, 2, 3, 4, 5])
|
||||
|> Vl.mark(:line)
|
||||
|> Vl.encode_field(:x, "in", type: :quantitative)
|
||||
|> Vl.encode_field(:y, "out", type: :quantitative)
|
||||
```
|
||||
|
||||
```vega-lite
|
||||
{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"in":1,"out":1},{"in":2,"out":2},{"in":3,"out":3},{"in":4,"out":4},{"in":5,"out":5}]},"encoding":{"x":{"field":"in","type":"quantitative"},"y":{"field":"out","type":"quantitative"}},"height":200,"mark":"line","width":500}
|
||||
```
|
||||
"""
|
||||
|
||||
{notebook, []} = Import.notebook_from_markdown(markdown)
|
||||
|
||||
assert %Notebook{
|
||||
name: "My Notebook",
|
||||
sections: [
|
||||
%Notebook.Section{
|
||||
name: "Section 1",
|
||||
cells: [
|
||||
%Cell.Elixir{
|
||||
source: """
|
||||
Vl.new(width: 500, height: 200)
|
||||
|> Vl.data_from_series(in: [1, 2, 3, 4, 5], out: [1, 2, 3, 4, 5])
|
||||
|> Vl.mark(:line)
|
||||
|> Vl.encode_field(:x, \"in\", type: :quantitative)
|
||||
|> Vl.encode_field(:y, \"out\", type: :quantitative)\
|
||||
""",
|
||||
outputs: [
|
||||
{0,
|
||||
{:vega_lite_static,
|
||||
%{
|
||||
"$schema" => "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data" => %{
|
||||
"values" => [
|
||||
%{"in" => 1, "out" => 1},
|
||||
%{"in" => 2, "out" => 2},
|
||||
%{"in" => 3, "out" => 3},
|
||||
%{"in" => 4, "out" => 4},
|
||||
%{"in" => 5, "out" => 5}
|
||||
]
|
||||
},
|
||||
"encoding" => %{
|
||||
"x" => %{"field" => "in", "type" => "quantitative"},
|
||||
"y" => %{"field" => "out", "type" => "quantitative"}
|
||||
},
|
||||
"height" => 200,
|
||||
"mark" => "line",
|
||||
"width" => 500
|
||||
}}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
output_counter: 1
|
||||
} = notebook
|
||||
end
|
||||
|
||||
test "imports notebook with invalid vega-lite output" do
|
||||
markdown = """
|
||||
# My Notebook
|
||||
|
||||
## Section 1
|
||||
|
||||
```elixir
|
||||
:ok
|
||||
```
|
||||
|
||||
```vega-lite
|
||||
not_a_json
|
||||
```
|
||||
"""
|
||||
|
||||
{notebook, []} = Import.notebook_from_markdown(markdown)
|
||||
|
||||
assert %Notebook{
|
||||
name: "My Notebook",
|
||||
sections: [
|
||||
%Notebook.Section{
|
||||
name: "Section 1",
|
||||
cells: [
|
||||
%Cell.Elixir{
|
||||
source: """
|
||||
:ok\
|
||||
""",
|
||||
outputs: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
} = notebook
|
||||
end
|
||||
|
||||
describe "backward compatibility" do
|
||||
test "warns if the imported notebook includes an input" do
|
||||
markdown = """
|
||||
|
|
|
|||
|
|
@ -277,31 +277,6 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
end
|
||||
|
||||
describe "outputs" do
|
||||
test "dynamic frame output renders output sent from the frame server",
|
||||
%{conn: conn, session: session} do
|
||||
frame_pid =
|
||||
spawn(fn ->
|
||||
output = {:text, "Dynamic output in frame"}
|
||||
|
||||
receive do
|
||||
{:connect, pid} -> send(pid, {:connect_reply, %{output: output}})
|
||||
end
|
||||
end)
|
||||
|
||||
frame_output = {:frame_dynamic, frame_pid}
|
||||
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :elixir)
|
||||
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
send(session.pid, {:evaluation_output, cell_id, frame_output})
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
assert render(view) =~ "Dynamic output in frame"
|
||||
end
|
||||
|
||||
test "stdout output update", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :elixir)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue