mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-06 11:35:54 +08:00
Bulk actions for sessions (#939)
* Initial implementation to close multiple sessions * Sessions: bulk actions with components * Rename Disconnect sessions to Disconnect runtime * Select all and disabled when nothing is selected * Styled checkbox * Renames toggle events * Warning about not persisted notebooks * Adds disconnect runtime option for a single session * Edit sessions on right * Fix: typos and plural * Minor adjustments * Removes the loop for rendering the menu * Menus with fixed width * Minor adjustments * Pluralize as global helper * Bulk actions form on client side * Track bulk actions buttons state * Fix: home live tests * Doctests for pluralize * Fix: bulk actions buttons losing state on session update * Fix: format * Minor adjustment on toggle_edit * Review-based adjustments * Reset the Edit state after single-session actions * Minor adjustments * Fixes bulk action events * Submit the bulk action form directly * Tests for bulk actions * Indentation * Update lib/livebook_web/live/home_live/close_session_component.ex Co-authored-by: José Valim <jose.valim@gmail.com> Co-authored-by: José Valim <jose.valim@gmail.com>
This commit is contained in:
parent
d07945738d
commit
4dd28388a5
10 changed files with 329 additions and 34 deletions
|
@ -163,6 +163,19 @@
|
|||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='10' cy='10' r='9.5' stroke='%233E64FF' fill='white' /%3e%3ccircle cx='10' cy='10' r='6' fill='%233E64FF' /%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.checkbox-base {
|
||||
@apply h-5 w-5 appearance-none border border-gray-300 rounded text-blue-600 cursor-pointer;
|
||||
}
|
||||
|
||||
.checkbox-base:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* Custom scrollbars */
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar {
|
||||
|
@ -212,6 +225,14 @@
|
|||
@apply w-full flex space-x-3 px-5 py-2 items-center hover:bg-gray-100 focus:bg-gray-100 whitespace-nowrap;
|
||||
}
|
||||
|
||||
.menu-item:disabled {
|
||||
@apply pointer-events-none opacity-50;
|
||||
}
|
||||
|
||||
.menu-item--disabled {
|
||||
@apply pointer-events-none opacity-50;
|
||||
}
|
||||
|
||||
/* Boxes */
|
||||
|
||||
.error-box {
|
||||
|
|
|
@ -91,6 +91,14 @@ window.addEventListener("lb:set_value", (event) => {
|
|||
event.target.value = event.detail.value;
|
||||
});
|
||||
|
||||
window.addEventListener("lb:check", (event) => {
|
||||
event.target.checked = true;
|
||||
});
|
||||
|
||||
window.addEventListener("lb:uncheck", (event) => {
|
||||
event.target.checked = false;
|
||||
});
|
||||
|
||||
window.addEventListener("lb:clipcopy", (event) => {
|
||||
if ("clipboard" in navigator) {
|
||||
const text = event.target.textContent;
|
||||
|
@ -113,6 +121,18 @@ window.addEventListener("contextmenu", (event) => {
|
|||
}
|
||||
});
|
||||
|
||||
window.addEventListener("lb:session_list:on_selection_change", () => {
|
||||
const anySessionSelected = !!document.querySelector(
|
||||
"[name='session_ids[]']:checked"
|
||||
);
|
||||
const disconnect = document.querySelector(
|
||||
"#edit-sessions [name='disconnect']"
|
||||
);
|
||||
const closeAll = document.querySelector("#edit-sessions [name='close_all']");
|
||||
disconnect.disabled = !anySessionSelected;
|
||||
closeAll.disabled = !anySessionSelected;
|
||||
});
|
||||
|
||||
// Global configuration
|
||||
|
||||
settingsStore.getAndSubscribe((settings) => {
|
||||
|
|
|
@ -375,4 +375,18 @@ defmodule LivebookWeb.Helpers do
|
|||
|
||||
def file_system_label(%FileSystem.Local{}), do: "Local disk"
|
||||
def file_system_label(%FileSystem.S3{} = fs), do: fs.bucket_url
|
||||
|
||||
@doc """
|
||||
Returns the text in singular or plural depending on the quantity
|
||||
|
||||
## Examples
|
||||
|
||||
iex> LivebookWeb.Helpers.pluralize(1, "notebook is not persisted", "notebooks are not persisted")
|
||||
"1 notebook is not persisted"
|
||||
|
||||
iex> LivebookWeb.Helpers.pluralize(3, "notebook is not persisted", "notebooks are not persisted")
|
||||
"3 notebooks are not persisted"
|
||||
"""
|
||||
def pluralize(1, singular, _plural), do: "1 #{singular}"
|
||||
def pluralize(count, _singular, plural), do: "#{count} #{plural}"
|
||||
end
|
||||
|
|
|
@ -107,7 +107,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
<div class="py-12">
|
||||
<.live_component module={LivebookWeb.HomeLive.SessionListComponent}
|
||||
id="session-list"
|
||||
sessions={@sessions} />
|
||||
sessions={@sessions}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -136,6 +136,17 @@ defmodule LivebookWeb.HomeLive do
|
|||
import_opts={@import_opts} />
|
||||
</.modal>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :edit_sessions do %>
|
||||
<.modal class="w-full max-w-xl" return_to={Routes.home_path(@socket, :page)}>
|
||||
<.live_component module={LivebookWeb.HomeLive.EditSessionsComponent}
|
||||
id="edit-sessions"
|
||||
action={@bulk_action}
|
||||
return_to={Routes.home_path(@socket, :page)}
|
||||
sessions={@sessions}
|
||||
selected_sessions={selected_sessions(@sessions, @selected_session_ids)} />
|
||||
</.modal>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -153,6 +164,14 @@ defmodule LivebookWeb.HomeLive do
|
|||
{:noreply, assign(socket, session: session)}
|
||||
end
|
||||
|
||||
def handle_params(
|
||||
%{"action" => action},
|
||||
_url,
|
||||
%{assigns: %{live_action: :edit_sessions}} = socket
|
||||
) do
|
||||
{:noreply, assign(socket, bulk_action: action)}
|
||||
end
|
||||
|
||||
def handle_params(%{"tab" => tab} = params, _url, %{assigns: %{live_action: :import}} = socket) do
|
||||
import_opts = [url: params["url"]]
|
||||
{:noreply, assign(socket, tab: tab, import_opts: import_opts)}
|
||||
|
@ -221,6 +240,22 @@ defmodule LivebookWeb.HomeLive do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("bulk_action", %{"action" => "disconnect"} = params, socket) do
|
||||
socket = assign(socket, selected_session_ids: params["session_ids"])
|
||||
{:noreply, push_patch(socket, to: Routes.home_path(socket, :edit_sessions, "disconnect"))}
|
||||
end
|
||||
|
||||
def handle_event("bulk_action", %{"action" => "close_all"} = params, socket) do
|
||||
socket = assign(socket, selected_session_ids: params["session_ids"])
|
||||
{:noreply, push_patch(socket, to: Routes.home_path(socket, :edit_sessions, "close_all"))}
|
||||
end
|
||||
|
||||
def handle_event("disconnect_runtime", %{"id" => session_id}, socket) do
|
||||
session = Enum.find(socket.assigns.sessions, &(&1.id == session_id))
|
||||
Session.disconnect_runtime(session.pid)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("fork_session", %{"id" => session_id}, socket) do
|
||||
session = Enum.find(socket.assigns.sessions, &(&1.id == session_id))
|
||||
%{images_dir: images_dir} = session
|
||||
|
@ -345,4 +380,8 @@ defmodule LivebookWeb.HomeLive do
|
|||
{:error, _} -> :none
|
||||
end
|
||||
end
|
||||
|
||||
defp selected_sessions(sessions, selected_session_ids) do
|
||||
Enum.filter(sessions, &(&1.id in selected_session_ids))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule LivebookWeb.HomeLive.CloseSessionComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
import LivebookWeb.HomeLive.SessionListComponent, only: [toggle_edit: 1]
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
@ -14,10 +16,11 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do
|
|||
<br/>
|
||||
<%= if @session.file,
|
||||
do: "This won't delete any persisted files.",
|
||||
else: "The notebook is not persisted and all content will be lost." %>
|
||||
else: "The notebook is not persisted and content may be lost." %>
|
||||
</p>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<button class="button-base button-red" phx-click="close" phx-target={@myself}>
|
||||
<button class="button-base button-red" role="button"
|
||||
phx-click={toggle_edit(:off) |> JS.push("close", target: @myself)}>
|
||||
<.remix_icon icon="close-circle-line" class="align-middle mr-1" />
|
||||
Close session
|
||||
</button>
|
||||
|
|
76
lib/livebook_web/live/home_live/edit_sessions_component.ex
Normal file
76
lib/livebook_web/live/home_live/edit_sessions_component.ex
Normal file
|
@ -0,0 +1,76 @@
|
|||
defmodule LivebookWeb.HomeLive.EditSessionsComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
import LivebookWeb.HomeLive.SessionListComponent, only: [toggle_edit: 1]
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 pb-4 flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
<%= title(@action) %>
|
||||
</h3>
|
||||
<.message action={@action} selected_sessions={@selected_sessions} sessions={@sessions}/>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<button class="button-base button-red" role="button"
|
||||
phx-click={toggle_edit(:off) |> JS.push(@action, target: @myself)}>
|
||||
<.remix_icon icon="close-circle-line" class="align-middle mr-1" />
|
||||
<%= button_label(@action) %>
|
||||
</button>
|
||||
<%= live_patch "Cancel", to: @return_to, class: "button-base button-outlined-gray" %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp message(%{action: "close_all"} = assigns) do
|
||||
~H"""
|
||||
<p class="text-gray-700">
|
||||
Are you sure you want to close <%= pluralize(length(@selected_sessions), "session", "sessions") %>?
|
||||
<%= if not_persisted_count(@selected_sessions) > 0 do %>
|
||||
<br/>
|
||||
<span class="font-medium">Important:</span>
|
||||
<%= pluralize(
|
||||
not_persisted_count(@selected_sessions),
|
||||
"notebook is not persisted and its content may be lost.",
|
||||
"notebooks are not persisted and their content may be lost."
|
||||
) %>
|
||||
<% end %>
|
||||
</p>
|
||||
"""
|
||||
end
|
||||
|
||||
defp message(%{action: "disconnect"} = assigns) do
|
||||
~H"""
|
||||
<p class="text-gray-700">
|
||||
Are you sure you want to disconnect <%= pluralize(length(@selected_sessions), "session", "sessions") %>?
|
||||
</p>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("close_all", %{}, socket) do
|
||||
socket.assigns.selected_sessions
|
||||
|> Enum.each(&Livebook.Session.close(&1.pid))
|
||||
|
||||
{:noreply, push_patch(socket, to: socket.assigns.return_to, replace: true)}
|
||||
end
|
||||
|
||||
def handle_event("disconnect", %{}, socket) do
|
||||
socket.assigns.selected_sessions
|
||||
|> Enum.reject(&(&1.memory_usage.runtime == nil))
|
||||
|> Enum.each(&Livebook.Session.disconnect_runtime(&1.pid))
|
||||
|
||||
{:noreply, push_patch(socket, to: socket.assigns.return_to, replace: true)}
|
||||
end
|
||||
|
||||
defp button_label("close_all"), do: "Close sessions"
|
||||
defp button_label("disconnect"), do: "Disconnect runtime"
|
||||
|
||||
defp title("close_all"), do: "Close sessions"
|
||||
defp title("disconnect"), do: "Disconnect runtime"
|
||||
|
||||
defp not_persisted_count(selected_sessions) do
|
||||
Enum.count(selected_sessions, &(!&1.file))
|
||||
end
|
||||
end
|
|
@ -32,35 +32,43 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<form id="bulk-action-form" phx-submit="bulk_action">
|
||||
<div class="mb-4 flex items-center md:items-end justify-between">
|
||||
<h2 class="uppercase font-semibold text-gray-500 text-sm md:text-base">
|
||||
Running sessions (<%= length(@sessions) %>)
|
||||
</h2>
|
||||
<div class="flex flex-row">
|
||||
<.memory_info />
|
||||
<.menu id="sessions-order-menu">
|
||||
<:toggle>
|
||||
<button class="button-base button-outlined-gray px-4 py-1">
|
||||
<span><%= order_by_label(@order_by) %></span>
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-lg leading-none align-middle ml-1" />
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<%= for order_by <- ["date", "title", "memory"] do %>
|
||||
<button class={"menu-item #{if order_by == @order_by, do: "text-gray-900", else: "text-gray-500"}"}
|
||||
role="menuitem"
|
||||
phx-click={JS.push("set_order", value: %{order_by: order_by}, target: @myself)}>
|
||||
<.remix_icon icon={order_by_icon(order_by)} />
|
||||
<span class="font-medium"><%= order_by_label(order_by) %></span>
|
||||
<h2 class="uppercase font-semibold text-gray-500 text-sm md:text-base">
|
||||
Running sessions (<%= length(@sessions) %>)
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<.memory_info />
|
||||
<%= if @sessions != [] do %>
|
||||
<.edit_sessions sessions={@sessions} socket={@socket}/>
|
||||
<% end %>
|
||||
<.menu id="sessions-order-menu">
|
||||
<:toggle>
|
||||
<button class="w-28 button-base button-outlined-gray px-4 py-1 flex justify-between items-center"
|
||||
type="button">
|
||||
<span><%= order_by_label(@order_by) %></span>
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-lg leading-none align-middle ml-1" />
|
||||
</button>
|
||||
<% end %>
|
||||
</:content>
|
||||
</.menu>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<%= for order_by <- ["date", "title", "memory"] do %>
|
||||
<button class={"menu-item #{if order_by == @order_by, do: "text-gray-900", else: "text-gray-500"}"}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
phx-click={JS.push("set_order", value: %{order_by: order_by}, target: @myself)}>
|
||||
<.remix_icon icon={order_by_icon(order_by)} />
|
||||
<span class="font-medium"><%= order_by_label(order_by) %></span>
|
||||
</button>
|
||||
<% end %>
|
||||
</:content>
|
||||
</.menu>
|
||||
</div>
|
||||
</div>
|
||||
<.session_list sessions={@sessions} socket={@socket} show_autosave_note?={@show_autosave_note?} />
|
||||
</div>
|
||||
<.session_list sessions={@sessions} socket={@socket}
|
||||
show_autosave_note?={@show_autosave_note?} />
|
||||
</form>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -93,6 +101,12 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
|
|||
<%= for session <- @sessions do %>
|
||||
<div class="py-4 flex items-center border-b border-gray-300"
|
||||
data-test-session-id={session.id}>
|
||||
<div id={"#{session.id}-checkbox"} phx-update="ignore">
|
||||
<input type="checkbox" name="session_ids[]" value={session.id}
|
||||
class="checkbox-base hidden mr-3"
|
||||
data-element="bulk-edit-member"
|
||||
phx-click={JS.dispatch("lb:session_list:on_selection_change")}>
|
||||
</div>
|
||||
<div class="grow flex flex-col items-start">
|
||||
<%= live_redirect session.notebook_name,
|
||||
to: Routes.session_path(@socket, :page, session.id),
|
||||
|
@ -113,12 +127,13 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
|
|||
</div>
|
||||
<.menu id={"session-#{session.id}-menu"}>
|
||||
<:toggle>
|
||||
<button class="icon-button" aria-label="open session menu">
|
||||
<button class="icon-button" aria-label="open session menu" type="button">
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<button class="menu-item text-gray-500"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
phx-click="fork_session"
|
||||
phx-value-id={session.id}>
|
||||
|
@ -132,6 +147,15 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
|
|||
<.remix_icon icon="dashboard-2-line" />
|
||||
<span class="font-medium">See on Dashboard</span>
|
||||
</a>
|
||||
<button class="menu-item text-gray-500"
|
||||
type="button"
|
||||
disabled={!session.memory_usage.runtime}
|
||||
role="menuitem"
|
||||
phx-click={toggle_edit(:off) |> JS.push("disconnect_runtime")}
|
||||
phx-value-id={session.id}>
|
||||
<.remix_icon icon="shut-down-line" />
|
||||
<span class="font-medium">Disconnect runtime</span>
|
||||
</button>
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, session.id),
|
||||
class: "menu-item text-red-600",
|
||||
role: "menuitem" do %>
|
||||
|
@ -171,6 +195,49 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp edit_sessions(assigns) do
|
||||
~H"""
|
||||
<div class="mx-4 mr-2 text-gray-600 flex flex-row gap-1">
|
||||
<.menu id="edit-sessions">
|
||||
<:toggle>
|
||||
<button id="toggle-edit" class="w-28 button-base button-outlined-gray px-4 pl-2 py-1"
|
||||
phx-click={toggle_edit(:on)} type="button">
|
||||
<.remix_icon icon="list-check-2" class="text-lg leading-none align-middle ml-1" />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button class="hidden w-28 button-base button-outlined-gray px-4 py-1 flex justify-between items-center"
|
||||
data-element="bulk-edit-member"
|
||||
type="button">
|
||||
<span>Actions</span>
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-lg leading-none align-middle ml-1" />
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<button class="menu-item text-gray-600" phx-click={toggle_edit(:off)} type="button">
|
||||
<.remix_icon icon="close-line" />
|
||||
<span class="font-medium">Cancel</span>
|
||||
</button>
|
||||
<button class="menu-item text-gray-600" phx-click={select_all()} type="button">
|
||||
<.remix_icon icon="checkbox-multiple-line" />
|
||||
<span class="font-medium">Select all</span>
|
||||
</button>
|
||||
<button class="menu-item text-gray-600" name="disconnect" type="button"
|
||||
phx-click={set_action("disconnect")}>
|
||||
<.remix_icon icon="shut-down-line" />
|
||||
<span class="font-medium">Disconnect runtime</span>
|
||||
</button>
|
||||
<button class="menu-item text-red-600" name="close_all" type="button"
|
||||
phx-click={set_action("close_all")}>
|
||||
<.remix_icon icon="close-circle-line" />
|
||||
<span class="font-medium">Close sessions</span>
|
||||
</button>
|
||||
<input id="bulk-action-input" class="hidden" type="text" name="action"/>
|
||||
</:content>
|
||||
</.menu>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("set_order", %{"order_by" => order_by}, socket) do
|
||||
sessions = sort_sessions(socket.assigns.sessions, order_by)
|
||||
|
@ -182,6 +249,19 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
|
|||
time_words <> " ago"
|
||||
end
|
||||
|
||||
def toggle_edit(:on) do
|
||||
JS.remove_class("hidden", to: "[data-element='bulk-edit-member']")
|
||||
|> JS.add_class("hidden", to: "#toggle-edit")
|
||||
|> JS.dispatch("lb:session_list:on_selection_change")
|
||||
end
|
||||
|
||||
def toggle_edit(:off) do
|
||||
JS.add_class("hidden", to: "[data-element='bulk-edit-member']")
|
||||
|> JS.remove_class("hidden", to: "#toggle-edit")
|
||||
|> JS.dispatch("lb:uncheck", to: "[name='session_ids[]']")
|
||||
|> JS.dispatch("lb:session_list:on_selection_change")
|
||||
end
|
||||
|
||||
defp order_by_label("date"), do: "Date"
|
||||
defp order_by_label("title"), do: "Title"
|
||||
defp order_by_label("memory"), do: "Memory"
|
||||
|
@ -206,4 +286,14 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
|
|||
|
||||
defp total_runtime_memory(%{memory_usage: %{runtime: nil}}), do: 0
|
||||
defp total_runtime_memory(%{memory_usage: %{runtime: %{total: total}}}), do: total
|
||||
|
||||
defp select_all() do
|
||||
JS.dispatch("lb:check", to: "[name='session_ids[]']")
|
||||
|> JS.dispatch("lb:session_list:on_selection_change")
|
||||
end
|
||||
|
||||
defp set_action(action) do
|
||||
JS.dispatch("lb:set_value", to: "#bulk-action-input", detail: %{value: action})
|
||||
|> JS.dispatch("submit", to: "#bulk-action-form")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,6 +35,7 @@ defmodule LivebookWeb.Router do
|
|||
live "/home/user-profile", HomeLive, :user
|
||||
live "/home/import/:tab", HomeLive, :import
|
||||
live "/home/sessions/:session_id/close", HomeLive, :close_session
|
||||
live "/home/sessions/edit_sessions/:action", HomeLive, :edit_sessions
|
||||
|
||||
live "/settings", SettingsLive, :page
|
||||
live "/settings/user-profile", SettingsLive, :user
|
||||
|
|
|
@ -3,6 +3,8 @@ defmodule LivebookWeb.HelpersTest do
|
|||
|
||||
alias LivebookWeb.Helpers
|
||||
|
||||
doctest Helpers
|
||||
|
||||
describe "names_to_html_ids/1" do
|
||||
test "title case" do
|
||||
assert(Helpers.names_to_html_ids(["Title of a Section"]) == ["title-of-a-section"])
|
||||
|
|
|
@ -29,7 +29,7 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
path = Path.expand("../../../lib", __DIR__) <> "/"
|
||||
|
||||
view
|
||||
|> element("form")
|
||||
|> element(~s{form[phx-change="set_path"]})
|
||||
|> render_change(%{path: path})
|
||||
|
||||
# Render the view separately to make sure it received the :set_file event
|
||||
|
@ -42,7 +42,7 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
path = test_notebook_path("basic")
|
||||
|
||||
view
|
||||
|> element("form")
|
||||
|> element(~s{form[phx-change="set_path"]})
|
||||
|> render_change(%{path: Path.dirname(path) <> "/"})
|
||||
|
||||
view
|
||||
|
@ -62,7 +62,7 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
{:ok, view, _} = live(conn, "/")
|
||||
|
||||
view
|
||||
|> element("form")
|
||||
|> element(~s{form[phx-change="set_path"]})
|
||||
|> render_change(%{path: tmp_dir <> "/"})
|
||||
|
||||
assert view
|
||||
|
@ -76,7 +76,7 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
path = File.cwd!() |> Path.join("nonexistent.livemd")
|
||||
|
||||
view
|
||||
|> element("form")
|
||||
|> element(~s{form[phx-change="set_path"]})
|
||||
|> render_change(%{path: path})
|
||||
|
||||
assert view
|
||||
|
@ -94,7 +94,7 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
File.chmod!(path, 0o444)
|
||||
|
||||
view
|
||||
|> element("form")
|
||||
|> element(~s{form[phx-change="set_path"]})
|
||||
|> render_change(%{path: tmp_dir <> "/"})
|
||||
|
||||
view
|
||||
|
@ -165,11 +165,40 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
|> render_click()
|
||||
|
||||
view
|
||||
|> element(~s{button}, "Close session")
|
||||
|> element(~s{button[role=button]}, "Close session")
|
||||
|> render_click()
|
||||
|
||||
refute render(view) =~ session.id
|
||||
end
|
||||
|
||||
test "close all selected sessions using bulk action", %{conn: conn} do
|
||||
{:ok, session1} = Sessions.create_session()
|
||||
{:ok, session2} = Sessions.create_session()
|
||||
{:ok, session3} = Sessions.create_session()
|
||||
|
||||
{:ok, view, _} = live(conn, "/")
|
||||
|
||||
assert render(view) =~ session1.id
|
||||
assert render(view) =~ session2.id
|
||||
assert render(view) =~ session3.id
|
||||
|
||||
view
|
||||
|> form("#bulk-action-form", %{
|
||||
"action" => "close_all",
|
||||
"session_ids" => [session1.id, session2.id, session3.id]
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert render(view) =~ "Are you sure you want to close 3 sessions?"
|
||||
|
||||
view
|
||||
|> element(~s{button[role="button"]}, "Close sessions")
|
||||
|> render_click()
|
||||
|
||||
refute render(view) =~ session1.id
|
||||
refute render(view) =~ session2.id
|
||||
refute render(view) =~ session3.id
|
||||
end
|
||||
end
|
||||
|
||||
test "link to introductory notebook correctly creates a new session", %{conn: conn} do
|
||||
|
|
Loading…
Add table
Reference in a new issue