mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-26 05:16:29 +08:00
Notebook status indicators (#127)
* Add notebook indicators * Make evaluation status button point to the corresponding cell * Rename introductory notebook * Update path highlight when chosen for saving * Allow specifying nonexistent directories when saving and create those * Update lib/livebook_web/live/session_live/indicators_component.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Update lib/livebook_web/live/session_live/indicators_component.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Update lib/livebook_web/live/session_live/indicators_component.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Update lib/livebook_web/live/session_live/indicators_component.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Update lib/livebook_web/live/session_live/indicators_component.ex Co-authored-by: José Valim <jose.valim@dashbit.co> Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
4e90666350
commit
5efd8eb851
14 changed files with 237 additions and 46 deletions
|
|
@ -64,6 +64,10 @@
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-outlined-button {
|
||||||
|
@apply rounded-full border-2;
|
||||||
|
}
|
||||||
|
|
||||||
/* Form fields */
|
/* Form fields */
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ solely client-side operations.
|
||||||
/* === Session === */
|
/* === Session === */
|
||||||
|
|
||||||
[data-element="session"]:not([data-js-insert-mode])
|
[data-element="session"]:not([data-js-insert-mode])
|
||||||
[data-element="insert-indicator"] {
|
[data-element="insert-mode-indicator"] {
|
||||||
@apply hidden;
|
@apply invisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-element="session"]
|
[data-element="session"]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Example usage:
|
||||||
/* Tooltip element wrapping the actual hoverable element */
|
/* Tooltip element wrapping the actual hoverable element */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
--distance: 4px;
|
--distance: 4px;
|
||||||
--arrow-size: 5px;
|
--arrow-size: 5px;
|
||||||
--show-delay: 0.5s;
|
--show-delay: 0.5s;
|
||||||
|
|
|
||||||
|
|
@ -47,15 +47,17 @@ const Session = {
|
||||||
handleSectionListClick(this, event);
|
handleSectionListClick(this, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getSectionsPanelToggle().addEventListener("click", (event) => {
|
||||||
|
toggleSectionsPanel(this);
|
||||||
|
});
|
||||||
|
|
||||||
getNotebook().addEventListener("scroll", (event) => {
|
getNotebook().addEventListener("scroll", (event) => {
|
||||||
updateSectionListHighlight();
|
updateSectionListHighlight();
|
||||||
});
|
});
|
||||||
|
|
||||||
document
|
getCellIndicators().addEventListener("click", (event) => {
|
||||||
.querySelector(`[data-element="sections-panel-toggle"]`)
|
handleCellIndicatorsClick(this, event);
|
||||||
.addEventListener("click", (event) => {
|
});
|
||||||
toggleSectionsPanel(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
// DOM setup
|
// DOM setup
|
||||||
|
|
||||||
|
|
@ -246,6 +248,17 @@ function handleSectionListClick(hook, event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles button clicks within cell indicators section.
|
||||||
|
*/
|
||||||
|
function handleCellIndicatorsClick(hook, event) {
|
||||||
|
const button = event.target.closest(`[data-element="focus-cell-button"]`);
|
||||||
|
if (button) {
|
||||||
|
const cellId = button.getAttribute("data-target");
|
||||||
|
setFocusedCell(hook, cellId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the main notebook area being scrolled.
|
* Handles the main notebook area being scrolled.
|
||||||
*/
|
*/
|
||||||
|
|
@ -552,10 +565,18 @@ function getSectionList() {
|
||||||
return document.querySelector(`[data-element="section-list"]`);
|
return document.querySelector(`[data-element="section-list"]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCellIndicators() {
|
||||||
|
return document.querySelector(`[data-element="notebook-indicators"]`);
|
||||||
|
}
|
||||||
|
|
||||||
function getNotebook() {
|
function getNotebook() {
|
||||||
return document.querySelector(`[data-element="notebook"]`);
|
return document.querySelector(`[data-element="notebook"]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSectionsPanelToggle() {
|
||||||
|
return document.querySelector(`[data-element="sections-panel-toggle"]`);
|
||||||
|
}
|
||||||
|
|
||||||
function cancelEvent(event) {
|
function cancelEvent(event) {
|
||||||
// Cancel any default browser behavior.
|
// Cancel any default browser behavior.
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Livebook.Notebook.HelloLivebook do
|
defmodule Livebook.Notebook.Welcome do
|
||||||
livemd = ~s'''
|
livemd = ~s'''
|
||||||
# Hello Livebook
|
# Welcome to Livebook
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ defmodule Livebook.Notebook.HelloLivebook do
|
||||||
There are **Markdown** cells (such as this one) that allow you to describe your work
|
There are **Markdown** cells (such as this one) that allow you to describe your work
|
||||||
and **Elixir** cells where the magic takes place!
|
and **Elixir** cells where the magic takes place!
|
||||||
|
|
||||||
To insert a new cell move your between cells and click one of the revealed buttons. 👇
|
To insert a new cell move your cursor between cells and click one of the revealed buttons. 👇
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
# This is an Elixir cell - as the name suggests that's where the code goes.
|
# This is an Elixir cell - as the name suggests that's where the code goes.
|
||||||
|
|
@ -579,10 +579,12 @@ defmodule Livebook.Session do
|
||||||
if state.data.path != nil and state.data.dirty do
|
if state.data.path != nil and state.data.dirty do
|
||||||
content = LiveMarkdown.Export.notebook_to_markdown(state.data.notebook)
|
content = LiveMarkdown.Export.notebook_to_markdown(state.data.notebook)
|
||||||
|
|
||||||
case File.write(state.data.path, content) do
|
dir = Path.dirname(state.data.path)
|
||||||
:ok ->
|
|
||||||
handle_operation(state, {:mark_as_not_dirty, self()})
|
|
||||||
|
|
||||||
|
with :ok <- File.mkdir_p(dir),
|
||||||
|
:ok <- File.write(state.data.path, content) do
|
||||||
|
handle_operation(state, {:mark_as_not_dirty, self()})
|
||||||
|
else
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
broadcast_error(state.session_id, "failed to save notebook - #{reason}")
|
broadcast_error(state.session_id, "failed to save notebook - #{reason}")
|
||||||
state
|
state
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ defmodule LivebookWeb.HomeLive do
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<span class="tooltip top" aria-label="Introduction">
|
<span class="tooltip top" aria-label="Introduction">
|
||||||
<button class="button button-outlined-gray button-square-icon"
|
<button class="button button-outlined-gray button-square-icon"
|
||||||
phx-click="hello_livebook">
|
phx-click="open_welcome">
|
||||||
<%= remix_icon("compass-line") %>
|
<%= remix_icon("compass-line") %>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -116,8 +116,8 @@ defmodule LivebookWeb.HomeLive do
|
||||||
{:noreply, assign(socket, path: path)}
|
{:noreply, assign(socket, path: path)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("hello_livebook", %{}, socket) do
|
def handle_event("open_welcome", %{}, socket) do
|
||||||
create_session(socket, notebook: Livebook.Notebook.HelloLivebook.new())
|
create_session(socket, notebook: Livebook.Notebook.Welcome.new())
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("new", %{}, socket) do
|
def handle_event("new", %{}, socket) do
|
||||||
|
|
|
||||||
|
|
@ -80,20 +80,20 @@ defmodule LivebookWeb.SessionLive do
|
||||||
<img src="/logo.png" height="40" width="40" alt="livebook" />
|
<img src="/logo.png" height="40" width="40" alt="livebook" />
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="tooltip right distant" aria-label="Sections (ss)">
|
<span class="tooltip right distant" aria-label="Sections (ss)">
|
||||||
<button class="text-2xl text-gray-600 hover:text-gray-50 focus:text-gray-50" data-element="sections-panel-toggle">
|
<button class="text-2xl text-gray-400 hover:text-gray-50 focus:text-gray-50" data-element="sections-panel-toggle">
|
||||||
<%= remix_icon("booklet-fill") %>
|
<%= remix_icon("booklet-fill") %>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip right distant" aria-label="Notebook settings (sn)">
|
<span class="tooltip right distant" aria-label="Notebook settings (sn)">
|
||||||
<%= live_patch to: Routes.session_path(@socket, :settings, @session_id, "file"),
|
<%= live_patch to: Routes.session_path(@socket, :settings, @session_id, "file"),
|
||||||
class: "text-gray-600 hover:text-gray-50 focus:text-gray-50 #{if(@live_action == :settings, do: "text-gray-50")}" do %>
|
class: "text-gray-400 hover:text-gray-50 focus:text-gray-50 #{if(@live_action == :settings, do: "text-gray-50")}" do %>
|
||||||
<%= remix_icon("settings-4-fill", class: "text-2xl") %>
|
<%= remix_icon("settings-4-fill", class: "text-2xl") %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex-grow"></div>
|
<div class="flex-grow"></div>
|
||||||
<span class="tooltip right distant" aria-label="Keyboard shortcuts (?)">
|
<span class="tooltip right distant" aria-label="Keyboard shortcuts (?)">
|
||||||
<%= live_patch to: Routes.session_path(@socket, :shortcuts, @session_id),
|
<%= live_patch to: Routes.session_path(@socket, :shortcuts, @session_id),
|
||||||
class: "text-gray-600 hover:text-gray-50 focus:text-gray-50 #{if(@live_action == :shortcuts, do: "text-gray-50")}" do %>
|
class: "text-gray-400 hover:text-gray-50 focus:text-gray-50 #{if(@live_action == :shortcuts, do: "text-gray-50")}" do %>
|
||||||
<%= remix_icon("keyboard-box-fill", class: "text-2xl") %>
|
<%= remix_icon("keyboard-box-fill", class: "text-2xl") %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -151,9 +151,10 @@ defmodule LivebookWeb.SessionLive do
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%# Show a tiny insert indicator for clarity %>
|
<div class="fixed bottom-[0.5rem] right-[1.5rem]">
|
||||||
<div class="fixed right-5 bottom-1 text-gray-500 text-semibold text-sm" data-element="insert-indicator">
|
<%= live_component @socket, LivebookWeb.SessionLive.IndicatorsComponent,
|
||||||
insert
|
session_id: @session_id,
|
||||||
|
data_view: @data_view %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
@ -328,6 +329,12 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("save", %{}, socket) do
|
||||||
|
Session.save(socket.assigns.session_id)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("show_shortcuts", %{}, socket) do
|
def handle_event("show_shortcuts", %{}, socket) do
|
||||||
{:noreply,
|
{:noreply,
|
||||||
push_patch(socket, to: Routes.session_path(socket, :shortcuts, socket.assigns.session_id))}
|
push_patch(socket, to: Routes.session_path(socket, :shortcuts, socket.assigns.session_id))}
|
||||||
|
|
@ -472,7 +479,9 @@ defmodule LivebookWeb.SessionLive do
|
||||||
defp data_to_view(data) do
|
defp data_to_view(data) do
|
||||||
%{
|
%{
|
||||||
path: data.path,
|
path: data.path,
|
||||||
|
dirty: data.dirty,
|
||||||
runtime: data.runtime,
|
runtime: data.runtime,
|
||||||
|
global_evaluation_status: global_evaluation_status(data),
|
||||||
notebook_name: data.notebook.name,
|
notebook_name: data.notebook.name,
|
||||||
sections_items:
|
sections_items:
|
||||||
for section <- data.notebook.sections do
|
for section <- data.notebook.sections do
|
||||||
|
|
@ -482,6 +491,33 @@ defmodule LivebookWeb.SessionLive do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp global_evaluation_status(data) do
|
||||||
|
cells =
|
||||||
|
data.notebook
|
||||||
|
|> Notebook.elixir_cells_with_section()
|
||||||
|
|> Enum.map(fn {cell, _} -> cell end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
evaluating = Enum.find(cells, &evaluating?(&1, data)) ->
|
||||||
|
{:evaluating, evaluating.id}
|
||||||
|
|
||||||
|
stale = Enum.find(cells, &stale?(&1, data)) ->
|
||||||
|
{:stale, stale.id}
|
||||||
|
|
||||||
|
evaluated = Enum.find(Enum.reverse(cells), &evaluated?(&1, data)) ->
|
||||||
|
{:evaluated, evaluated.id}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:fresh, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp evaluating?(cell, data), do: data.cell_infos[cell.id].evaluation_status == :evaluating
|
||||||
|
|
||||||
|
defp stale?(cell, data), do: data.cell_infos[cell.id].validity_status == :stale
|
||||||
|
|
||||||
|
defp evaluated?(cell, data), do: data.cell_infos[cell.id].validity_status == :evaluated
|
||||||
|
|
||||||
defp section_to_view(section, data) do
|
defp section_to_view(section, data) do
|
||||||
%{
|
%{
|
||||||
id: section.id,
|
id: section.id,
|
||||||
|
|
|
||||||
106
lib/livebook_web/live/session_live/indicators_component.ex
Normal file
106
lib/livebook_web/live/session_live/indicators_component.ex
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
defmodule LivebookWeb.SessionLive.IndicatorsComponent do
|
||||||
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~L"""
|
||||||
|
<div class="flex flex-col space-y-2 items-center" data-element="notebook-indicators">
|
||||||
|
<%= if @data_view.path do %>
|
||||||
|
<%= if @data_view.dirty do %>
|
||||||
|
<span class="tooltip left" aria-label="Autosave pending">
|
||||||
|
<button class="icon-button icon-outlined-button border-blue-400 hover:bg-blue-50 focus:bg-blue-50"
|
||||||
|
phx-click="save">
|
||||||
|
<%= remix_icon("save-line", class: "text-xl text-blue-500") %>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="tooltip left" aria-label="Notebook saved">
|
||||||
|
<button class="icon-button icon-outlined-button border-green-300 hover:bg-green-50 focus:bg-green-50 cursor-default">
|
||||||
|
<%= remix_icon("save-line", class: "text-xl text-green-400") %>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<span class="tooltip left" aria-label="Choose a file to save the notebook">
|
||||||
|
<%= live_patch to: Routes.session_path(@socket, :settings, @session_id, "file"),
|
||||||
|
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") %>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if @data_view.runtime do %>
|
||||||
|
<%= render_global_evaluation_status(@data_view.global_evaluation_status) %>
|
||||||
|
<% else %>
|
||||||
|
<span class="tooltip left" aria-label="Choose a runtime to run the notebook in">
|
||||||
|
<%= live_patch to: Routes.session_path(@socket, :settings, @session_id, "runtime"),
|
||||||
|
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") %>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%# Note: this indicator is shown/hidden using CSS based on the current mode %>
|
||||||
|
<span class="tooltip left" aria-label="Insert mode" data-element="insert-mode-indicator">
|
||||||
|
<span class="text-sm text-gray-400 font-medium cursor-default">
|
||||||
|
ins
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_global_evaluation_status({:evaluating, cell_id}) do
|
||||||
|
assigns = %{cell_id: cell_id}
|
||||||
|
|
||||||
|
~L"""
|
||||||
|
<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") %>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_global_evaluation_status({:evaluated, cell_id}) do
|
||||||
|
assigns = %{cell_id: cell_id}
|
||||||
|
|
||||||
|
~L"""
|
||||||
|
<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") %>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_global_evaluation_status({:stale, cell_id}) do
|
||||||
|
assigns = %{cell_id: cell_id}
|
||||||
|
|
||||||
|
~L"""
|
||||||
|
<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") %>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_global_evaluation_status({:fresh, nil}) do
|
||||||
|
assigns = %{}
|
||||||
|
|
||||||
|
~L"""
|
||||||
|
<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") %>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -6,7 +6,8 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
|
||||||
@impl true
|
@impl true
|
||||||
def mount(socket) do
|
def mount(socket) do
|
||||||
session_summaries = SessionSupervisor.get_session_summaries()
|
session_summaries = SessionSupervisor.get_session_summaries()
|
||||||
{:ok, assign(socket, session_summaries: session_summaries)}
|
running_paths = Enum.map(session_summaries, & &1.path)
|
||||||
|
{:ok, assign(socket, running_paths: running_paths)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -34,7 +35,7 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
|
||||||
id: "path_select",
|
id: "path_select",
|
||||||
path: @path,
|
path: @path,
|
||||||
extnames: [LiveMarkdown.extension()],
|
extnames: [LiveMarkdown.extension()],
|
||||||
running_paths: paths(@session_summaries),
|
running_paths: @running_paths,
|
||||||
target: @myself %>
|
target: @myself %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
@ -49,7 +50,7 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
|
||||||
class: "button button-blue",
|
class: "button button-blue",
|
||||||
phx_click: "save",
|
phx_click: "save",
|
||||||
phx_target: @myself,
|
phx_target: @myself,
|
||||||
disabled: not path_savable?(normalize_path(@path), @session_summaries) or normalize_path(@path) == @current_path %>
|
disabled: not path_savable?(normalize_path(@path), @running_paths) or normalize_path(@path) == @current_path %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -74,26 +75,28 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
|
||||||
def handle_event("save", %{}, socket) do
|
def handle_event("save", %{}, socket) do
|
||||||
path = normalize_path(socket.assigns.path)
|
path = normalize_path(socket.assigns.path)
|
||||||
Session.set_path(socket.assigns.session_id, path)
|
Session.set_path(socket.assigns.session_id, path)
|
||||||
{:noreply, socket}
|
|
||||||
|
running_paths =
|
||||||
|
if path do
|
||||||
|
[path | socket.assigns.running_paths]
|
||||||
|
else
|
||||||
|
List.delete(socket.assigns.running_paths, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, assign(socket, running_paths: running_paths)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp default_path() do
|
defp default_path() do
|
||||||
File.cwd!() |> Path.join("notebook")
|
File.cwd!() |> Path.join("notebook")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp paths(session_summaries) do
|
defp path_savable?(nil, _running_paths), do: true
|
||||||
Enum.map(session_summaries, & &1.path)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp path_savable?(nil, _session_summaries), do: true
|
defp path_savable?(path, running_paths) do
|
||||||
|
|
||||||
defp path_savable?(path, session_summaries) do
|
|
||||||
if File.exists?(path) do
|
if File.exists?(path) do
|
||||||
running_paths = paths(session_summaries)
|
|
||||||
File.regular?(path) and path not in running_paths
|
File.regular?(path) and path not in running_paths
|
||||||
else
|
else
|
||||||
dir = Path.dirname(path)
|
true
|
||||||
File.exists?(dir)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
defmodule Livebook.Notebook.HelloLivebookTest do
|
|
||||||
use ExUnit.Case, async: true
|
|
||||||
|
|
||||||
alias Livebook.Notebook
|
|
||||||
alias Livebook.Notebook.HelloLivebook
|
|
||||||
|
|
||||||
test "new/0 correctly builds a new notebook" do
|
|
||||||
assert %Notebook{} = HelloLivebook.new()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
10
test/livebook/notebook/welcome_test.exs
Normal file
10
test/livebook/notebook/welcome_test.exs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule Livebook.Notebook.WelcomeTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Livebook.Notebook
|
||||||
|
alias Livebook.Notebook.Welcome
|
||||||
|
|
||||||
|
test "new/0 correctly builds a new notebook" do
|
||||||
|
assert %Notebook{} = Welcome.new()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -222,6 +222,24 @@ defmodule Livebook.SessionTest do
|
||||||
assert File.exists?(path)
|
assert File.exists?(path)
|
||||||
assert File.read!(path) =~ "My notebook"
|
assert File.read!(path) =~ "My notebook"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "creates nonexistent directories", %{session_id: session_id, tmp_dir: tmp_dir} do
|
||||||
|
path = Path.join(tmp_dir, "nonexistent/dir/notebook.livemd")
|
||||||
|
Session.set_path(session_id, path)
|
||||||
|
# Perform a change, so the notebook is dirty
|
||||||
|
Session.set_notebook_name(session_id, "My notebook")
|
||||||
|
|
||||||
|
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||||
|
|
||||||
|
refute File.exists?(path)
|
||||||
|
|
||||||
|
Session.save(session_id)
|
||||||
|
|
||||||
|
assert_receive {:operation, {:mark_as_not_dirty, _}}
|
||||||
|
assert File.exists?(path)
|
||||||
|
assert File.read!(path) =~ "My notebook"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "close/1" do
|
describe "close/1" do
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ defmodule LivebookWeb.HomeLiveTest do
|
||||||
assert to =~ "/sessions/"
|
assert to =~ "/sessions/"
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, to)
|
{:ok, view, _} = live(conn, to)
|
||||||
assert render(view) =~ "Hello Livebook"
|
assert render(view) =~ "Welcome to Livebook"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue