defmodule LivebookWeb.SessionLive.Render do use LivebookWeb, :html import LivebookWeb.UserComponents import LivebookWeb.SessionHelpers import Livebook.Utils, only: [format_bytes: 1] alias Livebook.Notebook.Cell def render(assigns) do ~H"""
<.sidebar session={@session} live_action={@live_action} current_user={@current_user} runtime_connected_nodes={@data_view.runtime_connected_nodes} /> <.side_panel app={@app} session={@session} data_view={@data_view} client_id={@client_id} />
<.indicators session_id={@session.id} file={@data_view.file} dirty={@data_view.dirty} persistence_warnings={@data_view.persistence_warnings} autosave_interval_s={@data_view.autosave_interval_s} runtime_status={@data_view.runtime_status} global_status={@data_view.global_status} /> <.notebook_content data_view={@data_view} session={@session} client_id={@client_id} allowed_uri_schemes={@allowed_uri_schemes} saved_hubs={@saved_hubs} starred_files={@starred_files} />
<.current_user_modal current_user={@current_user} /> <.modal :if={@live_action == :runtime_settings} id="runtime-settings-modal" show width={:big} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.RuntimeComponent} id="runtime-settings" session={@session} return_to={@self_path} runtime={@data_view.runtime} runtime_status={@data_view.runtime_status} runtime_connect_info={@data_view.runtime_connect_info} hub={@data_view.hub} hub_secrets={@data_view.hub_secrets} /> <.modal :if={@live_action == :file_settings} id="persistence-modal" show width={:big} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.PersistenceComponent} id="persistence" session={@session} file={@data_view.file} hub={@data_view.hub} context={@action_assigns.context} persist_outputs={@data_view.persist_outputs} autosave_interval_s={@data_view.autosave_interval_s} /> <.modal :if={@live_action == :app_settings} id="app-settings-modal" show width={:medium} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.AppSettingsComponent} id="app-settings" session={@session} settings={@data_view.app_settings} context={@action_assigns.context} deployed_app_slug={@data_view.deployed_app_slug} /> <.modal :if={@live_action == :app_docker} id="app-docker-modal" show width={:large} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.AppDockerComponent} id="app-docker" session={@session} hub={@data_view.hub} file={@data_view.file} app_settings={@data_view.app_settings} secrets={@data_view.secrets} file_entries={@data_view.file_entries} settings={@data_view.app_settings} deployment_group_id={@data_view.deployment_group_id} /> <.modal :if={@live_action == :app_teams} id="app-teams-modal" show width={:big} patch={@self_path}> <%= live_render(@socket, LivebookWeb.SessionLive.AppTeamsLive, id: "app-teams", session: %{ "session_pid" => @session.pid } ) %> <.modal :if={@live_action == :app_teams_hub_info} id="app-teams-hub-info-modal" show width={:big} patch={@self_path} > <.app_teams_hub_info_content any_team_hub?={Enum.any?(@saved_hubs, &(Livebook.Hubs.Provider.type(&1.provider) == "team"))} session={@session} /> <.modal :if={@live_action == :add_file_entry} id="add-file-entry-modal" show width={:big} patch={@self_path} > <.add_file_entry_content session={@session} hub={@data_view.hub} file_entries={@data_view.file_entries} tab={@action_assigns.tab} /> <.modal :if={@live_action == :rename_file_entry} id="rename-file-entry-modal" show width={:big} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.RenameFileEntryComponent} id="rename-file-entry" session={@session} file_entry={@action_assigns.renaming_file_entry} /> <.modal :if={@live_action == :shortcuts} id="shortcuts-modal" show width={:large} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.ShortcutsComponent} id="shortcuts" platform={@platform} /> <.modal :if={@live_action == :cell_settings} id="cell-settings-modal" show width={:medium} patch={@self_path} > <.live_component module={settings_component_for(@action_assigns.cell)} id="cell-settings" session={@session} return_to={@self_path} cell={@action_assigns.cell} /> <.modal :if={@live_action == :insert_image} id="insert-image-modal" show width={:medium} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.InsertImageComponent} id="insert-image" session={@session} return_to={@self_path} insert_image_metadata={@action_assigns.insert_image_metadata} /> <.modal :if={@live_action == :insert_file} id="insert-file-modal" show width={:medium} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.InsertFileComponent} id="insert-file" session={@session} return_to={@self_path} insert_file_metadata={@action_assigns.insert_file_metadata} /> <.modal :if={@live_action == :bin} id="bin-modal" show width={:big} patch={@self_path}> <.live_component module={LivebookWeb.SessionLive.BinComponent} id="bin" session={@session} return_to={@self_path} bin_entries={@data_view.bin_entries} /> <.modal :if={@live_action == :export} id="export-modal" show width={:big} patch={@self_path}> <.live_component module={LivebookWeb.SessionLive.ExportComponent} id="export" session={@session} tab={@action_assigns.tab} any_stale_cell?={@action_assigns.any_stale_cell?} /> <.modal :if={@live_action == :package_search} id="package-search-modal" show width={:medium} patch={@self_path} > <%= live_render(@socket, LivebookWeb.SessionLive.PackageSearchLive, id: "package-search", session: %{ "session_pid" => @session.pid, "runtime" => @data_view.runtime } ) %> <.modal :if={@live_action == :secrets} id="secrets-modal" show width={if(@action_assigns.select_secret_metadata, do: :large, else: :medium)} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.SecretsComponent} id="secrets" session={@session} secrets={@data_view.secrets} hub_secrets={@data_view.hub_secrets} hub={@data_view.hub} select_secret_metadata={@action_assigns.select_secret_metadata} prefill_secret_name={@action_assigns.prefill_secret_name} return_to={@self_path} /> <.modal :if={@live_action == :custom_view_settings} id="custom-view-modal" show width={:medium} patch={@self_path} > <.live_component module={LivebookWeb.SessionLive.CustomViewComponent} id="custom" return_to={@self_path} session={@session} /> """ end defp settings_component_for(%Cell.Code{}), do: LivebookWeb.SessionLive.CodeCellSettingsComponent def sidebar(assigns) do ~H""" """ end def side_panel(assigns) do ~H"""
<.outline_list data_view={@data_view} />
<.clients_list data_view={@data_view} client_id={@client_id} />
<.live_component module={LivebookWeb.SessionLive.FilesListComponent} id="files-list" session={@session} file_entries={@data_view.file_entries} quarantine_file_entry_names={@data_view.quarantine_file_entry_names} />
<.live_component module={LivebookWeb.SessionLive.SecretsListComponent} id="secrets-list" session={@session} secrets={@data_view.secrets} hub_secrets={@data_view.hub_secrets} hub={@data_view.hub} />
<.live_component module={LivebookWeb.SessionLive.AppInfoComponent} id="app-info" session={@session} settings={@data_view.app_settings} app={@app} deployed_app_slug={@data_view.deployed_app_slug} any_session_secrets?={@data_view.any_session_secrets?} hub={@data_view.hub} />
<.runtime_info data_view={@data_view} session={@session} />
""" end defp button_item(assigns) do ~H""" """ end defp link_item(assigns) do assigns = assign_new(assigns, :link_attrs, fn -> [] end) ~H""" <.link patch={@path} class={[ "text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center", @active && "text-gray-50 bg-gray-700" ]} aria-label={@label} {@link_attrs} > <.remix_icon icon={@icon} class="text-2xl" /> """ end defp outline_list(assigns) do ~H"""

Outline

<.section_status status={elem(section_item.status, 0)} cell_id={elem(section_item.status, 1)} />
""" end defp branching_tooltip_attrs(name, parent_name) do direction = if String.length(name) >= 16, do: "left", else: "right" wrapped_name = Livebook.Utils.wrap_line("”" <> parent_name <> "”", 16) label = "Branches from\n#{wrapped_name}" [class: "tooltip #{direction}", "data-tooltip": label] end defp clients_list(assigns) do ~H"""

Users

<%= length(@data_view.clients) %> connected
<%= if client_id == @client_id do %> <.icon_button aria-label="edit profile" phx-click={show_current_user_modal()}> <.remix_icon icon="user-settings-line" /> <% else %> <.icon_button aria-label="follow this user"> <.remix_icon icon="pushpin-line" /> <.icon_button aria-label="unfollow this user"> <.remix_icon icon="pushpin-fill" /> <% end %>
""" end defp runtime_info(assigns) do ~H"""

Runtime

<.icon_button> <.remix_icon icon="question-line" />
<.labeled_text :for={{label, value} <- @data_view.runtime_metadata} label={label} one_line> <%= value %>
<.button :if={@data_view.runtime_status == :disconnected} phx-click="connect_runtime"> <.remix_icon icon="wireless-charging-line" /> Connect <.button :if={@data_view.runtime_status == :connecting} disabled> <.remix_icon icon="wireless-charging-line" /> Connecting... <.button :if={@data_view.runtime_status == :connected} phx-click="reconnect_runtime"> <.remix_icon icon="wireless-charging-line" /> Reconnect <.button color="gray" outlined patch={~p"/sessions/#{@session.id}/settings/runtime"}> Configure <.button :if={@data_view.runtime_status == :connected} color="red" outlined type="button" phx-click="disconnect_runtime" class="col-span-2" > Disconnect
<.message_box kind={:info}>
<.spinner /> Step: <%= @data_view.runtime_connect_info %>
<.memory_usage_info memory_usage={@session.memory_usage} runtime_metadata={@data_view.runtime_metadata} /> <.runtime_connected_nodes_info runtime_connected_nodes={@data_view.runtime_connected_nodes} />
""" end defp memory_usage_info(assigns) do ~H"""
Memory
<.icon_button :if={node = runtime_node(@runtime_metadata)} href={LivebookWeb.HTMLHelpers.live_dashboard_node_path(node)} target="_blank" aria-label="see on dashboard" > <.remix_icon icon="dashboard-2-line" />
<%= if uses_memory?(@memory_usage) do %> <.runtime_memory_info memory_usage={@memory_usage} /> <% else %>

<%= format_bytes(@memory_usage.system.free) %> available out of <%= format_bytes( @memory_usage.system.total ) %>

<% end %>
""" end defp runtime_node(runtime_metadata) do Enum.find_value(runtime_metadata, fn {key, value} -> key == "Node name" && value end) end defp runtime_memory_info(assigns) do assigns = assign(assigns, :runtime_memory, runtime_memory(assigns.memory_usage)) ~H"""
<%= type %> <%= memory.unit %>
Total: <%= format_bytes(@memory_usage.runtime.total) %>
""" end defp memory_color(:atom), do: "bg-blue-500" defp memory_color(:code), do: "bg-yellow-600" defp memory_color(:processes), do: "bg-blue-700" defp memory_color(:binary), do: "bg-green-500" defp memory_color(:ets), do: "bg-red-500" defp memory_color(:other), do: "bg-gray-400" defp runtime_memory(%{runtime: memory}) do memory |> Map.drop([:total, :system]) |> Enum.map(fn {type, bytes} -> {type, %{ unit: format_bytes(bytes), percentage: Float.round(bytes / memory.total * 100, 2), value: bytes }} end) end defp runtime_connected_nodes_info(assigns) do ~H"""
Connected nodes <%= if @runtime_connected_nodes == [] do %>
No connected nodes
<% else %>
<.remix_icon icon="circle-fill" class="mr-2 text-xs text-blue-500" />
<%= node %>
<.icon_button phx-click="runtime_disconnect_node" phx-value-node={node} small> <.remix_icon icon="close-line" />
<% end %>
""" end defp section_status(%{status: :evaluating} = assigns) do ~H""" """ end defp section_status(%{status: :stale} = assigns) do ~H""" """ end defp section_status(assigns), do: ~H"" def session_menu(assigns) do ~H""" <.menu id="session-menu"> <:toggle> <.icon_button aria-label="open notebook menu"> <.remix_icon icon="more-2-fill" /> <.menu_item> <.link patch={~p"/sessions/#{@session.id}/export/livemd"} role="menuitem"> <.remix_icon icon="download-2-line" /> Export <.menu_item> <.menu_item> <.menu_item> <.remix_icon icon="dashboard-2-line" /> See on Dashboard <.menu_item variant={:danger}> """ end defp app_teams_hub_info_content(assigns) do ~H"""

App deployment with Livebook Teams

<%= if @any_team_hub? do %> <.message_box kind={:info}> In order to deploy your app using Livebook Teams, you need to select a Livebook Teams workspace. To change the workspace, use the dropdown right below the notebook title. <.link class="text-blue-600 font-medium" patch={~p"/sessions/#{@session.id}"} phx-click={show_menu(%JS{}, "notebook-hub-menu", animate: true)} > Change workspace <.remix_icon icon="arrow-right-line" /> <% else %> <.message_box kind={:info}> In order to deploy your app using Livebook Teams, you need to create an organization. <.link class="text-blue-600 font-medium" patch={~p"/hub"}> Add organization <.remix_icon icon="arrow-right-line" /> <% end %>
""" end def add_file_entry_content(assigns) do ~H"""

Add file

<.link patch={~p"/sessions/#{@session.id}/add-file/storage"} class={["tab", @tab == "storage" && "active"]} > <.remix_icon icon="file-3-line" class="align-middle" /> From storage <.link patch={~p"/sessions/#{@session.id}/add-file/url"} class={["tab", @tab == "url" && "active"]} > <.remix_icon icon="download-cloud-2-line" class="align-middle" /> From URL <.link patch={~p"/sessions/#{@session.id}/add-file/upload"} class={["tab", @tab == "upload" && "active"]} > <.remix_icon icon="file-upload-line" class="align-middle" /> From upload <.link patch={~p"/sessions/#{@session.id}/add-file/unlisted"} class={["tab", @tab == "unlisted" && "active"]} > <.remix_icon icon="folder-shared-line" class="align-middle" /> From unlisted
<.live_component :if={@tab == "storage"} module={LivebookWeb.SessionLive.AddFileEntryFileComponent} id="add-file-entry-from-file" hub={@hub} session={@session} /> <.live_component :if={@tab == "url"} module={LivebookWeb.SessionLive.AddFileEntryUrlComponent} id="add-file-entry-from-url" hub={@hub} session={@session} /> <.live_component :if={@tab == "upload"} module={LivebookWeb.SessionLive.AddFileEntryUploadComponent} id="add-file-entry-from-upload" hub={@hub} session={@session} /> <.live_component :if={@tab == "unlisted"} module={LivebookWeb.SessionLive.AddFileEntryUnlistedComponent} id="add-file-entry-from-unlisted" hub={@hub} session={@session} file_entries={@file_entries} />
""" end def indicators(assigns) do ~H"""
<.view_indicator /> <.persistence_indicator file={@file} dirty={@dirty} persistence_warnings={@persistence_warnings} autosave_interval_s={@autosave_interval_s} session_id={@session_id} /> <.runtime_indicator runtime_status={@runtime_status} global_status={@global_status} session_id={@session_id} /> <.insert_mode_indicator />
""" end defp view_indicator(assigns) do ~H"""
<.menu id="views-menu" position={:bottom_right} sm_position={:top_right}> <:toggle> <.menu_item> <.menu_item> <.menu_item>
""" end defp persistence_indicator(%{file: nil} = assigns) do ~H""" <.link patch={~p"/sessions/#{@session_id}/settings/file"} class={status_button_classes(:gray)} aria-label="choose a file to save the notebook" > <.remix_icon icon="save-line" /> """ end defp persistence_indicator(%{dirty: false} = assigns) do ~H""" "Notebook saved" warnings -> "Notebook saved with warnings:\n" <> Enum.map_join(warnings, "\n", &("- " <> &1)) end } > <.link patch={~p"/sessions/#{@session_id}/settings/file"} class={status_button_classes(:green)} aria-label="notebook saved, click to open file settings" >
<.remix_icon icon="save-line" /> <.remix_icon :if={@persistence_warnings != []} icon="error-warning-fill" class="text-lg text-red-400 absolute -top-1.5 -right-2" />
""" end defp persistence_indicator(%{autosave_interval_s: nil} = assigns) do ~H""" <.link patch={~p"/sessions/#{@session_id}/settings/file"} class={status_button_classes(:yellow)} aria-label="no autosave configured, click to open file settings" > <.remix_icon icon="save-line" /> """ end defp persistence_indicator(assigns) do ~H""" <.link patch={~p"/sessions/#{@session_id}/settings/file"} class={status_button_classes(:blue)} aria-label="autosave pending, click to open file settings" > <.remix_icon icon="save-line" /> """ end defp runtime_indicator(assigns) do ~H""" <%= if @runtime_status == :disconnected do %> <.link patch={~p"/sessions/#{@session_id}/settings/runtime"} class={status_button_classes(:gray)} aria-label="choose a runtime to run the notebook in" > <.remix_icon icon="loader-3-line" /> <% else %> <.global_status status={elem(@global_status, 0)} cell_id={elem(@global_status, 1)} /> <% end %> """ end defp global_status(%{status: :evaluating} = assigns) do ~H""" """ end defp global_status(%{status: :evaluated} = assigns) do ~H""" """ end defp global_status(%{status: :errored} = assigns) do ~H""" """ end defp global_status(%{status: :stale} = assigns) do ~H""" """ end defp global_status(%{status: :fresh} = assigns) do ~H"""
<.remix_icon icon="loader-3-line" />
""" end defp status_button_classes(color) do [ "text-xl leading-none p-1 flex items-center justify-center rounded-full rounded-full border-2 focus-visible:outline-none", case color do :gray -> "text-gray-400 border-gray-200 hover:bg-gray-100 focus-visible:bg-gray-100" :blue -> "text-blue-500 border-blue-400 hover:bg-blue-50 focus-visible:bg-blue-50" :green -> "text-green-bright-400 border-green-bright-300 hover:bg-green-bright-50 focus-visible:bg-green-bright-50" :yellow -> "text-yellow-bright-300 border-yellow-bright-200 hover:bg-yellow-bright-50 focus-visible:bg-yellow-bright-50" :red -> "text-red-400 border-red-300 hover:bg-red-50 focus-visible:bg-red-50" end ] end defp insert_mode_indicator(assigns) do ~H""" <%!-- Note: this indicator is shown/hidden using CSS based on the current mode --%> ins """ end def notebook_content(assigns) do ~H"""

<%= @data_view.notebook_name %>

<.session_menu session={@session} />
<.menu position={:bottom_left} id="notebook-hub-menu"> <:toggle>
Using <%= @data_view.hub.hub_emoji %> <%= @data_view.hub.hub_name %> <.remix_icon icon="arrow-down-s-line" class="-ml-1" /> workspace
<.menu_item :for={hub <- @saved_hubs}> <.menu_item> <.link navigate={~p"/hub"} aria-label="Add Organization" role="menuitem"> <.remix_icon icon="add-line" class="align-middle mr-1" /> Add Organization
<.star_button file={@data_view.file} starred_files={@starred_files} />
<.live_component module={LivebookWeb.SessionLive.CellComponent} id={@data_view.setup_cell_view.id} session_id={@session.id} session_pid={@session.pid} client_id={@client_id} runtime={@data_view.runtime} installing?={@data_view.installing?} allowed_uri_schemes={@allowed_uri_schemes} cell_view={@data_view.setup_cell_view} />
+ Section
<.live_component :for={{section_view, index} <- Enum.with_index(@data_view.section_views)} module={LivebookWeb.SessionLive.SectionComponent} id={section_view.id} index={index} session_id={@session.id} session_pid={@session.pid} client_id={@client_id} runtime_status={@data_view.runtime_status} smart_cell_definitions={@data_view.smart_cell_definitions} example_snippet_definitions={@data_view.example_snippet_definitions} installing?={@data_view.installing?} allowed_uri_schemes={@allowed_uri_schemes} section_view={section_view} default_language={@data_view.default_language} />
""" end defp star_button(%{file: nil} = assigns) do ~H""" <.icon_button disabled> <.remix_icon icon="star-line" /> """ end defp star_button(assigns) do ~H""" <%= if starred?(@file, @starred_files) do %> <.icon_button phx-click="unstar_notebook"> <.remix_icon icon="star-fill" class="text-yellow-600" /> <% else %> <.icon_button phx-click="star_notebook"> <.remix_icon icon="star-line" /> <% end %> """ end defp starred?(file, starred_files) do Enum.any?(starred_files, &Livebook.FileSystem.File.equal?(&1, file)) end end