Skip to content

Commit

Permalink
Add output type setting to apps (#1905)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonatanklosko authored May 11, 2023
1 parent b4a6b76 commit 022f395
Show file tree
Hide file tree
Showing 11 changed files with 48 additions and 26 deletions.
2 changes: 1 addition & 1 deletion lib/livebook/live_markdown/export.ex
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ defmodule Livebook.LiveMarkdown.Export do
end

defp app_settings_metadata(app_settings) do
keys = [:slug, :access_type, :show_source]
keys = [:slug, :access_type, :show_source, :output_type]

put_unless_default(
%{},
Expand Down
3 changes: 3 additions & 0 deletions lib/livebook/live_markdown/import.ex
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ defmodule Livebook.LiveMarkdown.Import do
{"access_type", access_type}, attrs when access_type in ["public", "protected"] ->
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 ->
attrs
end)
Expand Down
11 changes: 7 additions & 4 deletions lib/livebook/notebook/app_settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ defmodule Livebook.Notebook.AppSettings do
slug: String.t() | nil,
access_type: access_type(),
password: String.t() | nil,
show_source: boolean()
show_source: boolean(),
output_type: :all | :rich
}

@type access_type :: :public | :protected
Expand All @@ -20,6 +21,7 @@ defmodule Livebook.Notebook.AppSettings do
field :access_type, Ecto.Enum, values: [:public, :protected]
field :password, :string
field :show_source, :boolean
field :output_type, Ecto.Enum, values: [:all, :rich]
end

@doc """
Expand All @@ -31,7 +33,8 @@ defmodule Livebook.Notebook.AppSettings do
slug: nil,
access_type: :protected,
password: generate_password(),
show_source: false
show_source: false,
output_type: :all
}
end

Expand All @@ -58,8 +61,8 @@ defmodule Livebook.Notebook.AppSettings do

defp changeset(settings, attrs) do
settings
|> cast(attrs, [:slug, :access_type, :show_source])
|> validate_required([:slug, :access_type, :show_source])
|> cast(attrs, [:slug, :access_type, :show_source, :output_type])
|> validate_required([:slug, :access_type, :show_source, :output_type])
|> validate_format(:slug, ~r/^[a-zA-Z0-9-]+$/,
message: "slug can only contain alphanumeric characters and dashes"
)
Expand Down
1 change: 1 addition & 0 deletions lib/livebook_web/components/form_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ defmodule LivebookWeb.FormComponents do

~H"""
<div phx-feedback-for={@name} class={[@errors != [] && "show-errors"]}>
<.label :if={@label} for={@id}><%= @label %></.label>
<div class="flex gap-4 text-gray-600">
<label :for={{value, description} <- @options} class="flex items-center gap-2 cursor-pointer">
<input
Expand Down
17 changes: 8 additions & 9 deletions lib/livebook_web/live/app_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,6 @@ defmodule LivebookWeb.AppLive do
input_values={output_view.input_values}
/>
</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 style="height: 80vh"></div>
</div>
Expand Down Expand Up @@ -241,16 +237,19 @@ defmodule LivebookWeb.AppLive do
for section <- Enum.reverse(notebook.sections),
cell <- Enum.reverse(section.cells),
Cell.evaluable?(cell),
output <- filter_outputs(cell.outputs),
output <- filter_outputs(cell.outputs, notebook.app_settings.output_type),
do: output
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
end

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}

defp filter_output({idx, {:tabs, outputs, metadata}}) do
Expand All @@ -265,15 +264,15 @@ defmodule LivebookWeb.AppLive do
end

defp filter_output({idx, {:grid, outputs, metadata}}) do
outputs = filter_outputs(outputs)
outputs = rich_outputs(outputs)

if outputs != [] do
{idx, {:grid, outputs, metadata}}
end
end

defp filter_output({idx, {:frame, outputs, metadata}}) do
outputs = filter_outputs(outputs)
outputs = rich_outputs(outputs)
{idx, {:frame, outputs, metadata}}
end

Expand Down
5 changes: 5 additions & 0 deletions lib/livebook_web/live/session_live/app_info_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
<% end %>
</div>
<.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 class="mt-6 flex space-x-2">
<button class="button-base button-blue" type="submit" disabled={not @changeset.valid?}>
Expand Down
1 change: 0 additions & 1 deletion lib/livebook_web/live/session_live/secrets_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
/>
<.radio_field
field={f[:hub_id]}
label="Storage"
options={[
{"", "only this session"},
{@hub.id, "in #{@hub.hub_emoji} #{@hub.hub_name}"}
Expand Down
5 changes: 3 additions & 2 deletions test/livebook/live_markdown/export_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1150,12 +1150,13 @@ defmodule Livebook.LiveMarkdown.ExportTest do
Notebook.AppSettings.new()
| slug: "app",
access_type: :public,
show_source: true
show_source: true,
output_type: :rich
}
}

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
"""
Expand Down
9 changes: 7 additions & 2 deletions test/livebook/live_markdown/import_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
describe "app settings" do
test "imports settings" do
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
"""
Expand All @@ -759,7 +759,12 @@ defmodule Livebook.LiveMarkdown.ImportTest do

assert %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
end

Expand Down
14 changes: 9 additions & 5 deletions test/livebook_web/live/app_live_test.exs
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
defmodule LivebookWeb.AppLiveTest do
use LivebookWeb.ConnCase, async: true

import Livebook.SessionHelpers
import Phoenix.LiveViewTest

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.subscribe(session.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_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)

assert_receive {:operation, {:add_app, _, _, _}}
assert_receive {:operation, {:set_app_registered, _, _, true}}

{:ok, view, _} = live(conn, ~p"/apps/#{slug}")

assert render(view) =~
"This deployed notebook is empty. Deployed apps only render Kino outputs."
refute render(view) =~ "Printed output"
assert render(view) =~ "Custom text"
end

defp start_session() do
Expand Down
6 changes: 4 additions & 2 deletions test/support/session_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ defmodule Livebook.SessionHelpers do

def insert_section(session_pid) do
Session.insert_section(session_pid, 0)
%{notebook: %{sections: [section]}} = Session.get_data(session_pid)
%{notebook: %{sections: [section | _]}} = Session.get_data(session_pid)
section.id
end

def insert_text_cell(session_pid, section_id, type, content \\ " ") do
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
end

Expand Down

0 comments on commit 022f395

Please sign in to comment.