mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-09 21:51:42 +08:00
Add output type setting to apps (#1905)
This commit is contained in:
parent
b4a6b76824
commit
022f395bce
11 changed files with 48 additions and 26 deletions
|
|
@ -86,7 +86,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp app_settings_metadata(app_settings) do
|
defp app_settings_metadata(app_settings) do
|
||||||
keys = [:slug, :access_type, :show_source]
|
keys = [:slug, :access_type, :show_source, :output_type]
|
||||||
|
|
||||||
put_unless_default(
|
put_unless_default(
|
||||||
%{},
|
%{},
|
||||||
|
|
|
||||||
|
|
@ -410,6 +410,9 @@ defmodule Livebook.LiveMarkdown.Import do
|
||||||
{"access_type", access_type}, attrs when access_type in ["public", "protected"] ->
|
{"access_type", access_type}, attrs when access_type in ["public", "protected"] ->
|
||||||
Map.put(attrs, :access_type, String.to_atom(access_type))
|
Map.put(attrs, :access_type, String.to_atom(access_type))
|
||||||
|
|
||||||
|
{"output_type", output_type}, attrs when output_type in ["all", "rich"] ->
|
||||||
|
Map.put(attrs, :output_type, String.to_atom(output_type))
|
||||||
|
|
||||||
_entry, attrs ->
|
_entry, attrs ->
|
||||||
attrs
|
attrs
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ defmodule Livebook.Notebook.AppSettings do
|
||||||
slug: String.t() | nil,
|
slug: String.t() | nil,
|
||||||
access_type: access_type(),
|
access_type: access_type(),
|
||||||
password: String.t() | nil,
|
password: String.t() | nil,
|
||||||
show_source: boolean()
|
show_source: boolean(),
|
||||||
|
output_type: :all | :rich
|
||||||
}
|
}
|
||||||
|
|
||||||
@type access_type :: :public | :protected
|
@type access_type :: :public | :protected
|
||||||
|
|
@ -20,6 +21,7 @@ defmodule Livebook.Notebook.AppSettings do
|
||||||
field :access_type, Ecto.Enum, values: [:public, :protected]
|
field :access_type, Ecto.Enum, values: [:public, :protected]
|
||||||
field :password, :string
|
field :password, :string
|
||||||
field :show_source, :boolean
|
field :show_source, :boolean
|
||||||
|
field :output_type, Ecto.Enum, values: [:all, :rich]
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -31,7 +33,8 @@ defmodule Livebook.Notebook.AppSettings do
|
||||||
slug: nil,
|
slug: nil,
|
||||||
access_type: :protected,
|
access_type: :protected,
|
||||||
password: generate_password(),
|
password: generate_password(),
|
||||||
show_source: false
|
show_source: false,
|
||||||
|
output_type: :all
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -58,8 +61,8 @@ defmodule Livebook.Notebook.AppSettings do
|
||||||
|
|
||||||
defp changeset(settings, attrs) do
|
defp changeset(settings, attrs) do
|
||||||
settings
|
settings
|
||||||
|> cast(attrs, [:slug, :access_type, :show_source])
|
|> cast(attrs, [:slug, :access_type, :show_source, :output_type])
|
||||||
|> validate_required([:slug, :access_type, :show_source])
|
|> validate_required([:slug, :access_type, :show_source, :output_type])
|
||||||
|> validate_format(:slug, ~r/^[a-zA-Z0-9-]+$/,
|
|> validate_format(:slug, ~r/^[a-zA-Z0-9-]+$/,
|
||||||
message: "slug can only contain alphanumeric characters and dashes"
|
message: "slug can only contain alphanumeric characters and dashes"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,7 @@ defmodule LivebookWeb.FormComponents do
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div phx-feedback-for={@name} class={[@errors != [] && "show-errors"]}>
|
<div phx-feedback-for={@name} class={[@errors != [] && "show-errors"]}>
|
||||||
|
<.label :if={@label} for={@id}><%= @label %></.label>
|
||||||
<div class="flex gap-4 text-gray-600">
|
<div class="flex gap-4 text-gray-600">
|
||||||
<label :for={{value, description} <- @options} class="flex items-center gap-2 cursor-pointer">
|
<label :for={{value, description} <- @options} class="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -119,10 +119,6 @@ defmodule LivebookWeb.AppLive do
|
||||||
input_values={output_view.input_values}
|
input_values={output_view.input_values}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :if={@data_view.output_views == []} class="info-box">
|
|
||||||
This deployed notebook is empty. Deployed apps only render Kino outputs.
|
|
||||||
Ensure you use Kino for interactive visualizations and dynamic content.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 80vh"></div>
|
<div style="height: 80vh"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -241,16 +237,19 @@ defmodule LivebookWeb.AppLive do
|
||||||
for section <- Enum.reverse(notebook.sections),
|
for section <- Enum.reverse(notebook.sections),
|
||||||
cell <- Enum.reverse(section.cells),
|
cell <- Enum.reverse(section.cells),
|
||||||
Cell.evaluable?(cell),
|
Cell.evaluable?(cell),
|
||||||
output <- filter_outputs(cell.outputs),
|
output <- filter_outputs(cell.outputs, notebook.app_settings.output_type),
|
||||||
do: output
|
do: output
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter_outputs(outputs) do
|
defp filter_outputs(outputs, :all), do: outputs
|
||||||
|
defp filter_outputs(outputs, :rich), do: rich_outputs(outputs)
|
||||||
|
|
||||||
|
defp rich_outputs(outputs) do
|
||||||
for output <- outputs, output = filter_output(output), do: output
|
for output <- outputs, output = filter_output(output), do: output
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter_output({idx, output})
|
defp filter_output({idx, output})
|
||||||
when elem(output, 0) in [:plain_text, :markdown, :image, :js, :control],
|
when elem(output, 0) in [:plain_text, :markdown, :image, :js, :control, :input],
|
||||||
do: {idx, output}
|
do: {idx, output}
|
||||||
|
|
||||||
defp filter_output({idx, {:tabs, outputs, metadata}}) do
|
defp filter_output({idx, {:tabs, outputs, metadata}}) do
|
||||||
|
|
@ -265,7 +264,7 @@ defmodule LivebookWeb.AppLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter_output({idx, {:grid, outputs, metadata}}) do
|
defp filter_output({idx, {:grid, outputs, metadata}}) do
|
||||||
outputs = filter_outputs(outputs)
|
outputs = rich_outputs(outputs)
|
||||||
|
|
||||||
if outputs != [] do
|
if outputs != [] do
|
||||||
{idx, {:grid, outputs, metadata}}
|
{idx, {:grid, outputs, metadata}}
|
||||||
|
|
@ -273,7 +272,7 @@ defmodule LivebookWeb.AppLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter_output({idx, {:frame, outputs, metadata}}) do
|
defp filter_output({idx, {:frame, outputs, metadata}}) do
|
||||||
outputs = filter_outputs(outputs)
|
outputs = rich_outputs(outputs)
|
||||||
{idx, {:frame, outputs, metadata}}
|
{idx, {:frame, outputs, metadata}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,11 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<.checkbox_field field={f[:show_source]} label="Show source" />
|
<.checkbox_field field={f[:show_source]} label="Show source" />
|
||||||
|
<.radio_field
|
||||||
|
field={f[:output_type]}
|
||||||
|
label="Output type"
|
||||||
|
options={[{"all", "All"}, {"rich", "Rich only"}]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 flex space-x-2">
|
<div class="mt-6 flex space-x-2">
|
||||||
<button class="button-base button-blue" type="submit" disabled={not @changeset.valid?}>
|
<button class="button-base button-blue" type="submit" disabled={not @changeset.valid?}>
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,6 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
||||||
/>
|
/>
|
||||||
<.radio_field
|
<.radio_field
|
||||||
field={f[:hub_id]}
|
field={f[:hub_id]}
|
||||||
label="Storage"
|
|
||||||
options={[
|
options={[
|
||||||
{"", "only this session"},
|
{"", "only this session"},
|
||||||
{@hub.id, "in #{@hub.hub_emoji} #{@hub.hub_name}"}
|
{@hub.id, "in #{@hub.hub_emoji} #{@hub.hub_name}"}
|
||||||
|
|
|
||||||
|
|
@ -1150,12 +1150,13 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
||||||
Notebook.AppSettings.new()
|
Notebook.AppSettings.new()
|
||||||
| slug: "app",
|
| slug: "app",
|
||||||
access_type: :public,
|
access_type: :public,
|
||||||
show_source: true
|
show_source: true,
|
||||||
|
output_type: :rich
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expected_document = """
|
expected_document = """
|
||||||
<!-- livebook:{"app_settings":{"access_type":"public","show_source":true,"slug":"app"}} -->
|
<!-- livebook:{"app_settings":{"access_type":"public","output_type":"rich","show_source":true,"slug":"app"}} -->
|
||||||
|
|
||||||
# My Notebook
|
# My Notebook
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -750,7 +750,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
||||||
describe "app settings" do
|
describe "app settings" do
|
||||||
test "imports settings" do
|
test "imports settings" do
|
||||||
markdown = """
|
markdown = """
|
||||||
<!-- livebook:{"app_settings":{"access_type":"public","show_source":true,"slug":"app"}} -->
|
<!-- livebook:{"app_settings":{"access_type":"public","output_type":"rich","show_source":true,"slug":"app"}} -->
|
||||||
|
|
||||||
# My Notebook
|
# My Notebook
|
||||||
"""
|
"""
|
||||||
|
|
@ -759,7 +759,12 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
||||||
|
|
||||||
assert %Notebook{
|
assert %Notebook{
|
||||||
name: "My Notebook",
|
name: "My Notebook",
|
||||||
app_settings: %{slug: "app", access_type: :public, show_source: true}
|
app_settings: %{
|
||||||
|
slug: "app",
|
||||||
|
access_type: :public,
|
||||||
|
show_source: true,
|
||||||
|
output_type: :rich
|
||||||
|
}
|
||||||
} = notebook
|
} = notebook
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
defmodule LivebookWeb.AppLiveTest do
|
defmodule LivebookWeb.AppLiveTest do
|
||||||
use LivebookWeb.ConnCase, async: true
|
use LivebookWeb.ConnCase, async: true
|
||||||
|
|
||||||
|
import Livebook.SessionHelpers
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Livebook.Session
|
alias Livebook.Session
|
||||||
|
|
||||||
test "render guidance when Kino output is empty", %{conn: conn} do
|
test "renders only rich output when output type is rich", %{conn: conn} do
|
||||||
session = start_session()
|
session = start_session()
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
|
|
||||||
slug = Livebook.Utils.random_short_id()
|
slug = Livebook.Utils.random_short_id()
|
||||||
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
|
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug, output_type: :rich}
|
||||||
Session.set_app_settings(session.pid, app_settings)
|
Session.set_app_settings(session.pid, app_settings)
|
||||||
|
|
||||||
Session.set_notebook_name(session.pid, "My app #{slug}")
|
section_id = insert_section(session.pid)
|
||||||
|
insert_cell_with_output(session.pid, section_id, {:stdout, "Printed output"})
|
||||||
|
insert_cell_with_output(session.pid, section_id, {:plain_text, "Custom text"})
|
||||||
|
|
||||||
Session.deploy_app(session.pid)
|
Session.deploy_app(session.pid)
|
||||||
|
|
||||||
assert_receive {:operation, {:add_app, _, _, _}}
|
assert_receive {:operation, {:add_app, _, _, _}}
|
||||||
|
|
@ -22,8 +26,8 @@ defmodule LivebookWeb.AppLiveTest do
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, ~p"/apps/#{slug}")
|
{:ok, view, _} = live(conn, ~p"/apps/#{slug}")
|
||||||
|
|
||||||
assert render(view) =~
|
refute render(view) =~ "Printed output"
|
||||||
"This deployed notebook is empty. Deployed apps only render Kino outputs."
|
assert render(view) =~ "Custom text"
|
||||||
end
|
end
|
||||||
|
|
||||||
defp start_session() do
|
defp start_session() do
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,15 @@ defmodule Livebook.SessionHelpers do
|
||||||
|
|
||||||
def insert_section(session_pid) do
|
def insert_section(session_pid) do
|
||||||
Session.insert_section(session_pid, 0)
|
Session.insert_section(session_pid, 0)
|
||||||
%{notebook: %{sections: [section]}} = Session.get_data(session_pid)
|
%{notebook: %{sections: [section | _]}} = Session.get_data(session_pid)
|
||||||
section.id
|
section.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_text_cell(session_pid, section_id, type, content \\ " ") do
|
def insert_text_cell(session_pid, section_id, type, content \\ " ") do
|
||||||
Session.insert_cell(session_pid, section_id, 0, type, %{source: content})
|
Session.insert_cell(session_pid, section_id, 0, type, %{source: content})
|
||||||
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_pid)
|
data = Session.get_data(session_pid)
|
||||||
|
{:ok, section} = Livebook.Notebook.fetch_section(data.notebook, section_id)
|
||||||
|
cell = hd(section.cells)
|
||||||
cell.id
|
cell.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue