mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
Merge branch 'elixir-nx:main' into main
This commit is contained in:
commit
52e248efa1
|
@ -281,11 +281,10 @@ function handleDocumentKeyDown(hook, event) {
|
|||
saveNotebook(hook);
|
||||
} else if (keyBuffer.tryMatch(["d", "d"])) {
|
||||
deleteFocusedCell(hook);
|
||||
} else if (
|
||||
hook.state.focusedCellType === "elixir" &&
|
||||
(keyBuffer.tryMatch(["e", "e"]) || (cmd && key === "Enter"))
|
||||
) {
|
||||
queueFocusedCellEvaluation(hook);
|
||||
} else if (keyBuffer.tryMatch(["e", "e"]) || (cmd && key === "Enter")) {
|
||||
if (hook.state.focusedCellType === "elixir") {
|
||||
queueFocusedCellEvaluation(hook);
|
||||
}
|
||||
} else if (keyBuffer.tryMatch(["e", "a"])) {
|
||||
queueAllCellsEvaluation(hook);
|
||||
} else if (keyBuffer.tryMatch(["e", "s"])) {
|
||||
|
|
|
@ -10,8 +10,8 @@ import { storeUserData } from "../lib/user";
|
|||
const UserForm = {
|
||||
mounted() {
|
||||
this.el.addEventListener("submit", (event) => {
|
||||
const name = this.el.data_name.value;
|
||||
const hex_color = this.el.data_hex_color.value;
|
||||
const name = this.el.user_form_name.value;
|
||||
const hex_color = this.el.user_form_hex_color.value;
|
||||
storeUserData({ name, hex_color });
|
||||
});
|
||||
},
|
||||
|
|
6
assets/package-lock.json
generated
6
assets/package-lock.json
generated
|
@ -46,14 +46,14 @@
|
|||
}
|
||||
},
|
||||
"../deps/phoenix": {
|
||||
"version": "1.5.9",
|
||||
"version": "1.5.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../deps/phoenix_html": {
|
||||
"version": "2.14.3"
|
||||
"version": "2.14.2"
|
||||
},
|
||||
"../deps/phoenix_live_view": {
|
||||
"version": "0.15.7",
|
||||
"version": "0.15.5",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
|
|
|
@ -3,6 +3,7 @@ module.exports = {
|
|||
purge: [
|
||||
'../lib/**/*.ex',
|
||||
'../lib/**/*.leex',
|
||||
'../lib/**/*.heex',
|
||||
'../lib/**/*.eex',
|
||||
'./js/**/*.js'
|
||||
],
|
||||
|
|
|
@ -39,11 +39,12 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
|
||||
defp render_cell(%Cell.Elixir{} = cell) do
|
||||
code = get_elixir_cell_code(cell)
|
||||
delimiter = code_block_delimiter(code)
|
||||
|
||||
"""
|
||||
```elixir
|
||||
#{delimiter}elixir
|
||||
#{code}
|
||||
```\
|
||||
#{delimiter}\
|
||||
"""
|
||||
|> prepend_metadata(cell.metadata)
|
||||
end
|
||||
|
@ -117,12 +118,23 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
|
||||
defp format_code(code) do
|
||||
try do
|
||||
Code.format_string!(code)
|
||||
code
|
||||
|> Code.format_string!()
|
||||
|> IO.iodata_to_binary()
|
||||
rescue
|
||||
_ -> code
|
||||
end
|
||||
end
|
||||
|
||||
defp code_block_delimiter(code) do
|
||||
max_streak =
|
||||
Regex.scan(~r/`{3,}/, code)
|
||||
|> Enum.map(fn [string] -> byte_size(string) end)
|
||||
|> Enum.max(&>=/2, fn -> 2 end)
|
||||
|
||||
String.duplicate("`", max_streak + 1)
|
||||
end
|
||||
|
||||
defp put_truthy(map, entries) do
|
||||
Enum.reduce(entries, map, fn {key, value}, map ->
|
||||
if value do
|
||||
|
|
|
@ -154,7 +154,7 @@ most busy processes!
|
|||
Sometimes you may want to render arbitrary content as rich-text,
|
||||
that's when `Kino.Markdown.new/1` comes into play:
|
||||
|
||||
```elixir
|
||||
````elixir
|
||||
"""
|
||||
# Example
|
||||
|
||||
|
@ -174,7 +174,7 @@ A regular Markdown file.
|
|||
| 2 | Erlang | https://www.erlang.org |
|
||||
"""
|
||||
|> Kino.Markdown.new()
|
||||
```
|
||||
````
|
||||
|
||||
## Kino.render/1
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
defmodule LivebookWeb.Helpers do
|
||||
import Phoenix.LiveView.Helpers
|
||||
import Phoenix.HTML.Tag
|
||||
use Phoenix.Component
|
||||
|
||||
alias LivebookWeb.Router.Helpers, as: Routes
|
||||
|
||||
@doc """
|
||||
|
@ -14,7 +14,7 @@ defmodule LivebookWeb.Helpers do
|
|||
modal_class = Keyword.get(opts, :modal_class)
|
||||
|
||||
modal_opts = [
|
||||
id: :modal,
|
||||
id: "modal",
|
||||
return_to: path,
|
||||
modal_class: modal_class,
|
||||
component: component,
|
||||
|
@ -41,15 +41,6 @@ defmodule LivebookWeb.Helpers do
|
|||
defp mac?(user_agent), do: String.match?(user_agent, ~r/Mac OS X/)
|
||||
defp windows?(user_agent), do: String.match?(user_agent, ~r/Windows/)
|
||||
|
||||
@doc """
|
||||
Returns [Remix](https://remixicon.com) icon tag.
|
||||
"""
|
||||
def remix_icon(name, attrs \\ []) do
|
||||
icon_class = "ri-#{name}"
|
||||
attrs = Keyword.update(attrs, :class, icon_class, fn class -> "#{icon_class} #{class}" end)
|
||||
content_tag(:i, "", attrs)
|
||||
end
|
||||
|
||||
defdelegate ansi_string_to_html(string, opts \\ []), to: LivebookWeb.ANSI
|
||||
|
||||
@doc """
|
||||
|
@ -83,7 +74,7 @@ defmodule LivebookWeb.Helpers do
|
|||
Returns path to specific process dialog within LiveDashboard.
|
||||
"""
|
||||
def live_dashboard_process_path(socket, pid) do
|
||||
pid_str = Phoenix.LiveDashboard.Helpers.encode_pid(pid)
|
||||
pid_str = Phoenix.LiveDashboard.PageBuilder.encode_pid(pid)
|
||||
Routes.live_dashboard_path(socket, :page, node(), "processes", info: pid_str)
|
||||
end
|
||||
|
||||
|
@ -116,15 +107,40 @@ defmodule LivebookWeb.Helpers do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Renders a list of select input options with the given one selected.
|
||||
"""
|
||||
def render_select(name, options, selected) do
|
||||
assigns = %{name: name, options: options, selected: selected}
|
||||
Renders [Remix](https://remixicon.com) icon.
|
||||
|
||||
~L"""
|
||||
<select class="input" name=<%= @name %>>
|
||||
<%= for {value, label} <- options do %>
|
||||
<%= tag :option, value: value, selected: value == selected %>
|
||||
## Examples
|
||||
|
||||
<.remix_icon icon="cpu-line" />
|
||||
|
||||
<.remix_icon icon="cpu-line" class="align-middle mr-1" />
|
||||
"""
|
||||
def remix_icon(assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign_new(:class, fn -> "" end)
|
||||
|> assign(:attrs, assigns_to_attributes(assigns, [:icon, :class]))
|
||||
|
||||
~H"""
|
||||
<i class={"ri-#{@icon} #{@class}"} {@attrs}></i>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a list of select input options with the given one selected.
|
||||
|
||||
## Examples
|
||||
|
||||
<.select
|
||||
name="language"
|
||||
selected={@language}
|
||||
options={[en: "English", pl: "Polski", fr: "Français"]} />
|
||||
"""
|
||||
def select(assigns) do
|
||||
~H"""
|
||||
<select class="input" name={@name}>
|
||||
<%= for {value, label} <- @options do %>
|
||||
<option value={value} selected={value == @selected}>
|
||||
<%= label %>
|
||||
</option>
|
||||
<% end %>
|
||||
|
@ -134,23 +150,47 @@ defmodule LivebookWeb.Helpers do
|
|||
|
||||
@doc """
|
||||
Renders a checkbox input styled as a switch.
|
||||
"""
|
||||
def render_switch(name, checked, label, opts \\ []) do
|
||||
assigns = %{
|
||||
name: name,
|
||||
checked: checked,
|
||||
label: label,
|
||||
disabled: Keyword.get(opts, :disabled, false)
|
||||
}
|
||||
|
||||
~L"""
|
||||
## Examples
|
||||
|
||||
<.switch_checkbox
|
||||
name="likes_cats"
|
||||
label="I very much like cats"
|
||||
checked={@likes_cats} />
|
||||
"""
|
||||
def switch_checkbox(assigns) do
|
||||
assigns = assign_new(assigns, :disabled, fn -> false end)
|
||||
|
||||
~H"""
|
||||
<div class="flex space-x-3 items-center justify-between">
|
||||
<span class="text-gray-700"><%= @label %></span>
|
||||
<label class="switch-button <%= if(@disabled, do: "switch-button--disabled") %>">
|
||||
<%= tag :input, class: "switch-button__checkbox", type: "checkbox", name: @name, checked: @checked %>
|
||||
<label class={"switch-button #{if(@disabled, do: "switch-button--disabled")}"}>
|
||||
<input class="switch-button__checkbox" type="checkbox" name={@name} checked={@checked} />
|
||||
<div class="switch-button__bg"></div>
|
||||
</label>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a choice button that is either active or not.
|
||||
|
||||
## Examples
|
||||
|
||||
<.choice_button active={@tab == "my_tab"} phx-click="set_my_tab">
|
||||
My tab
|
||||
</.choice_button>
|
||||
"""
|
||||
def choice_button(assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign_new(:class, fn -> "" end)
|
||||
|> assign(:attrs, assigns_to_attributes(assigns, [:active, :class]))
|
||||
|
||||
~H"""
|
||||
<button class={"choice-button #{if(@active, do: "active")} #{@class}"} {@attrs}>
|
||||
<%= render_block(@inner_block) %>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
defmodule LivebookWeb.NotebookCardComponent do
|
||||
use LivebookWeb, :live_component
|
||||
defmodule LivebookWeb.ExploreHelpers do
|
||||
use Phoenix.Component
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
alias LivebookWeb.Router.Helpers, as: Routes
|
||||
|
||||
@doc """
|
||||
Renders an explore notebook card.
|
||||
"""
|
||||
def notebook_card(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col">
|
||||
<%= live_redirect to: Routes.explore_path(@socket, :notebook, @notebook_info.slug),
|
||||
class: "flex items-center justify-center p-6 border-2 border-gray-100 rounded-t-2xl h-[150px]" do %>
|
||||
<img src="<%= @notebook_info.image_url %>" class="max-h-full max-w-[75%]" />
|
||||
<img src={@notebook_info.image_url} class="max-h-full max-w-[75%]" />
|
||||
<% end %>
|
||||
<div class="px-6 py-4 bg-gray-100 rounded-b-2xl flex-grow">
|
||||
<%= live_redirect @notebook_info.title,
|
|
@ -4,6 +4,7 @@ defmodule LivebookWeb.ExploreLive do
|
|||
import LivebookWeb.UserHelpers
|
||||
import LivebookWeb.SessionHelpers
|
||||
|
||||
alias LivebookWeb.{SidebarHelpers, ExploreHelpers}
|
||||
alias Livebook.Notebook.Explore
|
||||
|
||||
@impl true
|
||||
|
@ -26,22 +27,20 @@ defmodule LivebookWeb.ExploreLive do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex flex-grow h-full">
|
||||
<%= live_component LivebookWeb.SidebarComponent,
|
||||
id: :sidebar,
|
||||
items: [
|
||||
%{type: :logo},
|
||||
%{type: :break},
|
||||
%{type: :user, current_user: @current_user, path: Routes.explore_path(@socket, :user)}
|
||||
] %>
|
||||
<SidebarHelpers.sidebar>
|
||||
<SidebarHelpers.logo_item socket={@socket} />
|
||||
<SidebarHelpers.break_item />
|
||||
<SidebarHelpers.user_item current_user={@current_user} path={Routes.explore_path(@socket, :user)} />
|
||||
</SidebarHelpers.sidebar>
|
||||
<div class="flex-grow px-6 py-8 overflow-y-auto">
|
||||
<div class="max-w-screen-md w-full mx-auto px-4 pb-8 space-y-8">
|
||||
<div>
|
||||
<div class="relative">
|
||||
<%= live_patch to: Routes.home_path(@socket, :page),
|
||||
class: "hidden md:block absolute top-[50%] left-[-12px] transform -translate-y-1/2 -translate-x-full" do %>
|
||||
<%= remix_icon("arrow-left-line", class: "text-2xl align-middle") %>
|
||||
<.remix_icon icon="arrow-left-line" class="text-2xl align-middle" />
|
||||
<% end %>
|
||||
<h1 class="text-3xl text-gray-800 font-semibold">
|
||||
Explore
|
||||
|
@ -61,19 +60,20 @@ defmodule LivebookWeb.ExploreLive do
|
|||
<%= @lead_notebook_info.description %>
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<%= live_patch "Let's go", to: Routes.explore_path(@socket, :notebook, @lead_notebook_info.slug),
|
||||
<%= live_patch "Let's go",
|
||||
to: Routes.explore_path(@socket, :notebook, @lead_notebook_info.slug),
|
||||
class: "button button-blue" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow hidden md:flex flex items-center justify-center">
|
||||
<img src="<%= @lead_notebook_info.image_url %>" height="120" width="120" alt="livebook" />
|
||||
<img src={@lead_notebook_info.image_url} height="120" width="120" alt="livebook" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<%= for {info, idx} <- Enum.with_index(@notebook_infos) do %>
|
||||
<%= live_component LivebookWeb.NotebookCardComponent,
|
||||
id: "notebook-card-#{idx}",
|
||||
notebook_info: info %>
|
||||
<%# Note: it's fine to use stateless components in this comprehension,
|
||||
because @notebook_infos never change %>
|
||||
<%= for info <- @notebook_infos do %>
|
||||
<ExploreHelpers.notebook_card notebook_info={info} socket={@socket} />
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -82,7 +82,7 @@ defmodule LivebookWeb.ExploreLive do
|
|||
|
||||
<%= if @live_action == :user do %>
|
||||
<%= live_modal LivebookWeb.UserComponent,
|
||||
id: :user_modal,
|
||||
id: "user",
|
||||
modal_class: "w-full max-w-sm",
|
||||
user: @current_user,
|
||||
return_to: Routes.explore_path(@socket, :page) %>
|
||||
|
|
|
@ -4,6 +4,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
import LivebookWeb.UserHelpers
|
||||
import LivebookWeb.SessionHelpers
|
||||
|
||||
alias LivebookWeb.{SidebarHelpers, ExploreHelpers}
|
||||
alias Livebook.{SessionSupervisor, Session, LiveMarkdown, Notebook}
|
||||
|
||||
@impl true
|
||||
|
@ -28,64 +29,61 @@ defmodule LivebookWeb.HomeLive do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex flex-grow h-full">
|
||||
<%= live_component LivebookWeb.SidebarComponent,
|
||||
id: :sidebar,
|
||||
items: [
|
||||
%{type: :break},
|
||||
%{type: :user, current_user: @current_user, path: Routes.home_path(@socket, :user)}
|
||||
] %>
|
||||
<SidebarHelpers.sidebar>
|
||||
<SidebarHelpers.break_item />
|
||||
<SidebarHelpers.user_item current_user={@current_user} path={Routes.home_path(@socket, :user)} />
|
||||
</SidebarHelpers.sidebar>
|
||||
<div class="flex-grow px-6 py-8 overflow-y-auto">
|
||||
<div class="max-w-screen-lg w-full mx-auto px-4 pb-8 space-y-4">
|
||||
<div class="flex flex-col space-y-2 items-center sm:flex-row sm:space-y-0 sm:justify-between sm:pb-4 pb-8 border-b border-gray-200">
|
||||
<div class="flex flex-col space-y-2 items-center pb-4 border-b border-gray-200
|
||||
sm:flex-row sm:space-y-0 sm:justify-between">
|
||||
<div class="text-2xl text-gray-800 font-semibold">
|
||||
<img src="/images/logo-with-text.png" class="h-[50px]" alt="Livebook" />
|
||||
</div>
|
||||
<div class="flex space-x-2 pt-2">
|
||||
<%= live_patch to: Routes.home_path(@socket, :import, "url"),
|
||||
class: "button button-outlined-gray whitespace-nowrap" do %>
|
||||
Import
|
||||
<% end %>
|
||||
<button class="button button-blue"
|
||||
phx-click="new">
|
||||
<%= live_patch "Import",
|
||||
to: Routes.home_path(@socket, :import, "url"),
|
||||
class: "button button-outlined-gray whitespace-nowrap" %>
|
||||
<button class="button button-blue" phx-click="new">
|
||||
New notebook
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-80">
|
||||
<%= live_component LivebookWeb.PathSelectComponent,
|
||||
id: "path_select",
|
||||
path: @path,
|
||||
extnames: [LiveMarkdown.extension()],
|
||||
running_paths: paths(@session_summaries),
|
||||
phx_target: nil,
|
||||
phx_submit: nil do %>
|
||||
id: "path_select",
|
||||
path: @path,
|
||||
extnames: [LiveMarkdown.extension()],
|
||||
running_paths: paths(@session_summaries),
|
||||
phx_target: nil,
|
||||
phx_submit: nil do %>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<%= content_tag :button,
|
||||
class: "button button-outlined-gray whitespace-nowrap",
|
||||
phx_click: "fork",
|
||||
disabled: not path_forkable?(@path) do %>
|
||||
<%= remix_icon("git-branch-line", class: "align-middle mr-1") %>
|
||||
<button class="button button-outlined-gray whitespace-nowrap"
|
||||
phx-click="fork"
|
||||
disabled={not path_forkable?(@path)}>
|
||||
<.remix_icon icon="git-branch-line" class="align-middle mr-1" />
|
||||
<span>Fork</span>
|
||||
<% end %>
|
||||
</button>
|
||||
<%= if path_running?(@path, @session_summaries) do %>
|
||||
<%= live_redirect "Join session", to: Routes.session_path(@socket, :page, session_id_by_path(@path, @session_summaries)),
|
||||
class: "button button-blue" %>
|
||||
<%= live_redirect "Join session",
|
||||
to: Routes.session_path(@socket, :page, session_id_by_path(@path, @session_summaries)),
|
||||
class: "button button-blue" %>
|
||||
<% else %>
|
||||
<%= tag :span, if(File.regular?(@path) and not file_writable?(@path),
|
||||
do: [class: "tooltip top", aria_label: "This file is write-protected, please fork instead"],
|
||||
else: []
|
||||
) %>
|
||||
<%= content_tag :button, "Open",
|
||||
class: "button button-blue",
|
||||
phx_click: "open",
|
||||
disabled: not path_openable?(@path, @session_summaries) %>
|
||||
<span {open_button_tooltip_attrs(@path)}>
|
||||
<button class="button button-blue"
|
||||
phx-click="open"
|
||||
disabled={not path_openable?(@path, @session_summaries)}>
|
||||
Open
|
||||
</button>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mb-4 flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold text-gray-800">
|
||||
|
@ -94,37 +92,23 @@ defmodule LivebookWeb.HomeLive do
|
|||
<%= live_redirect to: Routes.explore_path(@socket, :page),
|
||||
class: "flex items-center text-blue-600" do %>
|
||||
<span class="font-semibold">See all</span>
|
||||
<%= remix_icon("arrow-right-line", class: "align-middle ml-1") %>
|
||||
<.remix_icon icon="arrow-right-line" class="align-middle ml-1" />
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<%= for {info, idx} <- Enum.with_index(@notebook_infos) do %>
|
||||
<%= live_component LivebookWeb.NotebookCardComponent,
|
||||
id: "notebook-card-#{idx}",
|
||||
notebook_info: info %>
|
||||
<%# Note: it's fine to use stateless components in this comprehension,
|
||||
because @notebook_infos never change %>
|
||||
<%= for info <- @notebook_infos do %>
|
||||
<ExploreHelpers.notebook_card notebook_info={info} socket={@socket} />
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-12">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-800">
|
||||
Running sessions
|
||||
</h2>
|
||||
<%= if @session_summaries == [] do %>
|
||||
<div class="p-5 flex space-x-4 items-center border border-gray-200 rounded-lg">
|
||||
<div>
|
||||
<%= remix_icon("windy-line", class: "text-gray-400 text-xl") %>
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
You do not have any running sessions.
|
||||
<br>
|
||||
Please create a new one by clicking <span class="font-semibold">“New notebook”</span>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= live_component LivebookWeb.HomeLive.SessionsComponent,
|
||||
id: "sessions_list",
|
||||
session_summaries: @session_summaries %>
|
||||
<% end %>
|
||||
<.sessions_list session_summaries={@session_summaries} socket={@socket} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -132,7 +116,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
|
||||
<%= if @live_action == :user do %>
|
||||
<%= live_modal LivebookWeb.UserComponent,
|
||||
id: :user_modal,
|
||||
id: "user",
|
||||
modal_class: "w-full max-w-sm",
|
||||
user: @current_user,
|
||||
return_to: Routes.home_path(@socket, :page) %>
|
||||
|
@ -140,7 +124,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
|
||||
<%= if @live_action == :close_session do %>
|
||||
<%= live_modal LivebookWeb.HomeLive.CloseSessionComponent,
|
||||
id: :close_session_modal,
|
||||
id: "close-session",
|
||||
modal_class: "w-full max-w-xl",
|
||||
return_to: Routes.home_path(@socket, :page),
|
||||
session_summary: @session_summary %>
|
||||
|
@ -148,7 +132,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
|
||||
<%= if @live_action == :import do %>
|
||||
<%= live_modal LivebookWeb.HomeLive.ImportComponent,
|
||||
id: :import_modal,
|
||||
id: "import",
|
||||
modal_class: "w-full max-w-xl",
|
||||
return_to: Routes.home_path(@socket, :page),
|
||||
tab: @tab %>
|
||||
|
@ -156,6 +140,73 @@ defmodule LivebookWeb.HomeLive do
|
|||
"""
|
||||
end
|
||||
|
||||
defp open_button_tooltip_attrs(path) do
|
||||
if File.regular?(path) and not file_writable?(path) do
|
||||
[class: "tooltip top", aria_label: "This file is write-protected, please fork instead"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp sessions_list(%{session_summaries: []} = assigns) do
|
||||
~H"""
|
||||
<div class="p-5 flex space-x-4 items-center border border-gray-200 rounded-lg">
|
||||
<div>
|
||||
<.remix_icon icon="windy-line" class="text-gray-400 text-xl" />
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
You do not have any running sessions.
|
||||
<br>
|
||||
Please create a new one by clicking <span class="font-semibold">“New notebook”</span>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp sessions_list(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col space-y-4">
|
||||
<%= for summary <- @session_summaries do %>
|
||||
<div class="p-5 flex items-center border border-gray-200 rounded-lg"
|
||||
data-test-session-id={summary.session_id}>
|
||||
<div class="flex-grow flex flex-col space-y-1">
|
||||
<%= live_redirect summary.notebook_name,
|
||||
to: Routes.session_path(@socket, :page, summary.session_id),
|
||||
class: "font-semibold text-gray-800 hover:text-gray-900" %>
|
||||
<div class="text-gray-600 text-sm">
|
||||
<%= summary.path || "No file" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" id={"session-#{summary.session_id}-menu"} phx-hook="Menu" data-element="menu">
|
||||
<button class="icon-button" data-toggle>
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<button class="menu__item text-gray-500"
|
||||
phx-click="fork_session"
|
||||
phx-value-id={summary.session_id}>
|
||||
<.remix_icon icon="git-branch-line" />
|
||||
<span class="font-medium">Fork</span>
|
||||
</button>
|
||||
<a class="menu__item text-gray-500"
|
||||
href={live_dashboard_process_path(@socket, summary.pid)}
|
||||
target="_blank">
|
||||
<.remix_icon icon="dashboard-2-line" />
|
||||
<span class="font-medium">See on Dashboard</span>
|
||||
</a>
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, summary.session_id),
|
||||
class: "menu__item text-red-600" do %>
|
||||
<.remix_icon icon="close-circle-line" />
|
||||
<span class="font-medium">Close</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"session_id" => session_id}, _url, socket) do
|
||||
session_summary = Enum.find(socket.assigns.session_summaries, &(&1.session_id == session_id))
|
||||
|
|
|
@ -5,7 +5,7 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 pb-4 flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Close session
|
||||
|
@ -16,8 +16,8 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do
|
|||
This won't delete any persisted files.
|
||||
</p>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<button class="button button-red" phx-click="close" phx-target="<%= @myself %>">
|
||||
<%= remix_icon("close-circle-line", class: "align-middle mr-1") %>
|
||||
<button class="button button-red" phx-click="close" phx-target={@myself}>
|
||||
<.remix_icon icon="close-circle-line" class="align-middle mr-1" />
|
||||
Close session
|
||||
</button>
|
||||
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
|
||||
|
|
|
@ -3,22 +3,22 @@ defmodule LivebookWeb.HomeLive.ImportComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 pb-4 flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Import notebook
|
||||
</h3>
|
||||
<div class="tabs">
|
||||
<%= live_patch to: Routes.home_path(@socket, :import, "url"),
|
||||
class: "tab #{if(@tab == "url", do: "active")}" do %>
|
||||
<%= remix_icon("download-cloud-2-line", class: "align-middle") %>
|
||||
class: "tab #{if(@tab == "url", do: "active")}" do %>
|
||||
<.remix_icon icon="download-cloud-2-line" class="align-middle" />
|
||||
<span class="font-medium">
|
||||
From URL
|
||||
</span>
|
||||
<% end %>
|
||||
<%= live_patch to: Routes.home_path(@socket, :import, "content"),
|
||||
class: "tab #{if(@tab == "content", do: "active")}" do %>
|
||||
<%= remix_icon("clipboard-line", class: "align-middle") %>
|
||||
class: "tab #{if(@tab == "content", do: "active")}" do %>
|
||||
<.remix_icon icon="clipboard-line" class="align-middle" />
|
||||
<span class="font-medium">
|
||||
From clipboard
|
||||
</span>
|
||||
|
@ -27,17 +27,12 @@ defmodule LivebookWeb.HomeLive.ImportComponent do
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= case @tab do %>
|
||||
<% "url" -> %>
|
||||
<%= live_component LivebookWeb.HomeLive.ImportUrlComponent,
|
||||
id: "import_url" %>
|
||||
|
||||
<% "content" -> %>
|
||||
<%= live_component LivebookWeb.HomeLive.ImportContentComponent,
|
||||
id: "import_content" %>
|
||||
<% end %>
|
||||
<%= live_component component_for_tab(@tab), id: "import-#{@tab}" %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp component_for_tab("url"), do: LivebookWeb.HomeLive.ImportUrlComponent
|
||||
defp component_for_tab("content"), do: LivebookWeb.HomeLive.ImportContentComponent
|
||||
end
|
||||
|
|
|
@ -8,26 +8,25 @@ defmodule LivebookWeb.HomeLive.ImportContentComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex-col space-y-5">
|
||||
<p class="text-gray-700">
|
||||
Import notebook by directly pasting the <span class="font-semibold">live markdown</span> content.
|
||||
</p>
|
||||
<%= f = form_for :data, "#",
|
||||
phx_submit: "import",
|
||||
phx_change: "validate",
|
||||
phx_target: @myself,
|
||||
autocomplete: "off" %>
|
||||
<.form let={f} for={:data}
|
||||
phx-submit="import"
|
||||
phx-change="validate"
|
||||
phx-target={@myself}
|
||||
autocomplete="off">
|
||||
<%= textarea f, :content, value: @content, class: "input resize-none",
|
||||
placeholder: "Notebook content",
|
||||
autofocus: true,
|
||||
spellcheck: "false",
|
||||
rows: 5 %>
|
||||
|
||||
<%= submit "Import",
|
||||
class: "mt-5 button button-blue",
|
||||
disabled: @content == "" %>
|
||||
</form>
|
||||
<button class="mt-5 button button-blue" type="submit" disabled={@content == ""}>
|
||||
Import
|
||||
</button>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ defmodule LivebookWeb.HomeLive.ImportUrlComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex-col space-y-5">
|
||||
<%= if @error_message do %>
|
||||
<div class="error-box">
|
||||
|
@ -20,19 +20,21 @@ defmodule LivebookWeb.HomeLive.ImportUrlComponent do
|
|||
<p class="text-gray-700">
|
||||
Paste the URL to a .livemd file, to a GitHub file, or to a Gist to import it.
|
||||
</p>
|
||||
<%= f = form_for :data, "#",
|
||||
phx_submit: "import",
|
||||
phx_change: "validate",
|
||||
phx_target: @myself,
|
||||
autocomplete: "off" %>
|
||||
<.form let={f} for={:data}
|
||||
phx-submit="import"
|
||||
phx-change="validate"
|
||||
phx-target={@myself}
|
||||
autocomplete="off">
|
||||
<%= text_input f, :url, value: @url, class: "input",
|
||||
placeholder: "Notebook URL",
|
||||
autofocus: true,
|
||||
spellcheck: "false" %>
|
||||
<%= submit "Import",
|
||||
class: "mt-5 button button-blue",
|
||||
disabled: not Utils.valid_url?(@url) %>
|
||||
</form>
|
||||
<button class="mt-5 button button-blue"
|
||||
type="submit"
|
||||
disabled={not Utils.valid_url?(@url)}>
|
||||
Import
|
||||
</button>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
defmodule LivebookWeb.HomeLive.SessionsComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div class="flex flex-col space-y-4">
|
||||
<%= for summary <- @session_summaries do %>
|
||||
<div class="p-5 flex items-center border border-gray-200 rounded-lg"
|
||||
data-test-session-id="<%= summary.session_id %>">
|
||||
<div class="flex-grow flex flex-col space-y-1">
|
||||
<%= live_redirect summary.notebook_name, to: Routes.session_path(@socket, :page, summary.session_id),
|
||||
class: "font-semibold text-gray-800 hover:text-gray-900" %>
|
||||
<div class="text-gray-600 text-sm">
|
||||
<%= summary.path || "No file" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" id="session-<%= summary.session_id %>-menu" phx-hook="Menu" data-element="menu">
|
||||
<button class="icon-button" data-toggle>
|
||||
<%= remix_icon("more-2-fill", class: "text-xl") %>
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<button class="menu__item text-gray-500"
|
||||
phx-click="fork_session"
|
||||
phx-value-id="<%= summary.session_id %>">
|
||||
<%= remix_icon("git-branch-line") %>
|
||||
<span class="font-medium">Fork</span>
|
||||
</button>
|
||||
<%= link to: live_dashboard_process_path(@socket, summary.pid),
|
||||
class: "menu__item text-gray-500",
|
||||
target: "_blank" do %>
|
||||
<%= remix_icon("dashboard-2-line") %>
|
||||
<span class="font-medium">See on Dashboard</span>
|
||||
<% end %>
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, summary.session_id),
|
||||
class: "menu__item text-red-600" do %>
|
||||
<%= remix_icon("close-circle-line") %>
|
||||
<span class="font-medium">Close</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -3,9 +3,8 @@ defmodule LivebookWeb.ModalComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div class="fixed z-[10000] inset-0"
|
||||
id="<%= @id %>">
|
||||
~H"""
|
||||
<div class="fixed z-[10000] inset-0">
|
||||
|
||||
<!-- Modal container -->
|
||||
<div class="h-screen flex items-center justify-center p-4">
|
||||
|
@ -15,17 +14,17 @@ defmodule LivebookWeb.ModalComponent do
|
|||
phx-capture-click="close"
|
||||
phx-window-keydown="close"
|
||||
phx-key="escape"
|
||||
phx-target="#<%= @id %>"
|
||||
phx-target={@myself}
|
||||
phx-page-loading></div>
|
||||
|
||||
<!-- Modal box -->
|
||||
<div class="relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl <%= @modal_class %>"
|
||||
<div class={"relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl #{@modal_class}"}
|
||||
role="dialog"
|
||||
aria-modal="true">
|
||||
|
||||
<%= live_patch to: @return_to, class: "absolute top-6 right-6 text-gray-400 flex space-x-1 items-center" do %>
|
||||
<span class="text-sm">(esc)</span>
|
||||
<%= remix_icon("close-line", class: "text-2xl") %>
|
||||
<.remix_icon icon="close-line" class="text-2xl" />
|
||||
<% end %>
|
||||
|
||||
<%= live_component @component, @opts %>
|
||||
|
|
|
@ -3,8 +3,8 @@ defmodule LivebookWeb.Output.ImageComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<%= tag :img, src: data_url(@content, @mime_type), alt: "output image" %>
|
||||
~H"""
|
||||
<img src={data_url(@content, @mime_type)} alt="output image" />
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
|
@ -13,11 +13,11 @@ defmodule LivebookWeb.Output.MarkdownComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="markdown"
|
||||
id="markdown-renderer-<%= @id %>"
|
||||
id={"markdown-renderer-#{@id}"}
|
||||
phx-hook="MarkdownRenderer"
|
||||
data-id="<%= @id %>">
|
||||
data-id={@id}>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
|
@ -30,7 +30,7 @@ defmodule LivebookWeb.Output.TableDynamicLive do
|
|||
|
||||
@impl true
|
||||
def render(%{loading: true} = assigns) do
|
||||
~L"""
|
||||
~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>
|
||||
|
@ -42,7 +42,7 @@ defmodule LivebookWeb.Output.TableDynamicLive do
|
|||
end
|
||||
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="mb-4 flex items-center space-x-3">
|
||||
<h3 class="font-semibold text-gray-800">
|
||||
<%= @name %>
|
||||
|
@ -52,9 +52,8 @@ defmodule LivebookWeb.Output.TableDynamicLive do
|
|||
<div class="flex space-x-2">
|
||||
<%= if :refetch in @features do %>
|
||||
<span class="tooltip left" aria-label="Refetch">
|
||||
<%= tag :button, class: "icon-button",
|
||||
phx_click: "refetch" %>
|
||||
<%= remix_icon("refresh-line", class: "text-xl") %>
|
||||
<button class="icon-button" phx-click="refetch">
|
||||
<.remix_icon icon="refresh-line" class="text-xl" />
|
||||
</button>
|
||||
</span>
|
||||
<% end %>
|
||||
|
@ -62,22 +61,20 @@ defmodule LivebookWeb.Output.TableDynamicLive do
|
|||
<!-- Pagination -->
|
||||
<%= if :pagination in @features and @total_rows > 0 do %>
|
||||
<div class="flex space-x-2">
|
||||
<%= tag :button,
|
||||
class: "flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300",
|
||||
disabled: @page == 1,
|
||||
phx_click: "prev" %>
|
||||
<%= remix_icon("arrow-left-s-line", class: "text-xl") %>
|
||||
<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>
|
||||
<%= tag :button,
|
||||
class: "flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300",
|
||||
disabled: @page == max_page(@total_rows, @limit),
|
||||
phx_click: "next" %>
|
||||
<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("arrow-right-s-line", class: "text-xl") %>
|
||||
<.remix_icon icon="arrow-right-s-line" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -94,13 +91,13 @@ defmodule LivebookWeb.Output.TableDynamicLive do
|
|||
<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") %>"
|
||||
<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 %>">
|
||||
phx-value-column_idx={idx}>
|
||||
<div class="flex items-center space-x-1">
|
||||
<span><%= column.label %></span>
|
||||
<%= tag :span, class: unless(@order_by == column.key, do: "invisible") %>
|
||||
<%= remix_icon(order_icon(@order), class: "text-xl align-middle leading-none") %>
|
||||
<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>
|
||||
|
|
|
@ -3,25 +3,25 @@ defmodule LivebookWeb.Output.TextComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div id="virtualized-text-<%= @id %>"
|
||||
~H"""
|
||||
<div id={"virtualized-text-#{@id}"}
|
||||
class="relative"
|
||||
phx-hook="VirtualizedLines"
|
||||
data-max-height="300"
|
||||
data-follow="<%= @follow %>">
|
||||
data-follow={to_string(@follow)}>
|
||||
<%# Add a newline to each element, so that multiple lines can be copied properly %>
|
||||
<div data-template class="hidden"
|
||||
id="virtualized-text-<%= @id %>-template"
|
||||
id={"virtualized-text-#{@id}-template"}
|
||||
><%= for line <- ansi_to_html_lines(@content) do %><div><%= [line, "\n"] %></div><% end %></div>
|
||||
<div data-content class="overflow-auto whitespace-pre font-editor text-gray-500 tiny-scrollbar"
|
||||
id="virtualized-text-<%= @id %>-content"
|
||||
id={"virtualized-text-#{@id}-content"}
|
||||
phx-update="ignore"></div>
|
||||
<div class="absolute right-0 top-0 z-10">
|
||||
<button class="icon-button bg-gray-100"
|
||||
id="virtualized-text-<%= @id %>-clipcopy"
|
||||
id={"virtualized-text-#{@id}-clipcopy"}
|
||||
phx-hook="ClipCopy"
|
||||
data-target-id="virtualized-text-<%= @id %>-template">
|
||||
<%= remix_icon("clipboard-line", class: "text-lg") %>
|
||||
data-target-id={"virtualized-text-#{@id}-template"}>
|
||||
<.remix_icon icon="clipboard-line" class="text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,8 +10,8 @@ defmodule LivebookWeb.Output.VegaLiteDynamicLive do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div id="vega-lite-<%= @id %>" phx-hook="VegaLite" phx-update="ignore" data-id="<%= @id %>">
|
||||
~H"""
|
||||
<div id={"vega-lite-#{@id}"} phx-hook="VegaLite" phx-update="ignore" data-id={@id}>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
|
@ -9,8 +9,8 @@ defmodule LivebookWeb.Output.VegaLiteStaticComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div id="vega-lite-<%= @id %>" phx-hook="VegaLite" phx-update="ignore" data-id="<%= @id %>">
|
||||
~H"""
|
||||
<div id={"vega-lite-#{@id}"} phx-hook="VegaLite" phx-update="ignore" data-id={@id}>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
|
@ -19,11 +19,10 @@ defmodule LivebookWeb.PathSelectComponent do
|
|||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
inner_block = Map.get(socket.assigns, :inner_block, nil)
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
inner_block: inner_block,
|
||||
socket
|
||||
|> assign_new(:inner_block, fn -> nil end)
|
||||
|> assign(
|
||||
current_dir: nil,
|
||||
new_directory_name: nil,
|
||||
deleting_path: nil,
|
||||
|
@ -60,34 +59,31 @@ defmodule LivebookWeb.PathSelectComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex space-x-3 items-center mb-4">
|
||||
<form class="flex-grow"
|
||||
phx-change="set_path"
|
||||
<%= if @phx_submit do %>
|
||||
phx-submit="<%= @phx_submit %>"
|
||||
<% else %>
|
||||
onsubmit="return false"
|
||||
<% end %>
|
||||
<%= if @phx_target, do: "phx-target=#{@phx_target}" %>>
|
||||
phx-submit={@phx_submit}
|
||||
onsubmit={unless(@phx_submit, do: "return false")}
|
||||
phx-target={@phx_target}>
|
||||
<input class="input"
|
||||
id="input-path"
|
||||
phx-hook="FocusOnUpdate"
|
||||
type="text"
|
||||
name="path"
|
||||
placeholder="File"
|
||||
value="<%= @path %>"
|
||||
value={@path}
|
||||
spellcheck="false"
|
||||
autocomplete="off" />
|
||||
</form>
|
||||
<div class="relative" id="path-selector-menu" phx-hook="Menu" data-element="menu">
|
||||
<button class="icon-button" data-toggle tabindex="-1">
|
||||
<%= remix_icon("more-2-fill", class: "text-xl") %>
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<button class="menu__item text-gray-500" phx-click="new_directory" phx-target="<%= @myself %>">
|
||||
<%= remix_icon("folder-add-fill", class: "text-gray-400") %>
|
||||
<button class="menu__item text-gray-500" phx-click="new_directory" phx-target={@myself}>
|
||||
<.remix_icon icon="folder-add-fill" class="text-gray-400" />
|
||||
<span class="font-medium">New directory</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -107,13 +103,13 @@ defmodule LivebookWeb.PathSelectComponent do
|
|||
<div class="flex space-x-4">
|
||||
<button class="text-red-600 font-medium text-sm whitespace-nowrap"
|
||||
phx-click="do_delete_file"
|
||||
phx-target="<%= @myself %>">
|
||||
<%= remix_icon("delete-bin-6-line", class: "align-middle mr-1") %>
|
||||
phx-target={@myself}>
|
||||
<.remix_icon icon="delete-bin-6-line" class="align-middle mr-1" />
|
||||
Delete
|
||||
</button>
|
||||
<button class="text-gray-600 font-medium text-sm"
|
||||
phx-click="cancel_delete_file"
|
||||
phx-target="<%= @myself %>">
|
||||
phx-target={@myself}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
@ -124,23 +120,23 @@ defmodule LivebookWeb.PathSelectComponent do
|
|||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2 border-b border-dashed border-grey-200 mb-2 pb-2">
|
||||
<div class="flex space-x-2 items-center p-2 rounded-lg">
|
||||
<span class="block">
|
||||
<%= remix_icon("folder-add-fill", class: "text-xl align-middle text-gray-400") %>
|
||||
<.remix_icon icon="folder-add-fill" class="text-xl align-middle text-gray-400" />
|
||||
</span>
|
||||
<span class="flex font-medium text-gray-500">
|
||||
<div
|
||||
phx-window-keydown="cancel_new_directory"
|
||||
phx-key="escape"
|
||||
phx-target="<%= @myself %>">
|
||||
phx-target={@myself}>
|
||||
<input
|
||||
type="text"
|
||||
value="<%= @new_directory_name %>"
|
||||
value={@new_directory_name}
|
||||
autofocus
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
phx-blur="cancel_new_directory"
|
||||
phx-window-keydown="create_directory"
|
||||
phx-key="enter"
|
||||
phx-target="<%= @myself %>" />
|
||||
phx-target={@myself} />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -150,14 +146,24 @@ defmodule LivebookWeb.PathSelectComponent do
|
|||
<%= if highlighting?(@files) do %>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2 border-b border-dashed border-grey-200 mb-2 pb-2">
|
||||
<%= for file <- @files, file.highlighted != "" do %>
|
||||
<%= render_file(file, @phx_target, @myself, @renaming_path, @renamed_name) %>
|
||||
<.file
|
||||
file={file}
|
||||
phx_target={@phx_target}
|
||||
myself={@myself}
|
||||
renaming_path={@renaming_path}
|
||||
renamed_name={@renamed_name} />
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||
<%= for file <- @files, file.highlighted == "" do %>
|
||||
<%= render_file(file, @phx_target, @myself, @renaming_path, @renamed_name) %>
|
||||
<.file
|
||||
file={file}
|
||||
phx_target={@phx_target}
|
||||
myself={@myself}
|
||||
renaming_path={@renaming_path}
|
||||
renamed_name={@renamed_name} />
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -169,68 +175,60 @@ defmodule LivebookWeb.PathSelectComponent do
|
|||
Enum.any?(files, &(&1.highlighted != ""))
|
||||
end
|
||||
|
||||
defp render_file(
|
||||
%{path: renaming_path} = file,
|
||||
_phx_target,
|
||||
myself,
|
||||
renaming_path,
|
||||
renamed_name
|
||||
) do
|
||||
assigns = %{file: file, myself: myself, renamed_name: renamed_name}
|
||||
|
||||
~L"""
|
||||
defp file(%{file: %{path: path}, renaming_path: path} = assigns) do
|
||||
~H"""
|
||||
<div class="flex space-x-2 items-center p-2 rounded-lg">
|
||||
<span class="block">
|
||||
<%= remix_icon("edit-line", class: "text-xl align-middle text-gray-400") %>
|
||||
<.remix_icon icon="edit-line" class="text-xl align-middle text-gray-400" />
|
||||
</span>
|
||||
<span class="flex font-medium text-gray-500">
|
||||
<div
|
||||
phx-window-keydown="cancel_rename_file"
|
||||
phx-key="escape"
|
||||
phx-target="<%= @myself %>">
|
||||
phx-target={@myself}>
|
||||
<input class="w-full"
|
||||
type="text"
|
||||
value="<%= @renamed_name %>"
|
||||
value={@renamed_name}
|
||||
autofocus
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
phx-blur="cancel_rename_file"
|
||||
phx-window-keydown="do_rename_file"
|
||||
phx-key="enter"
|
||||
phx-target="<%= @myself %>" />
|
||||
phx-target={@myself} />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_file(file, phx_target, myself, _renaming_path, _renamed_name) do
|
||||
defp file(assigns) do
|
||||
icon =
|
||||
case file do
|
||||
case assigns.file do
|
||||
%{is_running: true} -> "play-circle-line"
|
||||
%{is_dir: true} -> "folder-fill"
|
||||
_ -> "file-code-line"
|
||||
end
|
||||
|
||||
assigns = %{file: file, icon: icon, myself: myself}
|
||||
assigns = assign(assigns, :icon, icon)
|
||||
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="relative"
|
||||
id="file-menu-<%= @file.path %>"
|
||||
id={"file-menu-#{@file.path}"}
|
||||
phx-hook="Menu"
|
||||
data-primary="false"
|
||||
data-element="menu">
|
||||
<button class="w-full flex space-x-2 items-center p-2 rounded-lg hover:bg-gray-100 focus:ring-1 focus:ring-gray-400"
|
||||
data-toggle
|
||||
phx-click="set_path"
|
||||
phx-value-path="<%= @file.path %>"
|
||||
<%= if phx_target, do: "phx-target=#{phx_target}" %>>
|
||||
phx-value-path={@file.path}
|
||||
phx-target={@phx_target}>
|
||||
<span class="block">
|
||||
<%= remix_icon(@icon, class: "text-xl align-middle #{if(@file.is_running, do: "text-green-300", else: "text-gray-400")}") %>
|
||||
<.remix_icon icon={@icon} class={"text-xl align-middle #{if(@file.is_running, do: "text-green-300", else: "text-gray-400")}"} />
|
||||
</span>
|
||||
<span class="flex font-medium overflow-hidden whitespace-nowrap <%= if(@file.is_running, do: "text-green-300", else: "text-gray-500") %>">
|
||||
<span class={"flex font-medium overflow-hidden whitespace-nowrap #{if(@file.is_running, do: "text-green-300", else: "text-gray-500")}"}>
|
||||
<%= if @file.highlighted != "" do %>
|
||||
<span class="font-medium <%= if(@file.is_running, do: "text-green-400", else: "text-gray-900") %>">
|
||||
<span class={"font-medium #{if(@file.is_running, do: "text-green-400", else: "text-gray-900")}"}>
|
||||
<%= @file.highlighted %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
@ -242,16 +240,16 @@ defmodule LivebookWeb.PathSelectComponent do
|
|||
<div class="menu" data-content>
|
||||
<button class="menu__item text-gray-500"
|
||||
phx-click="rename_file"
|
||||
phx-target="<%= @myself %>"
|
||||
phx-value-path="<%= @file.path %>">
|
||||
<%= remix_icon("edit-line") %>
|
||||
phx-target={@myself}
|
||||
phx-value-path={@file.path}>
|
||||
<.remix_icon icon="edit-line" />
|
||||
<span class="font-medium">Rename</span>
|
||||
</button>
|
||||
<button class="menu__item text-red-600"
|
||||
phx-click="delete_file"
|
||||
phx-target="<%= @myself %>"
|
||||
phx-value-path="<%= @file.path %>">
|
||||
<%= remix_icon("delete-bin-6-line") %>
|
||||
phx-target={@myself}
|
||||
phx-value-path={@file.path}>
|
||||
<.remix_icon icon="delete-bin-6-line" />
|
||||
<span class="font-medium">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -2,10 +2,11 @@ defmodule LivebookWeb.SessionLive do
|
|||
use LivebookWeb, :live_view
|
||||
|
||||
import LivebookWeb.UserHelpers
|
||||
import Livebook.Utils, only: [access_by_id: 1]
|
||||
|
||||
alias LivebookWeb.SidebarHelpers
|
||||
alias Livebook.{SessionSupervisor, Session, Delta, Notebook, Runtime}
|
||||
alias Livebook.Notebook.Cell
|
||||
import Livebook.Utils, only: [access_by_id: 1]
|
||||
|
||||
@impl true
|
||||
def mount(%{"id" => session_id}, %{"current_user_id" => current_user_id} = session, socket) do
|
||||
|
@ -67,53 +68,41 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex flex-grow h-full"
|
||||
id="session"
|
||||
data-element="session"
|
||||
phx-hook="Session">
|
||||
<%= live_component LivebookWeb.SidebarComponent,
|
||||
id: :sidebar,
|
||||
items: [
|
||||
%{type: :logo},
|
||||
%{
|
||||
type: :button,
|
||||
data_element: "sections-list-toggle",
|
||||
icon: "booklet-fill",
|
||||
label: "Sections (ss)",
|
||||
active: false
|
||||
},
|
||||
%{
|
||||
type: :button,
|
||||
data_element: "clients-list-toggle",
|
||||
icon: "group-fill",
|
||||
label: "Connected users (su)",
|
||||
active: false
|
||||
},
|
||||
%{
|
||||
type: :link,
|
||||
icon: "cpu-line",
|
||||
path: Routes.session_path(@socket, :runtime_settings, @session_id),
|
||||
label: "Runtime settings (sr)",
|
||||
active: @live_action == :runtime_settings
|
||||
},
|
||||
%{
|
||||
type: :link,
|
||||
icon: "delete-bin-6-fill",
|
||||
path: Routes.session_path(@socket, :bin, @session_id),
|
||||
label: "Bin (sb)",
|
||||
active: @live_action == :bin
|
||||
},
|
||||
%{type: :break},
|
||||
%{
|
||||
type: :link,
|
||||
icon: "keyboard-box-fill",
|
||||
path: Routes.session_path(@socket, :shortcuts, @session_id),
|
||||
label: "Keyboard shortcuts (?)",
|
||||
active: @live_action == :shortcuts
|
||||
},
|
||||
%{type: :user, current_user: @current_user, path: Routes.session_path(@socket, :user, @session_id)}
|
||||
] %>
|
||||
<SidebarHelpers.sidebar>
|
||||
<SidebarHelpers.logo_item socket={@socket} />
|
||||
<SidebarHelpers.button_item
|
||||
icon="booklet-fill"
|
||||
label="Sections (ss)"
|
||||
data_element="sections-list-toggle" />
|
||||
<SidebarHelpers.button_item
|
||||
icon="group-fill"
|
||||
label="Connected users (su)"
|
||||
data_element="clients-list-toggle" />
|
||||
<SidebarHelpers.link_item
|
||||
icon="cpu-line"
|
||||
label="Runtime settings (sr)"
|
||||
path={Routes.session_path(@socket, :runtime_settings, @session_id)}
|
||||
active={@live_action == :runtime_settings} />
|
||||
<SidebarHelpers.link_item
|
||||
icon="delete-bin-6-fill"
|
||||
label="Bin (sb)"
|
||||
path={Routes.session_path(@socket, :bin, @session_id)}
|
||||
active={@live_action == :bin} />
|
||||
<SidebarHelpers.break_item />
|
||||
<SidebarHelpers.link_item
|
||||
icon="keyboard-box-fill"
|
||||
label="Keyboard shortcuts (?)"
|
||||
path={Routes.session_path(@socket, :shortcuts, @session_id)}
|
||||
active={@live_action == :shortcuts} />
|
||||
<SidebarHelpers.user_item
|
||||
current_user={@current_user}
|
||||
path={Routes.session_path(@socket, :user, @session_id)} />
|
||||
</SidebarHelpers.sidebar>
|
||||
<div class="flex flex-col h-full w-full max-w-xs absolute z-30 top-0 left-[64px] shadow-xl md:static md:shadow-none overflow-y-auto bg-gray-50 border-r border-gray-100 px-6 py-10"
|
||||
data-element="side-panel">
|
||||
<div data-element="sections-list">
|
||||
|
@ -125,14 +114,14 @@ defmodule LivebookWeb.SessionLive do
|
|||
<%= for section_item <- @data_view.sections_items do %>
|
||||
<button class="text-left hover:text-gray-900 text-gray-500"
|
||||
data-element="sections-list-item"
|
||||
data-section-id="<%= section_item.id %>">
|
||||
data-section-id={section_item.id}>
|
||||
<%= section_item.name %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<button class="mt-8 p-8 py-1 text-gray-500 text-sm font-medium rounded-xl border border-gray-400 border-dashed hover:bg-gray-100 inline-flex items-center justify-center space-x-2"
|
||||
phx-click="append_section">
|
||||
<%= remix_icon("add-line", class: "text-lg align-center") %>
|
||||
<.remix_icon icon="add-line" class="text-lg align-center" />
|
||||
<span>New section</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -148,13 +137,13 @@ defmodule LivebookWeb.SessionLive do
|
|||
<div class="mt-4 flex flex-col space-y-4">
|
||||
<%= for {client_pid, user} <- @data_view.clients do %>
|
||||
<div class="flex items-center justify-between space-x-2"
|
||||
id="clients-list-item-<%= inspect(client_pid) %>"
|
||||
id={"clients-list-item-#{inspect(client_pid)}"}
|
||||
data-element="clients-list-item"
|
||||
data-client-pid="<%= inspect(client_pid) %>">
|
||||
data-client-pid={inspect(client_pid)}>
|
||||
<button class="flex space-x-2 items-center text-gray-500 hover:text-gray-900 disabled:pointer-events-none"
|
||||
<%= if client_pid == @self, do: "disabled" %>
|
||||
disabled={client_pid == @self}
|
||||
data-element="client-link">
|
||||
<%= render_user_avatar(user, class: "h-7 w-7 flex-shrink-0", text_class: "text-xs") %>
|
||||
<.user_avatar user={user} class="h-7 w-7 flex-shrink-0" text_class="text-xs" />
|
||||
<span><%= user.name || "Anonymous" %></span>
|
||||
</button>
|
||||
<%= if client_pid != @self do %>
|
||||
|
@ -162,14 +151,14 @@ defmodule LivebookWeb.SessionLive do
|
|||
data-element="client-follow-toggle"
|
||||
data-meta="follow">
|
||||
<button class="icon-button">
|
||||
<%= remix_icon("pushpin-line", class: "text-lg") %>
|
||||
<.remix_icon icon="pushpin-line" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip left" aria-label="Unfollow this user"
|
||||
data-element="client-follow-toggle"
|
||||
data-meta="unfollow">
|
||||
<button class="icon-button">
|
||||
<%= remix_icon("pushpin-fill", class: "text-lg") %>
|
||||
<.remix_icon icon="pushpin-fill" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<% end %>
|
||||
|
@ -192,23 +181,23 @@ defmodule LivebookWeb.SessionLive do
|
|||
data-update-attribute="phx-value-name"><%= @data_view.notebook_name %></h1>
|
||||
<div class="relative" id="session-menu" phx-hook="Menu" data-element="menu">
|
||||
<button class="icon-button" data-toggle>
|
||||
<%= remix_icon("more-2-fill", class: "text-xl") %>
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<button class="menu__item text-gray-500"
|
||||
phx-click="fork_session">
|
||||
<%= remix_icon("git-branch-line") %>
|
||||
<.remix_icon icon="git-branch-line" />
|
||||
<span class="font-medium">Fork</span>
|
||||
</button>
|
||||
<%= link to: live_dashboard_process_path(@socket, @session_pid),
|
||||
class: "menu__item text-gray-500",
|
||||
target: "_blank" do %>
|
||||
<%= remix_icon("dashboard-2-line") %>
|
||||
<a class="menu__item text-gray-500"
|
||||
href={live_dashboard_process_path(@socket, @session_pid)}
|
||||
target="_blank">
|
||||
<.remix_icon icon="dashboard-2-line" />
|
||||
<span class="font-medium">See on Dashboard</span>
|
||||
<% end %>
|
||||
</a>
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, @session_id),
|
||||
class: "menu__item text-red-600" do %>
|
||||
<%= remix_icon("close-circle-line") %>
|
||||
<.remix_icon icon="close-circle-line" />
|
||||
<span class="font-medium">Close</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -237,13 +226,16 @@ defmodule LivebookWeb.SessionLive do
|
|||
<div class="fixed bottom-[0.4rem] right-[1.5rem]">
|
||||
<%= live_component LivebookWeb.SessionLive.IndicatorsComponent,
|
||||
session_id: @session_id,
|
||||
data_view: @data_view %>
|
||||
path: @data_view.path,
|
||||
dirty: @data_view.dirty,
|
||||
runtime: @data_view.runtime,
|
||||
global_evaluation_status: @data_view.global_evaluation_status %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if @live_action == :user do %>
|
||||
<%= live_modal LivebookWeb.UserComponent,
|
||||
id: :user_modal,
|
||||
id: "user",
|
||||
modal_class: "w-full max-w-sm",
|
||||
user: @current_user,
|
||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||
|
@ -251,7 +243,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
<%= if @live_action == :runtime_settings do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.RuntimeComponent,
|
||||
id: :runtime_settings_modal,
|
||||
id: "runtime-settings",
|
||||
modal_class: "w-full max-w-4xl",
|
||||
return_to: Routes.session_path(@socket, :page, @session_id),
|
||||
session_id: @session_id,
|
||||
|
@ -260,7 +252,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
<%= if @live_action == :file_settings do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.PersistenceComponent,
|
||||
id: :runtime_settings_modal,
|
||||
id: "persistence",
|
||||
modal_class: "w-full max-w-4xl",
|
||||
return_to: Routes.session_path(@socket, :page, @session_id),
|
||||
session_id: @session_id,
|
||||
|
@ -270,7 +262,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
<%= if @live_action == :shortcuts do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.ShortcutsComponent,
|
||||
id: :shortcuts_modal,
|
||||
id: "shortcuts",
|
||||
modal_class: "w-full max-w-6xl",
|
||||
platform: @platform,
|
||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||
|
@ -278,7 +270,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
<%= if @live_action == :cell_settings do %>
|
||||
<%= live_modal settings_component_for(@cell),
|
||||
id: :cell_settings_modal,
|
||||
id: "cell-settings",
|
||||
modal_class: "w-full max-w-xl",
|
||||
session_id: @session_id,
|
||||
cell: @cell,
|
||||
|
@ -287,7 +279,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
<%= if @live_action == :cell_upload do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.CellUploadComponent,
|
||||
id: :cell_upload_modal,
|
||||
id: "cell-upload",
|
||||
modal_class: "w-full max-w-xl",
|
||||
session_id: @session_id,
|
||||
cell: @cell,
|
||||
|
@ -297,7 +289,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
<%= if @live_action == :delete_section do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.DeleteSectionComponent,
|
||||
id: :delete_section_modal,
|
||||
id: "delete-section",
|
||||
modal_class: "w-full max-w-xl",
|
||||
session_id: @session_id,
|
||||
section: @section,
|
||||
|
@ -307,7 +299,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
<%= if @live_action == :bin do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.BinComponent,
|
||||
id: :bin_modal,
|
||||
id: "bin",
|
||||
modal_class: "w-full max-w-4xl",
|
||||
session_id: @session_id,
|
||||
bin_entries: @data_view.bin_entries,
|
||||
|
|
|
@ -15,7 +15,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex-col space-y-5">
|
||||
<%= if @error_message do %>
|
||||
<div class="error-box">
|
||||
|
@ -39,7 +39,11 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
<p class="text-gray-700">
|
||||
Then enter the connection information below:
|
||||
</p>
|
||||
<%= f = form_for :data, "#", phx_submit: "init", phx_change: "validate", autocomplete: "off", spellcheck: "false" %>
|
||||
<.form let={f} for={:data}
|
||||
phx-submit="init"
|
||||
phx-change="validate"
|
||||
autocomplete="off"
|
||||
spellcheck="false">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div>
|
||||
<div class="input-label">Name</div>
|
||||
|
@ -50,8 +54,12 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
<%= text_input f, :cookie, value: @data["cookie"], class: "input", placeholder: "mycookie" %>
|
||||
</div>
|
||||
</div>
|
||||
<%= submit "Connect", class: "mt-5 button button-blue", disabled: not data_valid?(@data) %>
|
||||
</form>
|
||||
<button class="mt-5 button button-blue"
|
||||
type="submit"
|
||||
disabled={not data_valid?(@data)}>
|
||||
Connect
|
||||
</button>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
|
@ -30,7 +30,7 @@ defmodule LivebookWeb.SessionLive.BinComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Bin
|
||||
|
@ -42,18 +42,18 @@ defmodule LivebookWeb.SessionLive.BinComponent do
|
|||
<%= if @bin_entries == [] do %>
|
||||
<div class="p-5 flex space-x-4 items-center border border-gray-200 rounded-lg">
|
||||
<div>
|
||||
<%= remix_icon("windy-line", class: "text-gray-400 text-xl") %>
|
||||
<.remix_icon icon="windy-line" class="text-gray-400 text-xl" />
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
There are currently no cells in the bin.
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<form phx-change="search" onsubmit="return false" phx-target="<%= @myself %>">
|
||||
<form phx-change="search" onsubmit="return false" phx-target={@myself}>
|
||||
<input class="input"
|
||||
type="text"
|
||||
name="search"
|
||||
value="<%= @search %>"
|
||||
value={@search}
|
||||
placeholder="Search"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
|
@ -67,40 +67,38 @@ defmodule LivebookWeb.SessionLive.BinComponent do
|
|||
<%= index %>.
|
||||
<span class="font-semibold"><%= Cell.type(cell) |> Atom.to_string() |> String.capitalize() %></span> cell
|
||||
deleted from <span class="font-semibold">“<%= entry.section_name %>”</span> section
|
||||
<span class="font-semibold">
|
||||
<%= entry.deleted_at |> DateTime.to_naive() |> Livebook.Utils.Time.time_ago_in_words() %> ago
|
||||
</span>
|
||||
<span class="font-semibold"><%= format_date_relatively(entry.deleted_at) %></span>
|
||||
</p>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<span class="tooltip left" aria-label="Copy source">
|
||||
<button class="icon-button"
|
||||
id="bin-cell-<%= cell.id %>-clipcopy"
|
||||
id={"bin-cell-#{cell.id}-clipcopy"}
|
||||
phx-hook="ClipCopy"
|
||||
data-target-id="bin-cell-<%= cell.id %>-source">
|
||||
<%= remix_icon("clipboard-line", class: "text-lg") %>
|
||||
data-target-id={"bin-cell-#{cell.id}-source"}>
|
||||
<.remix_icon icon="clipboard-line" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip left" aria-label="Restore">
|
||||
<button class="icon-button"
|
||||
phx-click="restore"
|
||||
phx-value-cell_id="<%= entry.cell.id %>"
|
||||
phx-target="<%= @myself %>">
|
||||
<%= remix_icon("arrow-go-back-line", class: "text-lg") %>
|
||||
phx-value-cell_id={entry.cell.id}
|
||||
phx-target={@myself}>
|
||||
<.remix_icon icon="arrow-go-back-line" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="markdown">
|
||||
<pre><code
|
||||
id="bin-cell-<%= cell.id %>-source"
|
||||
id={"bin-cell-#{cell.id}-source"}
|
||||
phx-hook="Highlight"
|
||||
data-language="<%= Cell.type(cell) %>"><%= cell.source %></code></pre>
|
||||
data-language={Cell.type(cell)}><%= cell.source %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= if length(@matching_entries) > @limit do %>
|
||||
<div class="flex justify-center">
|
||||
<button class="button button-outlined-gray" phx-click="more" phx-target="<%= @myself %>">
|
||||
<button class="button button-outlined-gray" phx-click="more" phx-target={@myself}>
|
||||
Older
|
||||
</button>
|
||||
</div>
|
||||
|
@ -112,6 +110,11 @@ defmodule LivebookWeb.SessionLive.BinComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp format_date_relatively(date) do
|
||||
time_words = date |> DateTime.to_naive() |> Livebook.Utils.Time.time_ago_in_words()
|
||||
time_words <> " ago"
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("search", %{"search" => search}, socket) do
|
||||
{:noreply, assign(socket, search: search, limit: @initial_limit) |> assign_matching_entries()}
|
||||
|
|
|
@ -2,86 +2,64 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
use LivebookWeb, :live_component
|
||||
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex flex-col relative"
|
||||
data-element="cell"
|
||||
id="cell-<%= @cell_view.id %>"
|
||||
id={"cell-#{@cell_view.id}"}
|
||||
phx-hook="Cell"
|
||||
data-cell-id="<%= @cell_view.id %>"
|
||||
data-type="<%= @cell_view.type %>"
|
||||
data-session-path="<%= Routes.session_path(@socket, :page, @session_id) %>">
|
||||
<%= render_cell_content(assigns) %>
|
||||
data-cell-id={@cell_view.id}
|
||||
data-type={@cell_view.type}
|
||||
data-session-path={Routes.session_path(@socket, :page, @session_id)}>
|
||||
<%= render_cell(assigns) %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def render_cell_content(%{cell_view: %{type: :markdown}} = assigns) do
|
||||
~L"""
|
||||
def render_cell(%{cell_view: %{type: :markdown}} = assigns) do
|
||||
~H"""
|
||||
<div class="mb-1 flex items-center justify-end">
|
||||
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
|
||||
<%= render_cell_anchor_link(assigns) %>
|
||||
<.cell_link_button cell_id={@cell_view.id} />
|
||||
<span class="tooltip top" aria-label="Edit content" data-element="enable-insert-mode-button">
|
||||
<button class="icon-button">
|
||||
<%= remix_icon("pencil-line", class: "text-xl") %>
|
||||
<.remix_icon icon="pencil-line" class="text-xl" />
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Insert image" data-element="insert-image-button">
|
||||
<%= live_patch to: Routes.session_path(@socket, :cell_upload, @session_id, @cell_view.id),
|
||||
class: "icon-button" do %>
|
||||
<%= remix_icon("image-add-line", class: "text-xl") %>
|
||||
<.remix_icon icon="image-add-line" class="text-xl" />
|
||||
<% end %>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Move up">
|
||||
<button class="icon-button"
|
||||
phx-click="move_cell"
|
||||
phx-value-cell_id="<%= @cell_view.id %>"
|
||||
phx-value-offset="-1">
|
||||
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Move down">
|
||||
<button class="icon-button"
|
||||
phx-click="move_cell"
|
||||
phx-value-cell_id="<%= @cell_view.id %>"
|
||||
phx-value-offset="1">
|
||||
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Delete">
|
||||
<button class="icon-button"
|
||||
phx-click="delete_cell"
|
||||
phx-value-cell_id="<%= @cell_view.id %>">
|
||||
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
|
||||
</button>
|
||||
</span>
|
||||
<.move_cell_up_button cell_id={@cell_view.id} />
|
||||
<.move_cell_down_button cell_id={@cell_view.id} />
|
||||
<.delete_cell_button cell_id={@cell_view.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex relative">
|
||||
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
|
||||
<.cell_body>
|
||||
<div class="pb-4" data-element="editor-box">
|
||||
<.editor cell_view={@cell_view} />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="pb-4" data-element="editor-box">
|
||||
<%= render_editor(assigns) %>
|
||||
</div>
|
||||
|
||||
<div class="markdown" data-element="markdown-container" id="markdown-container-<%= @cell_view.id %>" phx-update="ignore">
|
||||
<%= render_content_placeholder("bg-gray-200", @cell_view.empty?) %>
|
||||
</div>
|
||||
<div class="markdown"
|
||||
data-element="markdown-container"
|
||||
id={"markdown-container-#{@cell_view.id}"}
|
||||
phx-update="ignore">
|
||||
<.content_placeholder bg_class="bg-gray-200" empty={@cell_view.empty?} />
|
||||
</div>
|
||||
</div>
|
||||
</.cell_body>
|
||||
"""
|
||||
end
|
||||
|
||||
def render_cell_content(%{cell_view: %{type: :elixir}} = assigns) do
|
||||
~L"""
|
||||
def render_cell(%{cell_view: %{type: :elixir}} = assigns) do
|
||||
~H"""
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions" data-primary>
|
||||
<%= if @cell_view.evaluation_status == :ready do %>
|
||||
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
|
||||
phx-click="queue_cell_evaluation"
|
||||
phx-value-cell_id="<%= @cell_view.id %>">
|
||||
<%= remix_icon("play-circle-fill", class: "text-xl") %>
|
||||
phx-value-cell_id={@cell_view.id}>
|
||||
<.remix_icon icon="play-circle-fill" class="text-xl" />
|
||||
<span class="text-sm font-medium">
|
||||
<%= if(@cell_view.validity_status == :evaluated, do: "Reevaluate", else: "Evaluate") %>
|
||||
</span>
|
||||
|
@ -89,8 +67,8 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
<% else %>
|
||||
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
|
||||
phx-click="cancel_cell_evaluation"
|
||||
phx-value-cell_id="<%= @cell_view.id %>">
|
||||
<%= remix_icon("stop-circle-fill", class: "text-xl") %>
|
||||
phx-value-cell_id={@cell_view.id}>
|
||||
<.remix_icon icon="stop-circle-fill" class="text-xl" />
|
||||
<span class="text-sm font-medium">
|
||||
Stop
|
||||
</span>
|
||||
|
@ -98,344 +76,255 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
<% end %>
|
||||
</div>
|
||||
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
|
||||
<%= render_cell_anchor_link(assigns) %>
|
||||
<span class="tooltip top" aria-label="Cell settings">
|
||||
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_view.id), class: "icon-button" do %>
|
||||
<%= remix_icon("list-settings-line", class: "text-xl") %>
|
||||
<% end %>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Move up">
|
||||
<button class="icon-button"
|
||||
phx-click="move_cell"
|
||||
phx-value-cell_id="<%= @cell_view.id %>"
|
||||
phx-value-offset="-1">
|
||||
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Move down">
|
||||
<button class="icon-button"
|
||||
phx-click="move_cell"
|
||||
phx-value-cell_id="<%= @cell_view.id %>"
|
||||
phx-value-offset="1">
|
||||
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Delete">
|
||||
<button class="icon-button"
|
||||
phx-click="delete_cell"
|
||||
phx-value-cell_id="<%= @cell_view.id %>">
|
||||
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
|
||||
</button>
|
||||
</span>
|
||||
<.cell_link_button cell_id={@cell_view.id} />
|
||||
<.cell_settings_button cell_id={@cell_view.id} socket={@socket} session_id={@session_id} />
|
||||
<.move_cell_up_button cell_id={@cell_view.id} />
|
||||
<.move_cell_down_button cell_id={@cell_view.id} />
|
||||
<.delete_cell_button cell_id={@cell_view.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.cell_body>
|
||||
<.editor cell_view={@cell_view} />
|
||||
|
||||
<%= if @cell_view.outputs != [] do %>
|
||||
<div class="mt-2">
|
||||
<.outputs cell_view={@cell_view} socket={@socket} />
|
||||
</div>
|
||||
<% end %>
|
||||
</.cell_body>
|
||||
"""
|
||||
end
|
||||
|
||||
def render_cell(%{cell_view: %{type: :input}} = assigns) do
|
||||
~H"""
|
||||
<div class="mb-1 flex items-center justify-end">
|
||||
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
|
||||
<.cell_link_button cell_id={@cell_view.id} />
|
||||
<.cell_settings_button cell_id={@cell_view.id} socket={@socket} session_id={@session_id} />
|
||||
<.move_cell_up_button cell_id={@cell_view.id} />
|
||||
<.move_cell_down_button cell_id={@cell_view.id} />
|
||||
<.delete_cell_button cell_id={@cell_view.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.cell_body>
|
||||
<form phx-change="set_cell_value" phx-submit="queue_bound_cells_evaluation">
|
||||
<input type="hidden" name="cell_id" value={@cell_view.id} />
|
||||
<div class="input-label">
|
||||
<%= @cell_view.name %>
|
||||
</div>
|
||||
|
||||
<%= if @cell_view.input_type == :textarea do %>
|
||||
<textarea
|
||||
data-element="input"
|
||||
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
|
||||
name="value"
|
||||
spellcheck="false"
|
||||
tabindex="-1"><%= [?\n, @cell_view.value] %></textarea>
|
||||
<% else %>
|
||||
<input type={html_input_type(@cell_view.input_type)}
|
||||
data-element="input"
|
||||
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
|
||||
name="value"
|
||||
value={@cell_view.value}
|
||||
phx-debounce="300"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
tabindex="-1" />
|
||||
<% end %>
|
||||
|
||||
<%= if @cell_view.error do %>
|
||||
<div class="input-error">
|
||||
<%= String.capitalize(@cell_view.error) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</form>
|
||||
</.cell_body>
|
||||
"""
|
||||
end
|
||||
|
||||
defp cell_body(assigns) do
|
||||
~H"""
|
||||
<div class="flex relative">
|
||||
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<%= render_editor(assigns) %>
|
||||
|
||||
<%= if @cell_view.outputs != [] do %>
|
||||
<div class="mt-2">
|
||||
<%= render_outputs(assigns, @socket) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render_block(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def render_cell_content(%{cell_view: %{type: :input}} = assigns) do
|
||||
~L"""
|
||||
<div class="mb-1 flex items-center justify-end">
|
||||
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
|
||||
<%= render_cell_anchor_link(assigns) %>
|
||||
<span class="tooltip top" aria-label="Cell settings">
|
||||
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_view.id), class: "icon-button" do %>
|
||||
<%= remix_icon("list-settings-line", class: "text-xl") %>
|
||||
<% end %>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Move up">
|
||||
<button class="icon-button"
|
||||
phx-click="move_cell"
|
||||
phx-value-cell_id="<%= @cell_view.id %>"
|
||||
phx-value-offset="-1">
|
||||
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Move down">
|
||||
<button class="icon-button"
|
||||
phx-click="move_cell"
|
||||
phx-value-cell_id="<%= @cell_view.id %>"
|
||||
phx-value-offset="1">
|
||||
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Delete">
|
||||
<button class="icon-button"
|
||||
phx-click="delete_cell"
|
||||
phx-value-cell_id="<%= @cell_view.id %>">
|
||||
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex relative">
|
||||
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
|
||||
</div>
|
||||
<div>
|
||||
<form phx-change="set_cell_value" phx-submit="queue_bound_cells_evaluation">
|
||||
<input type="hidden" name="cell_id" value="<%= @cell_view.id %>" />
|
||||
<div class="input-label">
|
||||
<%= @cell_view.name %>
|
||||
</div>
|
||||
|
||||
<%= if (@cell_view.input_type == :textarea) do %>
|
||||
<textarea
|
||||
data-element="input"
|
||||
class="input <%= if(@cell_view.error, do: "input--error") %>"
|
||||
name="value"
|
||||
spellcheck="false"
|
||||
tabindex="-1"><%= [?\n, @cell_view.value] %></textarea>
|
||||
<% else %>
|
||||
<input type="<%= html_input_type(@cell_view.input_type) %>"
|
||||
data-element="input"
|
||||
class="input <%= if(@cell_view.error, do: "input--error") %>"
|
||||
name="value"
|
||||
value="<%= @cell_view.value %>"
|
||||
phx-debounce="300"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
tabindex="-1" />
|
||||
<% end %>
|
||||
|
||||
<%= if @cell_view.error do %>
|
||||
<div class="input-error">
|
||||
<%= String.capitalize(@cell_view.error) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
defp cell_link_button(assigns) do
|
||||
~H"""
|
||||
<span class="tooltip top" aria-label="Link">
|
||||
<a href={"#cell-#{@cell_id}"} class="icon-button">
|
||||
<.remix_icon icon="link" class="text-xl" />
|
||||
</a>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_editor(assigns) do
|
||||
~L"""
|
||||
defp cell_settings_button(assigns) do
|
||||
~H"""
|
||||
<span class="tooltip top" aria-label="Cell settings">
|
||||
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_id), class: "icon-button" do %>
|
||||
<.remix_icon icon="list-settings-line" class="text-xl" />
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp move_cell_up_button(assigns) do
|
||||
~H"""
|
||||
<span class="tooltip top" aria-label="Move up">
|
||||
<button class="icon-button"
|
||||
phx-click="move_cell"
|
||||
phx-value-cell_id={@cell_id}
|
||||
phx-value-offset="-1">
|
||||
<.remix_icon icon="arrow-up-s-line" class="text-xl" />
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp move_cell_down_button(assigns) do
|
||||
~H"""
|
||||
<span class="tooltip top" aria-label="Move down">
|
||||
<button class="icon-button"
|
||||
phx-click="move_cell"
|
||||
phx-value-cell_id={@cell_id}
|
||||
phx-value-offset="1">
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp delete_cell_button(assigns) do
|
||||
~H"""
|
||||
<span class="tooltip top" aria-label="Delete">
|
||||
<button class="icon-button"
|
||||
phx-click="delete_cell"
|
||||
phx-value-cell_id={@cell_id}>
|
||||
<.remix_icon icon="delete-bin-6-line" class="text-xl" />
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp editor(assigns) do
|
||||
~H"""
|
||||
<div class="py-3 rounded-lg bg-editor relative">
|
||||
<div
|
||||
id="editor-container-<%= @cell_view.id %>"
|
||||
id={"editor-container-#{@cell_view.id}"}
|
||||
data-element="editor-container"
|
||||
phx-update="ignore">
|
||||
<div class="px-8">
|
||||
<%= render_content_placeholder("bg-gray-500", @cell_view.empty?) %>
|
||||
<.content_placeholder bg_class="bg-gray-500" empty={@cell_view.empty?} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if @cell_view.type == :elixir do %>
|
||||
<div class="absolute bottom-2 right-2">
|
||||
<%= render_cell_status(
|
||||
@cell_view.validity_status,
|
||||
@cell_view.evaluation_status,
|
||||
@cell_view.evaluation_time_ms,
|
||||
"cell-#{@cell_view.id}-evaluation#{@cell_view.number_of_evaluations}"
|
||||
) %>
|
||||
<.cell_status cell_view={@cell_view} />
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_anchor_link(assigns) do
|
||||
~L"""
|
||||
<span class="tooltip top" aria-label="Link">
|
||||
<a href="#cell-<%= @cell_view.id %>" class="icon-button">
|
||||
<%= remix_icon("link", class: "text-xl") %>
|
||||
</a>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
# The whole page has to load and then hooks are mounded.
|
||||
# There may be a tiny delay before the markdown is rendered
|
||||
# or editors are mounted, so show neat placeholders immediately.
|
||||
|
||||
defp render_content_placeholder(_bg_class, true = _empty) do
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="h-4"></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_content_placeholder(bg_class, false = _empty) do
|
||||
assigns = %{bg_class: bg_class}
|
||||
|
||||
~L"""
|
||||
<div class="max-w-2xl w-full animate-pulse">
|
||||
<div class="flex-1 space-y-4">
|
||||
<div class="h-4 <%= @bg_class %> rounded-lg w-3/4"></div>
|
||||
<div class="h-4 <%= @bg_class %> rounded-lg"></div>
|
||||
<div class="h-4 <%= @bg_class %> rounded-lg w-5/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_outputs(assigns, socket) do
|
||||
~L"""
|
||||
<div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200">
|
||||
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
|
||||
<div class="p-4 max-w-full overflow-y-auto tiny-scrollbar">
|
||||
<%= render_output(socket, output, "cell-#{@cell_view.id}-output#{index}") %>
|
||||
defp content_placeholder(assigns) do
|
||||
~H"""
|
||||
<%= if @empty do %>
|
||||
<div class="h-4"></div>
|
||||
<% else %>
|
||||
<div class="max-w-2xl w-full animate-pulse">
|
||||
<div class="flex-1 space-y-4">
|
||||
<div class={"#{@bg_class} h-4 rounded-lg w-3/4"}></div>
|
||||
<div class={"#{@bg_class} h-4 rounded-lg"}></div>
|
||||
<div class={"#{@bg_class} h-4 rounded-lg w-5/6"}></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output(_socket, text, id) when is_binary(text) do
|
||||
# Captured output usually has a trailing newline that we can ignore,
|
||||
# because each line is itself an HTML block anyway.
|
||||
text = String.replace_suffix(text, "\n", "")
|
||||
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: true)
|
||||
end
|
||||
|
||||
defp render_output(_socket, {:text, text}, id) do
|
||||
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: false)
|
||||
end
|
||||
|
||||
defp render_output(_socket, {:markdown, markdown}, id) do
|
||||
live_component(LivebookWeb.Output.MarkdownComponent, id: id, content: markdown)
|
||||
end
|
||||
|
||||
defp render_output(_socket, {:image, content, mime_type}, id) do
|
||||
live_component(LivebookWeb.Output.ImageComponent,
|
||||
id: id,
|
||||
content: content,
|
||||
mime_type: mime_type
|
||||
)
|
||||
end
|
||||
|
||||
defp render_output(_socket, {:vega_lite_static, spec}, id) do
|
||||
live_component(LivebookWeb.Output.VegaLiteStaticComponent, id: id, spec: spec)
|
||||
end
|
||||
|
||||
defp render_output(socket, {:vega_lite_dynamic, pid}, id) do
|
||||
live_render(socket, LivebookWeb.Output.VegaLiteDynamicLive,
|
||||
id: id,
|
||||
session: %{"id" => id, "pid" => pid}
|
||||
)
|
||||
end
|
||||
|
||||
defp render_output(socket, {:table_dynamic, pid}, id) do
|
||||
live_render(socket, LivebookWeb.Output.TableDynamicLive,
|
||||
id: id,
|
||||
session: %{"id" => id, "pid" => pid}
|
||||
)
|
||||
end
|
||||
|
||||
defp render_output(_socket, {:error, formatted, :runtime_restart_required}, _id) do
|
||||
assigns = %{formatted: formatted}
|
||||
|
||||
~L"""
|
||||
<div class="flex flex-col space-y-4">
|
||||
<%= render_error_message_output(@formatted) %>
|
||||
<div>
|
||||
<button class="button button-gray" phx-click="restart_runtime">
|
||||
Restart runtime
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output(_socket, {:error, formatted, _type}, _id) do
|
||||
render_error_message_output(formatted)
|
||||
end
|
||||
|
||||
defp render_output(_socket, output, _id) do
|
||||
render_error_message_output("""
|
||||
Unknown output format: #{inspect(output)}. If you're using Kino,
|
||||
you may want to update Kino and Livebook to the latest version.
|
||||
""")
|
||||
end
|
||||
|
||||
defp render_error_message_output(message) do
|
||||
assigns = %{message: message}
|
||||
|
||||
~L"""
|
||||
<div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @message %></div>
|
||||
defp cell_status(%{cell_view: %{evaluation_status: :evaluating}} = assigns) do
|
||||
~H"""
|
||||
<.status_indicator circle_class="bg-blue-500" animated_circle_class="bg-blue-400" change_indicator={true}>
|
||||
<span class="font-mono"
|
||||
id={"cell-timer-#{@cell_view.id}-evaluation-#{@cell_view.number_of_evaluations}"}
|
||||
phx-hook="Timer"
|
||||
phx-update="ignore">
|
||||
</span>
|
||||
</.status_indicator>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(cell_view, evaluation_status, evaluation_time_ms, evaluation_id)
|
||||
|
||||
defp render_cell_status(_, :evaluating, _, evaluation_id) do
|
||||
timer =
|
||||
content_tag(:span, nil,
|
||||
phx_hook: "Timer",
|
||||
# Make sure each evaluation gets its own timer
|
||||
id: "#{evaluation_id}-timer",
|
||||
phx_update: "ignore",
|
||||
class: "font-mono"
|
||||
)
|
||||
|
||||
render_status_indicator(timer, "bg-blue-500",
|
||||
animated_circle_class: "bg-blue-400",
|
||||
change_indicator: true
|
||||
)
|
||||
defp cell_status(%{cell_view: %{evaluation_status: :queued}} = assigns) do
|
||||
~H"""
|
||||
<.status_indicator circle_class="bg-gray-500" animated_circle_class="bg-gray-400">
|
||||
Queued
|
||||
</.status_indicator>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(_, :queued, _, _) do
|
||||
render_status_indicator("Queued", "bg-gray-500", animated_circle_class: "bg-gray-400")
|
||||
defp cell_status(%{cell_view: %{validity_status: :evaluated}} = assigns) do
|
||||
~H"""
|
||||
<.status_indicator
|
||||
circle_class="bg-green-400"
|
||||
change_indicator={true}
|
||||
tooltip={evaluated_label(@cell_view.evaluation_time_ms)}>
|
||||
Evaluated
|
||||
</.status_indicator>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(:evaluated, _, evaluation_time_ms, _) do
|
||||
render_status_indicator("Evaluated", "bg-green-400",
|
||||
change_indicator: true,
|
||||
tooltip: evaluated_label(evaluation_time_ms)
|
||||
)
|
||||
defp cell_status(%{cell_view: %{validity_status: :stale}} = assigns) do
|
||||
~H"""
|
||||
<.status_indicator circle_class="bg-yellow-200" change_indicator={true}>
|
||||
Stale
|
||||
</.status_indicator>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(:stale, _, evaluation_time_ms, _) do
|
||||
render_status_indicator("Stale", "bg-yellow-200",
|
||||
change_indicator: true,
|
||||
tooltip: evaluated_label(evaluation_time_ms)
|
||||
)
|
||||
defp cell_status(%{cell_view: %{validity_status: :aborted}} = assigns) do
|
||||
~H"""
|
||||
<.status_indicator circle_class="bg-red-400">
|
||||
Aborted
|
||||
</.status_indicator>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(:aborted, _, _, _) do
|
||||
render_status_indicator("Aborted", "bg-red-400")
|
||||
end
|
||||
defp cell_status(assigns), do: ~H""
|
||||
|
||||
defp render_cell_status(_, _, _, _), do: nil
|
||||
defp status_indicator(assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign_new(:animated_circle_class, fn -> nil end)
|
||||
|> assign_new(:change_indicator, fn -> false end)
|
||||
|> assign_new(:tooltip, fn -> nil end)
|
||||
|
||||
defp render_status_indicator(element, circle_class, opts \\ []) do
|
||||
assigns = %{
|
||||
element: element,
|
||||
circle_class: circle_class,
|
||||
animated_circle_class: Keyword.get(opts, :animated_circle_class),
|
||||
change_indicator: Keyword.get(opts, :change_indicator, false),
|
||||
tooltip: Keyword.get(opts, :tooltip)
|
||||
}
|
||||
|
||||
~L"""
|
||||
<div class="<%= if(@tooltip, do: "tooltip") %> bottom distant-medium" aria-label="<%= @tooltip %>">
|
||||
~H"""
|
||||
<div class={"#{if(@tooltip, do: "tooltip")} bottom distant-medium"} aria-label={@tooltip}>
|
||||
<div class="flex items-center space-x-1">
|
||||
<div class="flex text-xs text-gray-400">
|
||||
<%= @element %>
|
||||
<%= render_block(@inner_block) %>
|
||||
<%= if @change_indicator do %>
|
||||
<span data-element="change-indicator">*</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<span class="flex relative h-3 w-3">
|
||||
<%= if @animated_circle_class do %>
|
||||
<span class="animate-ping absolute inline-flex h-3 w-3 rounded-full <%= @animated_circle_class %> opacity-75"></span>
|
||||
<span class={"#{@animated_circle_class} animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75"}></span>
|
||||
<% end %>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 <%= @circle_class %>"></span>
|
||||
<span class={"#{@circle_class} relative inline-flex rounded-full h-3 w-3"}></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -456,6 +345,95 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
|
||||
defp evaluated_label(_time_ms), do: nil
|
||||
|
||||
# Outputs
|
||||
|
||||
defp outputs(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200">
|
||||
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
|
||||
<div class="p-4 max-w-full overflow-y-auto tiny-scrollbar">
|
||||
<%= render_output(output, %{id: "cell-#{@cell_view.id}-output#{index}", socket: @socket}) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output(text, %{id: id}) when is_binary(text) do
|
||||
# Captured output usually has a trailing newline that we can ignore,
|
||||
# because each line is itself an HTML block anyway.
|
||||
text = String.replace_suffix(text, "\n", "")
|
||||
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: true)
|
||||
end
|
||||
|
||||
defp render_output({:text, text}, %{id: id}) do
|
||||
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: false)
|
||||
end
|
||||
|
||||
defp render_output({:markdown, markdown}, %{id: id}) do
|
||||
live_component(LivebookWeb.Output.MarkdownComponent, id: id, content: markdown)
|
||||
end
|
||||
|
||||
defp render_output({:image, content, mime_type}, %{id: id}) do
|
||||
live_component(LivebookWeb.Output.ImageComponent,
|
||||
id: id,
|
||||
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({: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({:error, formatted, :runtime_restart_required}, %{}) do
|
||||
assigns = %{formatted: formatted}
|
||||
|
||||
~H"""
|
||||
<div class="flex flex-col space-y-4">
|
||||
<%= render_error_message_output(@formatted) %>
|
||||
<div>
|
||||
<button class="button button-gray" phx-click="restart_runtime">
|
||||
Restart runtime
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output({:error, formatted, _type}, %{}) do
|
||||
render_error_message_output(formatted)
|
||||
end
|
||||
|
||||
defp render_output(output, %{}) do
|
||||
render_error_message_output("""
|
||||
Unknown output format: #{inspect(output)}. If you're using Kino,
|
||||
you may want to update Kino and Livebook to the latest version.
|
||||
""")
|
||||
end
|
||||
|
||||
defp render_error_message_output(message) do
|
||||
assigns = %{message: message}
|
||||
|
||||
~H"""
|
||||
<div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @message %></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp html_input_type(:password), do: "password"
|
||||
defp html_input_type(:number), do: "number"
|
||||
defp html_input_type(:color), do: "color"
|
||||
|
|
|
@ -10,7 +10,7 @@ defmodule LivebookWeb.SessionLive.CellUploadComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 pb-4 flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Insert image
|
||||
|
@ -28,31 +28,32 @@ defmodule LivebookWeb.SessionLive.CellUploadComponent do
|
|||
</div>
|
||||
<div class="w-full h-2 rounded-lg bg-blue-200">
|
||||
<div class="h-full rounded-lg bg-blue-600 transition-all ease-out duration-1000"
|
||||
style="width: <%= entry.progress %>%">
|
||||
style={"width: #{entry.progress}%"}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<form phx-submit="save" phx-change="validate" phx-target="<%= @myself %>">
|
||||
<form phx-submit="save" phx-change="validate" phx-target={@myself}>
|
||||
<div class="w-full flex space-x-2">
|
||||
<div>
|
||||
<label>
|
||||
<%= live_file_input @uploads.cell_image, class: "hidden" %>
|
||||
<div class="cursor-pointer button button-gray button-square-icon">
|
||||
<%= remix_icon("folder-upload-line") %>
|
||||
<.remix_icon icon="folder-upload-line" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<input class="input" name="name" placeholder="Name" autocomplete="off" value="<%= @name %>" />
|
||||
<input class="input" name="name" value={@name} placeholder="Name" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
|
||||
<%= content_tag :button, "Upload",
|
||||
type: :submit,
|
||||
class: "button button-blue",
|
||||
disabled: @uploads.cell_image.entries == [] or @name == "" %>
|
||||
<button class="button button-blue"
|
||||
type="submit"
|
||||
disabled={@uploads.cell_image.entries == [] or @name == ""}>
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule LivebookWeb.SessionLive.DeleteSectionComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 pb-4 flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Delete section
|
||||
|
@ -12,15 +12,19 @@ defmodule LivebookWeb.SessionLive.DeleteSectionComponent do
|
|||
Are you sure you want to delete this section -
|
||||
<span class="font-semibold">“<%= @section.name %>”</span>?
|
||||
</p>
|
||||
<form phx-submit="delete" phx-target="<%= @myself %>">
|
||||
<form phx-submit="delete" phx-target={@myself}>
|
||||
<h3 class="mb-1 text-lg font-semibold text-gray-800">
|
||||
Options
|
||||
</h3>
|
||||
<%# If there is no previous section, all cells need to be deleted %>
|
||||
<%= render_switch("delete_cells", @is_first, "Delete all cells in this section", disabled: @is_first) %>
|
||||
<.switch_checkbox
|
||||
name="delete_cells"
|
||||
label="Delete all cells in this section"
|
||||
checked={@is_first}
|
||||
disabled={@is_first} />
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<button type="submit" class="button button-red">
|
||||
<%= remix_icon("delete-bin-6-line", class: "align-middle mr-1") %>
|
||||
<.remix_icon icon="delete-bin-6-line" class="align-middle mr-1" />
|
||||
Delete
|
||||
</button>
|
||||
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
|
||||
|
|
|
@ -15,14 +15,17 @@ defmodule LivebookWeb.SessionLive.ElixirCellSettingsComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 pb-4 flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Cell settings
|
||||
</h3>
|
||||
<form phx-submit="save" phx-target="<%= @myself %>">
|
||||
<form phx-submit="save" phx-target={@myself}>
|
||||
<div class="w-full flex-col space-y-6">
|
||||
<%= render_switch("disable_formatting", @disable_formatting, "Disable code formatting (when saving to file)") %>
|
||||
<.switch_checkbox
|
||||
name="disable_formatting"
|
||||
label="Disable code formatting (when saving to file)"
|
||||
checked={@disable_formatting} />
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
|
||||
|
|
|
@ -15,7 +15,7 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex-col space-y-5">
|
||||
<%= if @error_message do %>
|
||||
<div class="error-box">
|
||||
|
|
|
@ -10,7 +10,7 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex-col space-y-5">
|
||||
<p class="text-gray-700">
|
||||
Run the notebook code within the Livebook node itself.
|
||||
|
|
|
@ -3,21 +3,21 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex flex-col space-y-2 items-center" data-element="notebook-indicators">
|
||||
<%= if @data_view.path do %>
|
||||
<%= if @data_view.dirty do %>
|
||||
<%= if @path do %>
|
||||
<%= if @dirty do %>
|
||||
<span class="tooltip left" aria-label="Autosave pending">
|
||||
<%= live_patch to: Routes.session_path(@socket, :file_settings, @session_id),
|
||||
class: "icon-button icon-outlined-button border-blue-400 hover:bg-blue-50 focus:bg-blue-50" do %>
|
||||
<%= remix_icon("save-line", class: "text-xl text-blue-500") %>
|
||||
<.remix_icon icon="save-line" class="text-xl text-blue-500" />
|
||||
<% end %>
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="tooltip left" aria-label="Notebook saved">
|
||||
<%= live_patch to: Routes.session_path(@socket, :file_settings, @session_id),
|
||||
class: "icon-button icon-outlined-button border-green-300 hover:bg-green-50 focus:bg-green-50" do %>
|
||||
<%= remix_icon("save-line", class: "text-xl text-green-400") %>
|
||||
<.remix_icon icon="save-line" class="text-xl text-green-400" />
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
@ -25,18 +25,20 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
|
|||
<span class="tooltip left" aria-label="Choose a file to save the notebook">
|
||||
<%= live_patch to: Routes.session_path(@socket, :file_settings, @session_id),
|
||||
class: "icon-button icon-outlined-button border-gray-200 hover:bg-gray-100 focus:bg-gray-100" do %>
|
||||
<%= remix_icon("save-line", class: "text-xl text-gray-400") %>
|
||||
<.remix_icon icon="save-line" class="text-xl text-gray-400" />
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<%= if @data_view.runtime do %>
|
||||
<%= render_global_evaluation_status(@data_view.global_evaluation_status) %>
|
||||
<%= if @runtime do %>
|
||||
<.global_evaluation_status
|
||||
status={elem(@global_evaluation_status, 0)}
|
||||
cell_id={elem(@global_evaluation_status, 1)} />
|
||||
<% else %>
|
||||
<span class="tooltip left" aria-label="Choose a runtime to run the notebook in">
|
||||
<%= live_patch to: Routes.session_path(@socket, :runtime_settings, @session_id),
|
||||
class: "icon-button icon-outlined-button border-gray-200 hover:bg-gray-100 focus:bg-gray-100" do %>
|
||||
<%= remix_icon("loader-3-line", class: "text-xl text-gray-400") %>
|
||||
<.remix_icon icon="loader-3-line" class="text-xl text-gray-400" />
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
@ -51,55 +53,47 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp render_global_evaluation_status({:evaluating, cell_id}) do
|
||||
assigns = %{cell_id: cell_id}
|
||||
|
||||
~L"""
|
||||
defp global_evaluation_status(%{status: :evaluating} = assigns) do
|
||||
~H"""
|
||||
<span class="tooltip left" aria-label="Go to evaluating cell">
|
||||
<button class="icon-button icon-outlined-button border-blue-400 hover:bg-blue-50 focus:bg-blue-50"
|
||||
data-element="focus-cell-button"
|
||||
data-target="<%= @cell_id %>">
|
||||
<%= remix_icon("loader-3-line", class: "text-xl text-blue-500 animate-spin") %>
|
||||
data-target={@cell_id}>
|
||||
<.remix_icon icon="loader-3-line" class="text-xl text-blue-500 animate-spin" />
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_global_evaluation_status({:evaluated, cell_id}) do
|
||||
assigns = %{cell_id: cell_id}
|
||||
|
||||
~L"""
|
||||
defp global_evaluation_status(%{status: :evaluated} = assigns) do
|
||||
~H"""
|
||||
<span class="tooltip left" aria-label="Go to last evaluated cell">
|
||||
<button class="icon-button icon-outlined-button border-green-300 hover:bg-green-50 focus:bg-green-50"
|
||||
data-element="focus-cell-button"
|
||||
data-target="<%= @cell_id %>">
|
||||
<%= remix_icon("loader-3-line", class: "text-xl text-green-400") %>
|
||||
data-target={@cell_id}>
|
||||
<.remix_icon icon="loader-3-line" class="text-xl text-green-400" />
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_global_evaluation_status({:stale, cell_id}) do
|
||||
assigns = %{cell_id: cell_id}
|
||||
|
||||
~L"""
|
||||
defp global_evaluation_status(%{status: :stale} = assigns) do
|
||||
~H"""
|
||||
<span class="tooltip left" aria-label="Go to first stale cell">
|
||||
<button class="icon-button icon-outlined-button border-yellow-200 hover:bg-yellow-50 focus:bg-yellow-50"
|
||||
data-element="focus-cell-button"
|
||||
data-target="<%= @cell_id %>">
|
||||
<%= remix_icon("loader-3-line", class: "text-xl text-yellow-300") %>
|
||||
data-target={@cell_id}>
|
||||
<.remix_icon icon="loader-3-line" class="text-xl text-yellow-300" />
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_global_evaluation_status({:fresh, nil}) do
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
defp global_evaluation_status(%{status: :fresh} = assigns) do
|
||||
~H"""
|
||||
<span class="tooltip left" aria-label="Ready to evaluate">
|
||||
<button class="icon-button icon-outlined-button border-gray-200 hover:bg-gray-100 focus:bg-gray-100 cursor-default">
|
||||
<%= remix_icon("loader-3-line", class: "text-xl text-gray-400") %>
|
||||
<.remix_icon icon="loader-3-line" class="text-xl text-gray-400" />
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
|
|
|
@ -19,31 +19,33 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 pb-4 flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Cell settings
|
||||
</h3>
|
||||
<form phx-submit="save" phx-change="validate" phx-target="<%= @myself %>">
|
||||
<form phx-submit="save" phx-change="validate" phx-target={@myself}>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<div>
|
||||
<div class="input-label">Type</div>
|
||||
<%= render_select("type", input_types(), @type) %>
|
||||
<.select name="type" selected={@type} options={input_types()} />
|
||||
</div>
|
||||
<div>
|
||||
<div class="input-label">Name</div>
|
||||
<input type="text" class="input" name="name" value="<%= @name %>" spellcheck="false" autocomplete="off" autofocus />
|
||||
<input type="text" class="input" name="name" value={@name} spellcheck="false" autocomplete="off" autofocus />
|
||||
</div>
|
||||
<div>
|
||||
<%= render_switch("reactive", @reactive, "Reactive (reevaluates dependent cells on change)") %>
|
||||
<.switch_checkbox
|
||||
name="reactive"
|
||||
label="Reactive (reevaluates dependent cells on change)"
|
||||
checked={@reactive} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
|
||||
<%= content_tag :button, "Save",
|
||||
type: :submit,
|
||||
class: "button button-blue",
|
||||
disabled: @name == "" %>
|
||||
<button class="button button-blue" type="submit" disabled={@name == ""}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -2,31 +2,31 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
|||
use LivebookWeb, :live_component
|
||||
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="relative top-0.5 m-0 flex justify-center" data-element="insert-buttons">
|
||||
<div class="w-full absolute z-10 <%= if(@persistent, do: "opacity-100", else: "opacity-0") %> hover:opacity-100 focus-within:opacity-100 flex space-x-2 justify-center items-center">
|
||||
<div class={"w-full absolute z-10 #{if(@persistent, do: "opacity-100", else: "opacity-0")} hover:opacity-100 focus-within:opacity-100 flex space-x-2 justify-center items-center"}>
|
||||
<button class="button button-small"
|
||||
phx-click="insert_cell"
|
||||
phx-value-type="markdown"
|
||||
phx-value-section_id="<%= @section_id %>"
|
||||
phx-value-index="<%= @insert_cell_index %>"
|
||||
phx-value-section_id={@section_id}
|
||||
phx-value-index={@insert_cell_index}
|
||||
>+ Markdown</button>
|
||||
<button class="button button-small"
|
||||
phx-click="insert_cell"
|
||||
phx-value-type="elixir"
|
||||
phx-value-section_id="<%= @section_id %>"
|
||||
phx-value-index="<%= @insert_cell_index %>"
|
||||
phx-value-section_id={@section_id}
|
||||
phx-value-index={@insert_cell_index}
|
||||
>+ Elixir</button>
|
||||
<button class="button button-small"
|
||||
phx-click="insert_cell"
|
||||
phx-value-type="input"
|
||||
phx-value-section_id="<%= @section_id %>"
|
||||
phx-value-index="<%= @insert_cell_index %>"
|
||||
phx-value-section_id={@section_id}
|
||||
phx-value-index={@insert_cell_index}
|
||||
>+ Input</button>
|
||||
<button class="button button-small"
|
||||
phx-click="insert_section_into"
|
||||
phx-value-section_id="<%= @section_id %>"
|
||||
phx-value-index="<%= @insert_cell_index %>"
|
||||
phx-value-section_id={@section_id}
|
||||
phx-value-index={@insert_cell_index}
|
||||
>+ Section</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex-col space-y-5">
|
||||
<p class="text-gray-700">
|
||||
Start a new local node in the context of a Mix project.
|
||||
|
@ -40,17 +40,16 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
|||
<%= if @status == :initial do %>
|
||||
<div class="h-full h-52">
|
||||
<%= live_component LivebookWeb.PathSelectComponent,
|
||||
id: "path_select",
|
||||
path: @path,
|
||||
extnames: [],
|
||||
running_paths: [],
|
||||
phx_target: nil,
|
||||
phx_submit: if(disabled?(@path), do: nil, else: "init") %>
|
||||
id: "path_select",
|
||||
path: @path,
|
||||
extnames: [],
|
||||
running_paths: [],
|
||||
phx_target: nil,
|
||||
phx_submit: if(disabled?(@path), do: nil, else: "init") %>
|
||||
</div>
|
||||
<%= content_tag :button, if(matching_runtime?(@current_runtime, @path), do: "Reconnect", else: "Connect"),
|
||||
class: "button button-blue",
|
||||
phx_click: "init",
|
||||
disabled: disabled?(@path) %>
|
||||
<button class="button button-blue" phx-click="init" disabled={disabled?(@path)}>
|
||||
<%= if(matching_runtime?(@current_runtime, @path), do: "Reconnect", else: "Connect") %>
|
||||
</button>
|
||||
<% end %>
|
||||
<%= if @status != :initial do %>
|
||||
<div class="markdown">
|
||||
|
@ -58,7 +57,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
|||
id="mix-standalone-init-output"
|
||||
phx-update="append"
|
||||
phx-hook="ScrollOnUpdate"
|
||||
><%= for {output, i} <- @outputs do %><span id="output-<%= i %>"><%= ansi_string_to_html(output) %></span><% end %></code></pre>
|
||||
><%= for {output, i} <- @outputs do %><span id={"output-#{i}"}><%= ansi_string_to_html(output) %></span><% end %></code></pre>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 pb-4 flex flex-col space-y-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
File
|
||||
|
@ -22,26 +22,30 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
|
|||
Specify where the notebook should be automatically persisted.
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<%= content_tag :button, "Save to file",
|
||||
class: "choice-button #{if(@path != nil, do: "active")}",
|
||||
phx_click: "set_persistence_type",
|
||||
phx_value_type: "file",
|
||||
phx_target: @myself %>
|
||||
<%= content_tag :button, "Memory only",
|
||||
class: "choice-button #{if(@path == nil, do: "active")}",
|
||||
phx_click: "set_persistence_type",
|
||||
phx_value_type: "memory",
|
||||
phx_target: @myself %>
|
||||
<.choice_button
|
||||
active={@path != nil}
|
||||
phx-click="set_persistence_type"
|
||||
phx-value-type="file"
|
||||
phx-target={@myself}>
|
||||
Save to file
|
||||
</.choice_button>
|
||||
<.choice_button
|
||||
active={@path == nil}
|
||||
phx-click="set_persistence_type"
|
||||
phx-value-type="memory"
|
||||
phx-target={@myself}>
|
||||
Memory only
|
||||
</.choice_button>
|
||||
</div>
|
||||
<%= if @path != nil do %>
|
||||
<div class="h-full h-52">
|
||||
<%= live_component LivebookWeb.PathSelectComponent,
|
||||
id: "path_select",
|
||||
path: @path,
|
||||
extnames: [LiveMarkdown.extension()],
|
||||
running_paths: @running_paths,
|
||||
phx_target: @myself,
|
||||
phx_submit: if(disabled?(@path, @current_path, @running_paths), do: nil, else: "save") %>
|
||||
id: "path_select",
|
||||
path: @path,
|
||||
extnames: [LiveMarkdown.extension()],
|
||||
running_paths: @running_paths,
|
||||
phx_target: @myself,
|
||||
phx_submit: if(disabled?(@path, @current_path, @running_paths), do: nil, else: "save") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex flex-col space-y-2">
|
||||
|
@ -51,11 +55,12 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
|
|||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<%= content_tag :button, "Save",
|
||||
class: "button button-blue mt-2",
|
||||
phx_click: "save",
|
||||
phx_target: @myself,
|
||||
disabled: disabled?(@path, @current_path, @running_paths) %>
|
||||
<button class="button button-blue mt-2"
|
||||
phx-click="save"
|
||||
phx-target={@myself}
|
||||
disabled={disabled?(@path, @current_path, @running_paths)}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 pb-4 max-w-4xl flex flex-col space-y-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Runtime
|
||||
|
@ -60,7 +60,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
|||
<button class="button button-outlined-red"
|
||||
type="button"
|
||||
phx-click="disconnect"
|
||||
phx-target="<%= @myself %>">
|
||||
phx-target={@myself}>
|
||||
Disconnect
|
||||
</button>
|
||||
<% else %>
|
||||
|
@ -70,48 +70,39 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
|||
<% end %>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<%= content_tag :button, "Elixir standalone",
|
||||
class: "choice-button #{if(@type == "elixir_standalone", do: "active")}",
|
||||
phx_click: "set_runtime_type",
|
||||
phx_value_type: "elixir_standalone",
|
||||
phx_target: @myself %>
|
||||
<%= content_tag :button, "Mix standalone",
|
||||
class: "choice-button #{if(@type == "mix_standalone", do: "active")}",
|
||||
phx_click: "set_runtime_type",
|
||||
phx_value_type: "mix_standalone",
|
||||
phx_target: @myself %>
|
||||
<%= content_tag :button, "Attached node",
|
||||
class: "choice-button #{if(@type == "attached", do: "active")}",
|
||||
phx_click: "set_runtime_type",
|
||||
phx_value_type: "attached",
|
||||
phx_target: @myself %>
|
||||
<%= content_tag :button, "Embedded",
|
||||
class: "choice-button #{if(@type == "embedded", do: "active")}",
|
||||
phx_click: "set_runtime_type",
|
||||
phx_value_type: "embedded",
|
||||
phx_target: @myself %>
|
||||
<.choice_button
|
||||
active={@type == "elixir_standalone"}
|
||||
phx-click="set_runtime_type"
|
||||
phx-value-type="elixir_standalone"
|
||||
phx-target={@myself}>
|
||||
Elixir standalone
|
||||
</.choice_button>
|
||||
<.choice_button
|
||||
active={@type == "mix_standalone"}
|
||||
phx-click="set_runtime_type"
|
||||
phx-value-type="mix_standalone"
|
||||
phx-target={@myself}>
|
||||
Mix standalone
|
||||
</.choice_button>
|
||||
<.choice_button
|
||||
active={@type == "attached"}
|
||||
phx-click="set_runtime_type"
|
||||
phx-value-type="attached"
|
||||
phx-target={@myself}>
|
||||
Attached node
|
||||
</.choice_button>
|
||||
<.choice_button
|
||||
active={@type == "embedded"}
|
||||
phx-click="set_runtime_type"
|
||||
phx-value-type="embedded"
|
||||
phx-target={@myself}>
|
||||
Embedded
|
||||
</.choice_button>
|
||||
</div>
|
||||
<div>
|
||||
<%= if @type == "elixir_standalone" do %>
|
||||
<%= live_render @socket, LivebookWeb.SessionLive.ElixirStandaloneLive,
|
||||
id: :elixir_standalone_runtime,
|
||||
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
|
||||
<% end %>
|
||||
<%= if @type == "mix_standalone" do %>
|
||||
<%= live_render @socket, LivebookWeb.SessionLive.MixStandaloneLive,
|
||||
id: :mix_standalone_runtime,
|
||||
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
|
||||
<% end %>
|
||||
<%= if @type == "attached" do %>
|
||||
<%= live_render @socket, LivebookWeb.SessionLive.AttachedLive,
|
||||
id: :attached_runtime,
|
||||
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
|
||||
<% end %>
|
||||
<%= if @type == "embedded" do %>
|
||||
<%= live_render @socket, LivebookWeb.SessionLive.EmbeddedLive,
|
||||
id: :embedded_runtime,
|
||||
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
|
||||
<% end %>
|
||||
<%= live_render @socket, live_view_for_type(@type),
|
||||
id: "runtime-config-#{@type}",
|
||||
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -128,6 +119,11 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
|||
defp runtime_type(%Runtime.Attached{}), do: "attached"
|
||||
defp runtime_type(%Runtime.Embedded{}), do: "embedded"
|
||||
|
||||
defp live_view_for_type("elixir_standalone"), do: LivebookWeb.SessionLive.ElixirStandaloneLive
|
||||
defp live_view_for_type("mix_standalone"), do: LivebookWeb.SessionLive.MixStandaloneLive
|
||||
defp live_view_for_type("attached"), do: LivebookWeb.SessionLive.AttachedLive
|
||||
defp live_view_for_type("embedded"), do: LivebookWeb.SessionLive.EmbeddedLive
|
||||
|
||||
@impl true
|
||||
def handle_event("set_runtime_type", %{"type" => type}, socket) do
|
||||
{:noreply, assign(socket, type: type)}
|
||||
|
|
|
@ -2,48 +2,47 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
use LivebookWeb, :live_component
|
||||
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div
|
||||
data-element="section"
|
||||
data-section-id="<%= @section_view.id %>">
|
||||
~H"""
|
||||
<div data-element="section" data-section-id={@section_view.id}>
|
||||
<div class="flex space-x-4 items-center" data-element="section-headline">
|
||||
<h2 class="flex-grow text-gray-800 font-semibold text-2xl px-1 -ml-1 rounded-lg border border-transparent hover:border-blue-200 focus:border-blue-300"
|
||||
<h2 class="flex-grow text-gray-800 font-semibold text-2xl px-1 -ml-1 rounded-lg border
|
||||
border-transparent hover:border-blue-200 focus:border-blue-300"
|
||||
data-element="section-name"
|
||||
id="<%= @section_view.html_id %>"
|
||||
id={@section_view.html_id}
|
||||
contenteditable
|
||||
spellcheck="false"
|
||||
phx-blur="set_section_name"
|
||||
phx-value-section_id="<%= @section_view.id %>"
|
||||
phx-value-section_id={@section_view.id}
|
||||
phx-hook="ContentEditable"
|
||||
data-update-attribute="phx-value-name"><%= @section_view.name %></h2>
|
||||
<%# ^ Note it's important there's no space between <h2> and </h2>
|
||||
because we want the content to exactly match section name. %>
|
||||
<div class="flex space-x-2 items-center" data-element="section-actions">
|
||||
<span class="tooltip top" aria-label="Link">
|
||||
<a href="#<%= @section_view.html_id %>" class="icon-button">
|
||||
<%= remix_icon("link", class: "text-xl") %>
|
||||
<a href={"##{@section_view.html_id}"} class="icon-button">
|
||||
<.remix_icon icon="link" class="text-xl" />
|
||||
</a>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Move up">
|
||||
<button class="icon-button"
|
||||
phx-click="move_section"
|
||||
phx-value-section_id="<%= @section_view.id %>"
|
||||
phx-value-section_id={@section_view.id}
|
||||
phx-value-offset="-1">
|
||||
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
|
||||
<.remix_icon icon="arrow-up-s-line" class="text-xl" />
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Move down">
|
||||
<button class="icon-button"
|
||||
phx-click="move_section"
|
||||
phx-value-section_id="<%= @section_view.id %>"
|
||||
phx-value-section_id={@section_view.id}
|
||||
phx-value-offset="1">
|
||||
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip top" aria-label="Delete">
|
||||
<%= live_patch to: Routes.session_path(@socket, :delete_section, @session_id, @section_view.id),
|
||||
class: "icon-button" do %>
|
||||
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
|
||||
<.remix_icon icon="delete-bin-6-line" class="text-xl" />
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -56,11 +55,13 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
persistent: false,
|
||||
section_id: @section_view.id,
|
||||
insert_cell_index: index %>
|
||||
|
||||
<%= live_component LivebookWeb.SessionLive.CellComponent,
|
||||
id: cell_view.id,
|
||||
session_id: @session_id,
|
||||
cell_view: cell_view %>
|
||||
<% end %>
|
||||
|
||||
<%= live_component LivebookWeb.SessionLive.InsertButtonsComponent,
|
||||
id: "#{@section_view.id}:last",
|
||||
persistent: @section_view.cell_views == [],
|
||||
|
|
|
@ -122,7 +122,7 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Keyboard shortcuts
|
||||
|
@ -134,54 +134,55 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
|
|||
the notebook and execute commands, whereas in the <span class="font-semibold">insert mode</span>
|
||||
you have editor focus and directly modify the given cell content.
|
||||
</p>
|
||||
<%= render_shortcuts_section("Navigation mode", @shortcuts.navigation_mode, @basic, @platform) %>
|
||||
<%= render_shortcuts_section("Insert mode", @shortcuts.insert_mode, @basic, @platform) %>
|
||||
<%= render_shortcuts_section("Universal", @shortcuts.universal, @basic, @platform) %>
|
||||
<.shortcuts_section title="Navigation mode" shortcuts={@shortcuts.navigation_mode} basic={@basic} platform={@platform} />
|
||||
<.shortcuts_section title="Insert mode" shortcuts={@shortcuts.insert_mode} basic={@basic} platform={@platform} />
|
||||
<.shortcuts_section title="Universal" shortcuts={@shortcuts.universal} basic={@basic} platform={@platform} />
|
||||
<div class="mt-8 flex justify-end">
|
||||
<form phx-change="settings" onsubmit="return false;" phx-target="<%= @myself %>">
|
||||
<%= render_switch("basic", @basic, "Basic view (essential shortcuts only)") %>
|
||||
<form phx-change="settings" onsubmit="return false;" phx-target={@myself}>
|
||||
<.switch_checkbox
|
||||
name="basic"
|
||||
label="Basic view (essential shortcuts only)"
|
||||
checked={@basic} />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_shortcuts_section(title, shortcuts, basic, platform) do
|
||||
defp shortcuts_section(assigns) do
|
||||
shortcuts =
|
||||
if basic do
|
||||
Enum.filter(shortcuts, & &1[:basic])
|
||||
if assigns.basic do
|
||||
Enum.filter(assigns.shortcuts, & &1[:basic])
|
||||
else
|
||||
shortcuts
|
||||
assigns.shortcuts
|
||||
end
|
||||
|
||||
{left, right} = split_in_half(shortcuts)
|
||||
assigns = %{title: title, left: left, right: right, platform: platform}
|
||||
assigns = assign(assigns, left: left, right: right)
|
||||
|
||||
~L"""
|
||||
~H"""
|
||||
<h3 class="text-lg font-medium text-gray-900 pt-4">
|
||||
<%= @title %>
|
||||
</h3>
|
||||
<div class="mt-2 flex flex-col lg:flex-row lg:space-x-4">
|
||||
<div class="lg:flex-grow">
|
||||
<%= render_shortcuts_section_table(@left, @platform) %>
|
||||
<.shortcuts_section_table shortcuts={@left} platform={@platform} />
|
||||
</div>
|
||||
<div class="lg:w-1/2">
|
||||
<%= render_shortcuts_section_table(@right, @platform) %>
|
||||
<.shortcuts_section_table shortcuts={@right} platform={@platform} />
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_shortcuts_section_table(shortcuts, platform) do
|
||||
assigns = %{shortcuts: shortcuts, platform: platform}
|
||||
|
||||
~L"""
|
||||
defp shortcuts_section_table(assigns) do
|
||||
~H"""
|
||||
<table>
|
||||
<tbody>
|
||||
<%= for shortcut <- @shortcuts do %>
|
||||
<tr>
|
||||
<td class="py-2 pr-3">
|
||||
<%= render_shortcut_seq(shortcut, @platform) %>
|
||||
<.shortcut shortcut={shortcut} platform={@platform} />
|
||||
</td>
|
||||
<td class="lg:whitespace-nowrap">
|
||||
<%= shortcut.desc %>
|
||||
|
@ -193,19 +194,24 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp render_shortcut_seq(shortcut, platform) do
|
||||
defp shortcut(%{shortcut: shortcut, platform: platform}) do
|
||||
seq = shortcut[:"seq_#{platform}"] || shortcut.seq
|
||||
press_all = Map.get(shortcut, :press_all, false)
|
||||
|
||||
joiner =
|
||||
if press_all do
|
||||
remix_icon("add-line", class: "text-lg text-gray-600")
|
||||
assigns = %{}
|
||||
|
||||
~H"""
|
||||
<.remix_icon icon="add-line" class="text-lg text-gray-600" />
|
||||
"""
|
||||
end
|
||||
|
||||
elements = Enum.map_intersperse(seq, joiner, &content_tag("kbd", &1))
|
||||
|
||||
assigns = %{elements: elements}
|
||||
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="flex space-x-1 items-center markdown">
|
||||
<%= for element <- @elements do %>
|
||||
<%= element %>
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
defmodule LivebookWeb.SidebarComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
import LivebookWeb.UserHelpers
|
||||
|
||||
# ## Attributes
|
||||
#
|
||||
# * `:items` - a list of sidebar items
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div class="w-16 flex flex-col items-center space-y-5 px-3 py-7 bg-gray-900">
|
||||
<%= for item <- @items do %>
|
||||
<%= render_item(@socket, item) %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(socket, %{type: :logo} = item) do
|
||||
assigns = %{item: item}
|
||||
|
||||
~L"""
|
||||
<%= live_patch to: Routes.home_path(socket, :page) do %>
|
||||
<img src="/images/logo.png" height="40" width="40" alt="livebook" />
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(_socket, %{type: :button} = item) do
|
||||
assigns = %{item: item}
|
||||
|
||||
~L"""
|
||||
<span class="tooltip right distant" aria-label="<%= item.label %>">
|
||||
<button class="text-2xl text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center"
|
||||
data-element="<%= item.data_element %>">
|
||||
<%= remix_icon(item.icon) %>
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(_socket, %{type: :link} = item) do
|
||||
assigns = %{item: item}
|
||||
|
||||
~L"""
|
||||
<span class="tooltip right distant" aria-label="<%= item.label %>">
|
||||
<%= live_patch to: item.path,
|
||||
class: "text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center #{if(item.active, do: "text-gray-50 bg-gray-700")}" do %>
|
||||
<%= remix_icon(item.icon, class: "text-2xl") %>
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(_socket, %{type: :break}) do
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="flex-grow"></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(_socket, %{type: :user} = item) do
|
||||
assigns = %{item: item}
|
||||
|
||||
~L"""
|
||||
<span class="tooltip right distant" aria-label="User profile">
|
||||
<%= live_patch to: item.path,
|
||||
class: "text-gray-400 rounded-xl h-8 w-8 flex items-center justify-center" do %>
|
||||
<%= render_user_avatar(item.current_user, class: "h-full w-full", text_class: "text-xs") %>
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
end
|
70
lib/livebook_web/live/sidebar_helpers.ex
Normal file
70
lib/livebook_web/live/sidebar_helpers.ex
Normal file
|
@ -0,0 +1,70 @@
|
|||
defmodule LivebookWeb.SidebarHelpers do
|
||||
use Phoenix.Component
|
||||
|
||||
import LivebookWeb.Helpers
|
||||
import LivebookWeb.UserHelpers
|
||||
|
||||
alias LivebookWeb.Router.Helpers, as: Routes
|
||||
|
||||
@doc """
|
||||
Renders sidebar container.
|
||||
|
||||
Other functions in this module render sidebar
|
||||
items of various type.
|
||||
"""
|
||||
def sidebar(assigns) do
|
||||
~H"""
|
||||
<div class="w-16 flex flex-col items-center space-y-5 px-3 py-7 bg-gray-900">
|
||||
<%= render_block(@inner_block) %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def logo_item(assigns) do
|
||||
~H"""
|
||||
<span>
|
||||
<%= live_patch to: Routes.home_path(@socket, :page) do %>
|
||||
<img src="/images/logo.png" height="40" width="40" alt="livebook" />
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
def button_item(assigns) do
|
||||
~H"""
|
||||
<span class="tooltip right distant" aria-label={@label}>
|
||||
<button class="text-2xl text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center"
|
||||
data-element={@data_element}>
|
||||
<.remix_icon icon={@icon} />
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
def link_item(assigns) do
|
||||
~H"""
|
||||
<span class="tooltip right distant" aria-label={@label}>
|
||||
<%= live_patch to: @path,
|
||||
class: "text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center #{if(@active, do: "text-gray-50 bg-gray-700")}" do %>
|
||||
<.remix_icon icon={@icon} class="text-2xl" />
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
def break_item(assigns) do
|
||||
~H"""
|
||||
<div class="flex-grow"></div>
|
||||
"""
|
||||
end
|
||||
|
||||
def user_item(assigns) do
|
||||
~H"""
|
||||
<span class="tooltip right distant" aria-label="User profile">
|
||||
<%= live_patch to: @path, class: "text-gray-400 rounded-xl h-8 w-8 flex items-center justify-center" do %>
|
||||
<.user_avatar user={@current_user} text_class="text-xs" />
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -19,20 +19,20 @@ defmodule LivebookWeb.UserComponent do
|
|||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
User profile
|
||||
</h3>
|
||||
<div class="flex justify-center">
|
||||
<%= render_user_avatar(@preview_user, class: "h-20 w-20", text_class: "text-3xl") %>
|
||||
<.user_avatar user={@preview_user} class="h-20 w-20" text_class="text-3xl" />
|
||||
</div>
|
||||
<%= f = form_for :data, "#",
|
||||
id: "user_form",
|
||||
phx_target: @myself,
|
||||
phx_submit: "save",
|
||||
phx_change: "validate",
|
||||
phx_hook: "UserForm" %>
|
||||
<.form let={f} for={:data}
|
||||
phx-submit="save"
|
||||
phx-change="validate"
|
||||
phx-target={@myself}
|
||||
id="user_form"
|
||||
phx-hook="UserForm">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<div>
|
||||
<div class="input-label">Display name</div>
|
||||
|
@ -42,30 +42,32 @@ defmodule LivebookWeb.UserComponent do
|
|||
<div class="input-label">Cursor color</div>
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div class="border-[3px] rounded-lg p-1 flex justify-center items-center"
|
||||
style="border-color: <%= @preview_user.hex_color %>">
|
||||
style={"border-color: #{@preview_user.hex_color}"}>
|
||||
<div class="rounded h-5 w-5"
|
||||
style="background-color: <%= @preview_user.hex_color %>">
|
||||
style={"background-color: #{@preview_user.hex_color}"}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex-grow">
|
||||
<%= text_input f, :hex_color, value: @data["hex_color"], class: "input", spellcheck: "false", maxlength: 7 %>
|
||||
<%= tag :button, class: "icon-button absolute right-2 top-1",
|
||||
type: "button",
|
||||
phx_click: "randomize_color",
|
||||
phx_target: @myself %>
|
||||
<%= remix_icon("refresh-line", class: "text-xl") %>
|
||||
<button
|
||||
class="icon-button absolute right-2 top-1"
|
||||
type="button"
|
||||
phx-click="randomize_color"
|
||||
phx-target={@myself}>
|
||||
<.remix_icon icon="refresh-line" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= tag :button, class: "button button-blue flex space-x-1 justify-center items-center",
|
||||
type: "submit",
|
||||
disabled: not @valid %>
|
||||
<%= remix_icon("save-line") %>
|
||||
<button
|
||||
class="button button-blue flex space-x-1 justify-center items-center"
|
||||
type="submit"
|
||||
disabled={not @valid}>
|
||||
<.remix_icon icon="save-line" />
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
defmodule LivebookWeb.UserHelpers do
|
||||
import Phoenix.LiveView
|
||||
import Phoenix.LiveView.Helpers
|
||||
use Phoenix.Component
|
||||
|
||||
alias Livebook.Users.User
|
||||
|
||||
@doc """
|
||||
Renders user avatar,
|
||||
Renders user avatar.
|
||||
|
||||
## Options
|
||||
## Examples
|
||||
|
||||
* `:class` - class added to the avatar box
|
||||
|
||||
* `:text_class` - class added to the avatar text
|
||||
<.user_avatar user={@user} class="h-20 w-20" text_class="text-3xl" />
|
||||
"""
|
||||
def render_user_avatar(user, opts \\ []) do
|
||||
assigns = %{
|
||||
name: user.name,
|
||||
hex_color: user.hex_color,
|
||||
class: Keyword.get(opts, :class, "w-full h-full"),
|
||||
text_class: Keyword.get(opts, :text_class)
|
||||
}
|
||||
def user_avatar(assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign_new(:class, fn -> "w-full h-full" end)
|
||||
|> assign_new(:text_class, fn -> "" end)
|
||||
|
||||
~L"""
|
||||
<div class="rounded-full <%= @class %> flex items-center justify-center" style="background-color: <%= @hex_color %>">
|
||||
<div class="<%= @text_class %> text-gray-100 font-semibold">
|
||||
<%= avatar_text(@name) %>
|
||||
~H"""
|
||||
<div class={"#{@class} rounded-full flex items-center justify-center"} style={"background-color: #{@user.hex_color}"}>
|
||||
<div class={"#{@text_class} text-gray-100 font-semibold"}>
|
||||
<%= avatar_text(@user.name) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="flex items-center space-x-2 rounded-lg px-4 py-2 bg-blue-100 text-blue-600 hover:opacity-75 cursor-pointer" role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="info">
|
||||
<%= remix_icon("information-line", class: "text-2xl") %>
|
||||
<.remix_icon icon="information-line" class="text-2xl" />
|
||||
<span class="whitespace-pre-wrap"><%= live_flash(@flash, :info) %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -13,7 +13,7 @@
|
|||
<div class="flex items-center space-x-2 rounded-lg px-4 py-2 bg-red-100 text-red-400 hover:opacity-75 cursor-pointer" role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="error">
|
||||
<%= remix_icon("error-warning-line", class: "text-2xl") %>
|
||||
<.remix_icon icon="error-warning-line" class="text-2xl" />
|
||||
<span class="whitespace-pre-wrap"><%= live_flash(@flash, :error) %></span>
|
||||
</div>
|
||||
<% end %>
|
|
@ -6,8 +6,8 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<%= csrf_meta_tag() %>
|
||||
<%= live_title_tag assigns[:page_title] || "Livebook" %>
|
||||
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
|
||||
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
||||
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")}/>
|
||||
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/app.js")}></script>
|
||||
</head>
|
||||
<body>
|
||||
<%= @inner_content %>
|
8
mix.exs
8
mix.exs
|
@ -34,14 +34,14 @@ defmodule Livebook.MixProject do
|
|||
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.5.7"},
|
||||
{:phoenix, "1.6.0-dev", github: "phoenixframework/phoenix", override: true},
|
||||
# We point LV to an exact version, because we install
|
||||
# the npm package from there to bundle all the assets,
|
||||
# so the Elixir-side version must match
|
||||
{:phoenix_live_view, "0.15.7"},
|
||||
{:phoenix_live_dashboard, "~> 0.4"},
|
||||
{:phoenix_live_view, "0.16.0-dev", github: "phoenixframework/phoenix_live_view"},
|
||||
{:phoenix_live_dashboard, "0.5.0-dev", github: "phoenixframework/phoenix_live_dashboard"},
|
||||
{:floki, ">= 0.27.0", only: :test},
|
||||
{:phoenix_html, "~> 2.11"},
|
||||
{:phoenix_html, "3.0.0-dev", github: "phoenixframework/phoenix_html", override: true},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:telemetry_metrics, "~> 0.4"},
|
||||
{:telemetry_poller, "~> 0.4"},
|
||||
|
|
13
mix.lock
13
mix.lock
|
@ -10,17 +10,18 @@
|
|||
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
|
||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
||||
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
|
||||
"phoenix": {:hex, :phoenix, "1.5.9", "a6368d36cfd59d917b37c44386e01315bc89f7609a10a45a22f47c007edf2597", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7e4bce20a67c012f1fbb0af90e5da49fa7bf0d34e3a067795703b74aef75427d"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.15.7", "09720b8e5151b3ca8ef739cd7626d4feb987c69ba0b509c9bbdb861d5a365881", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a756cf662420272d0f1b3b908cce5222163b5a95aa9bab404f9d29aff53276e"},
|
||||
"phoenix": {:git, "https://github.com/phoenixframework/phoenix.git", "95b9383c6a08be4ab1a0c8a7bbf6b6911d888ca1", []},
|
||||
"phoenix_html": {:git, "https://github.com/phoenixframework/phoenix_html.git", "d35bebbea395569573ef0e1757cbec735da0573b", []},
|
||||
"phoenix_live_dashboard": {:git, "https://github.com/phoenixframework/phoenix_live_dashboard.git", "1cc67e3c7275b8e68d8201e5dc3660893ae9e4ec", []},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
|
||||
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "92100b658b257a9dffc11a4ca13e4e9054048f61", []},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
|
||||
"phoenix_view": {:git, "https://github.com/phoenixframework/phoenix_view.git", "90ce9c9ef5f832f80e956b77d079f79171ed45d0", []},
|
||||
"plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.5.0", "51c998f788c4e68fc9f947a5eba8c215fbb1d63a520f7604134cab0270ea6513", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5b2c8925a5e2587446f33810a58c01e66b3c345652eeec809b76ba007acde71a"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -418,4 +418,57 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
|
||||
assert expected_document == document
|
||||
end
|
||||
|
||||
test "handles backticks in code cell" do
|
||||
notebook = %{
|
||||
Notebook.new()
|
||||
| name: "My Notebook",
|
||||
metadata: %{},
|
||||
sections: [
|
||||
%{
|
||||
Notebook.Section.new()
|
||||
| name: "Section 1",
|
||||
metadata: %{},
|
||||
cells: [
|
||||
%{
|
||||
Notebook.Cell.new(:elixir)
|
||||
| source: """
|
||||
\"\"\"
|
||||
```elixir
|
||||
x = 1
|
||||
```
|
||||
|
||||
````markdown
|
||||
# Heading
|
||||
````
|
||||
\"\"\"\
|
||||
"""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
expected_document = """
|
||||
# My Notebook
|
||||
|
||||
## Section 1
|
||||
|
||||
`````elixir
|
||||
\"\"\"
|
||||
```elixir
|
||||
x = 1
|
||||
```
|
||||
|
||||
````markdown
|
||||
# Heading
|
||||
````
|
||||
\"\"\"
|
||||
`````
|
||||
"""
|
||||
|
||||
document = Export.notebook_to_markdown(notebook)
|
||||
|
||||
assert expected_document == document
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ defmodule Livebook.Runtime.ErlDist.NodeManagerTest do
|
|||
|
||||
test "terminates when the last runtime server terminates" do
|
||||
{:ok, manager_pid} =
|
||||
NodeManager.start_link(unload_modules_on_termination: false, anonymous: true)
|
||||
start_supervised({NodeManager, [unload_modules_on_termination: false, anonymous: true]})
|
||||
|
||||
server1 = NodeManager.start_runtime_server(manager_pid)
|
||||
server2 = NodeManager.start_runtime_server(manager_pid)
|
||||
|
|
|
@ -5,7 +5,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
|
||||
setup do
|
||||
{:ok, manager_pid} =
|
||||
NodeManager.start_link(unload_modules_on_termination: false, anonymous: true)
|
||||
start_supervised({NodeManager, [unload_modules_on_termination: false, anonymous: true]})
|
||||
|
||||
runtime_server_pid = NodeManager.start_runtime_server(manager_pid)
|
||||
RuntimeServer.set_owner(runtime_server_pid, self())
|
||||
|
|
Loading…
Reference in a new issue