Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bulk actions for sessions #939

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c268820
Initial implementation to close multiple sessions
cristineguadelupe Jan 26, 2022
78f92b5
Sessions: bulk actions with components
cristineguadelupe Jan 26, 2022
d54e7d7
Rename Disconnect sessions to Disconnect runtime
cristineguadelupe Jan 26, 2022
e0ea31f
Select all and disabled when nothing is selected
cristineguadelupe Jan 26, 2022
7589c1b
Styled checkbox
cristineguadelupe Jan 26, 2022
05803e5
Renames toggle events
cristineguadelupe Jan 26, 2022
9e25df7
Warning about not persisted notebooks
cristineguadelupe Jan 26, 2022
cd5a767
Adds disconnect runtime option for a single session
cristineguadelupe Jan 26, 2022
3c4fde7
Edit sessions on right
cristineguadelupe Jan 26, 2022
fb00187
Fix: typos and plural
cristineguadelupe Jan 26, 2022
84fe0f7
Minor adjustments
cristineguadelupe Jan 26, 2022
4fb2637
Removes the loop for rendering the menu
cristineguadelupe Jan 26, 2022
8983996
Menus with fixed width
cristineguadelupe Jan 26, 2022
d5ea660
Minor adjustments
cristineguadelupe Jan 27, 2022
2a06ed7
Pluralize as global helper
cristineguadelupe Jan 27, 2022
df58ff7
Bulk actions form on client side
cristineguadelupe Jan 27, 2022
8d26f9c
Track bulk actions buttons state
cristineguadelupe Jan 27, 2022
9a4335f
Fix: home live tests
cristineguadelupe Jan 27, 2022
be52429
Doctests for pluralize
cristineguadelupe Jan 27, 2022
ed7fcec
Fix: bulk actions buttons losing state on session update
cristineguadelupe Jan 27, 2022
a062c87
Fix: format
cristineguadelupe Jan 27, 2022
1278eb2
Minor adjustment on toggle_edit
cristineguadelupe Jan 27, 2022
4d09ce0
Review-based adjustments
cristineguadelupe Jan 28, 2022
b319f29
Reset the Edit state after single-session actions
cristineguadelupe Jan 28, 2022
1fe5369
Minor adjustments
cristineguadelupe Jan 28, 2022
f499a31
Fixes bulk action events
cristineguadelupe Jan 28, 2022
c677a6a
Submit the bulk action form directly
cristineguadelupe Jan 28, 2022
00ff31b
Tests for bulk actions
cristineguadelupe Jan 28, 2022
15f19a0
Indentation
cristineguadelupe Jan 28, 2022
b05deae
Update lib/livebook_web/live/home_live/close_session_component.ex
cristineguadelupe Jan 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions assets/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,18 @@
.error-box {
@apply rounded-lg px-4 py-2 bg-red-100 text-red-400 font-medium;
}

/* Checkbox */
.checkbox-base {
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved
@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;
}
}
59 changes: 57 additions & 2 deletions lib/livebook_web/live/home_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ defmodule LivebookWeb.HomeLive do
file_info: %{exists: true, access: :read_write},
sessions: sessions,
notebook_infos: notebook_infos,
editing?: false,
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved
selected_sessions: [],
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved
page_title: "Livebook"
)}
end
Expand Down Expand Up @@ -107,7 +109,9 @@ defmodule LivebookWeb.HomeLive do
<div class="py-12">
<.live_component module={LivebookWeb.HomeLive.SessionListComponent}
id="session-list"
sessions={@sessions} />
sessions={@sessions}
editing?={@editing?}
selected_sessions={@selected_sessions} />
</div>
</div>
</div>
Expand Down Expand Up @@ -136,6 +140,17 @@ defmodule LivebookWeb.HomeLive do
import_opts={@import_opts} />
</.modal>
<% end %>

<%= if @live_action == :edit_sessions do %>
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved
<.modal class="w-full max-w-xl" return_to={Routes.home_path(@socket, :page)}>
<.live_component module={LivebookWeb.HomeLive.EditSessionsComponent}
id="edit-sessions"
action={@action}
return_to={Routes.home_path(@socket, :page)}
sessions={@sessions}
selected_sessions={@selected_sessions} />
</.modal>
<% end %>
"""
end

Expand All @@ -153,6 +168,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, action: action)}
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved
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)}
Expand All @@ -173,7 +196,7 @@ defmodule LivebookWeb.HomeLive do
end
end

def handle_params(_params, _url, socket), do: {:noreply, socket}
def handle_params(_params, _url, socket), do: {:noreply, clean_selected(socket)}

@impl true
def handle_event("new", %{}, socket) do
Expand Down Expand Up @@ -221,6 +244,36 @@ defmodule LivebookWeb.HomeLive do
{:noreply, socket}
end

def handle_event("toggle_bulk_edit", _, socket) do
{:noreply, update(socket, :editing?, &(!&1))}
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved
end

def handle_event("cancel_bulk_edit", _, socket) do
{:noreply, clean_selected(socket)}
end

def handle_event("select_all", _, socket) do
selected_sessions = Enum.map(socket.assigns.sessions, & &1.id)
{:noreply, assign(socket, selected_sessions: selected_sessions)}
end

def handle_event("select_session", %{"id" => session_id}, socket) do
selected_sessions =
if session_id in socket.assigns.selected_sessions do
List.delete(socket.assigns.selected_sessions, session_id)
else
[session_id | socket.assigns.selected_sessions]
end

{:noreply, assign(socket, selected_sessions: selected_sessions)}
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
Expand Down Expand Up @@ -345,4 +398,6 @@ defmodule LivebookWeb.HomeLive do
{:error, _} -> :none
end
end

defp clean_selected(socket), do: assign(socket, editing?: false, selected_sessions: [])
end
78 changes: 78 additions & 0 deletions lib/livebook_web/live/home_live/edit_sessions_component.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
defmodule LivebookWeb.HomeLive.EditSessionsComponent do
use LivebookWeb, :live_component

@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" phx-click={@action} phx-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 <%= length(assigns.selected_sessions) %> sections?
<%= if not_persisted(assigns) > 0 do %>
<br/>
<span class="font-medium">Important:</span>
<%= not_persisted(assigns) %> notebooks are not persisted and their content will be lost.
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
<% end %>
</p>
"""
end

defp message(%{action: "disconnect"} = assigns) do
~H"""
<p class="text-gray-700">
Are you sure you want to disconnect <%= length(@selected_sessions) %> sections?
</p>
"""
end

@impl true
def handle_event("close_all", %{}, socket) do
socket.assigns
|> selected_sessions()
|> Enum.map(&Livebook.Session.close(&1.pid))
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved

{: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.map(&Livebook.Session.disconnect_runtime(&1.pid))
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved

{:noreply, push_patch(socket, to: socket.assigns.return_to, replace: true)}
end

defp selected_sessions(assigns) do
assigns.sessions
|> Enum.filter(&(&1.id in assigns.selected_sessions))
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(assigns) do
assigns
|> selected_sessions()
|> Enum.count(&(!&1.file))
end
end
80 changes: 78 additions & 2 deletions lib/livebook_web/live/home_live/session_list_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do

@impl true
def mount(socket) do
{:ok, assign(socket, order_by: "date")}
{:ok, assign(socket, order_by: "date", editing?: false)}
end

@impl true
Expand Down Expand Up @@ -34,9 +34,11 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
~H"""
<div>
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved
<div class="mb-4 flex items-center md:items-end justify-between">
<div class="flex flex-row">
<h2 class="uppercase font-semibold text-gray-500 text-sm md:text-base">
Running sessions (<%= length(@sessions) %>)
</h2>
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved
</div>
<div class="flex flex-row">
<.memory_info />
<.menu id="sessions-order-menu">
Expand All @@ -57,9 +59,14 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
<% end %>
</:content>
</.menu>
<%= if length(@sessions) > 0 do %>
<.edit_sessions sessions={@sessions} socket={@socket}
editing?={@editing?} selected_sessions={@selected_sessions} />
<% end %>
</div>
</div>
<.session_list sessions={@sessions} socket={@socket} show_autosave_note?={@show_autosave_note?} />
<.session_list sessions={@sessions} socket={@socket}
show_autosave_note?={@show_autosave_note?} editing?={@editing?} selected_sessions={@selected_sessions} />
</div>
"""
end
Expand Down Expand Up @@ -93,6 +100,14 @@ 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}>
<%= if @editing? do %>
<input
class="checkbox-base mr-3"
type="checkbox"
phx-click="select_session"
phx-value-id={session.id}
checked={selected?(session.id, @selected_sessions)} />
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
<% end %>
<div class="grow flex flex-col items-start">
<%= live_redirect session.notebook_name,
to: Routes.session_path(@socket, :page, session.id),
Expand Down Expand Up @@ -132,6 +147,14 @@ 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
#{if !session.memory_usage.runtime, do: "opacity-50 pointer-events-none"}"}
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
role="menuitem"
phx-click="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 %>
Expand Down Expand Up @@ -171,6 +194,47 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
"""
end

defp edit_sessions(assigns) do
~H"""
<div class="mx-4 mr-0 text-gray-600 flex flex-row gap-1">
<%= if @editing? do %>
<.menu id="edit-sessions">
<:toggle>
<button class="button-base button-outlined-gray px-4 py-1">
<span>Actions</span>
<.remix_icon icon="arrow-down-s-line" class="text-lg leading-none align-middle ml-1" />
</button>
</:toggle>
<:content>
<%= for action <- ["cancel_bulk_edit", "select_all"] do %>
<a href="#" class="menu-item text-gray-600" phx-click={action}>
<.remix_icon icon={action_icon(action)} />
<span class="font-medium"><%= action_label(action) %></span>
</a>
<% end %>
<%= for action <- ["close_all", "disconnect"] do %>
<%= live_patch to: Routes.home_path(@socket, :edit_sessions, action),
class: "menu-item
#{if length(@selected_sessions) == 0, do: "opacity-50 pointer-events-none"}
#{if action == "close_all", do: "text-red-600", else: "text-gray-600"}",
cristineguadelupe marked this conversation as resolved.
Show resolved Hide resolved
role: "menuitem" do %>
<.remix_icon icon={action_icon(action)} />
<span class="font-medium"><%= action_label(action) %></span>
<% end %>
<% end %>
</:content>
</.menu>
<% else %>
<span class="tooltip top" data-tooltip="Edit sessions">
<button class="button-base button-outlined-gray px-2 pl-0 py-1" phx-click="toggle_bulk_edit">
<.remix_icon icon="list-check-2" class="text-lg leading-none align-middle ml-2" />
</button>
</span>
<% end %>
</div>
"""
end

@impl true
def handle_event("set_order", %{"order_by" => order_by}, socket) do
sessions = sort_sessions(socket.assigns.sessions, order_by)
Expand Down Expand Up @@ -206,4 +270,16 @@ 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 selected?(session, selected_sessions), do: session in selected_sessions

defp action_label("close_all"), do: "Close sessions"
defp action_label("disconnect"), do: "Disconnect runtime"
defp action_label("cancel_bulk_edit"), do: "Cancel"
defp action_label("select_all"), do: "Select all"

defp action_icon("close_all"), do: "close-circle-line"
defp action_icon("disconnect"), do: "shut-down-line"
defp action_icon("cancel_bulk_edit"), do: "close-line"
defp action_icon("select_all"), do: "checkbox-multiple-line"
end
1 change: 1 addition & 0 deletions lib/livebook_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down