mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
Rebuild confirmation modal system (#1903)
This commit is contained in:
parent
ff22d5b31c
commit
f262b9c0c9
|
@ -18,6 +18,10 @@ import { loadAppAuthToken } from "./lib/app";
|
|||
import { settingsStore } from "./lib/settings";
|
||||
import { registerTopbar, registerGlobalEventHandlers } from "./events";
|
||||
import { cookieOptions } from "./lib/utils";
|
||||
import {
|
||||
loadConfirmOptOutIds,
|
||||
registerGlobalEventHandlersForConfirm,
|
||||
} from "./confirm";
|
||||
|
||||
function connect() {
|
||||
const csrfToken = document
|
||||
|
@ -34,6 +38,7 @@ function connect() {
|
|||
// Pass the most recent user data to the LiveView in `connect_params`
|
||||
user_data: loadUserData(),
|
||||
app_auth_token: loadAppAuthToken(),
|
||||
confirm_opt_out_ids: loadConfirmOptOutIds(),
|
||||
};
|
||||
},
|
||||
hooks: hooks,
|
||||
|
@ -47,6 +52,8 @@ function connect() {
|
|||
// Handle custom events dispatched with JS.dispatch/3
|
||||
registerGlobalEventHandlers();
|
||||
|
||||
registerGlobalEventHandlersForConfirm();
|
||||
|
||||
// Reflect global configuration in attributes to enable CSS rules
|
||||
settingsStore.getAndSubscribe((settings) => {
|
||||
document.body.setAttribute("data-editor-theme", settings.editor_theme);
|
||||
|
|
16
assets/js/confirm.js
Normal file
16
assets/js/confirm.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { load, store } from "./lib/storage";
|
||||
|
||||
const OPT_OUT_IDS_KEY = "confirm-opted-out-ids";
|
||||
|
||||
export function loadConfirmOptOutIds() {
|
||||
return load(OPT_OUT_IDS_KEY) || [];
|
||||
}
|
||||
|
||||
export function registerGlobalEventHandlersForConfirm() {
|
||||
window.addEventListener("phx:add_confirm_opt_out_id", (event) => {
|
||||
const optedOutIds = load(OPT_OUT_IDS_KEY) || [];
|
||||
const optOutId = event.detail.opt_out_id;
|
||||
optedOutIds.push(optOutId);
|
||||
store(OPT_OUT_IDS_KEY, optedOutIds);
|
||||
});
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
import { load, store } from "../lib/storage";
|
||||
|
||||
const OPT_OUT_IDS_KEY = "confirm-opted-out-ids";
|
||||
|
||||
const ConfirmModal = {
|
||||
mounted() {
|
||||
let confirmEvent = null;
|
||||
|
||||
const titleEl = this.el.querySelector("[data-title]");
|
||||
const descriptionEl = this.el.querySelector("[data-description]");
|
||||
const confirmIconEl = this.el.querySelector("[data-confirm-icon]");
|
||||
const confirmTextEl = this.el.querySelector("[data-confirm-text]");
|
||||
const confirmButtonEl = this.el.querySelector("[data-confirm-button]");
|
||||
const actionsEl = this.el.querySelector("[data-actions]");
|
||||
const optOutEl = this.el.querySelector("[data-opt-out]");
|
||||
const optOutElCheckbox = optOutEl.querySelector("input");
|
||||
|
||||
const optedOutIds = load(OPT_OUT_IDS_KEY) || [];
|
||||
|
||||
this.handleConfirmRequest = (event) => {
|
||||
confirmEvent = event;
|
||||
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
confirm_text,
|
||||
confirm_icon,
|
||||
danger,
|
||||
html,
|
||||
opt_out_id,
|
||||
} = event.detail;
|
||||
|
||||
if (opt_out_id && optedOutIds.includes(opt_out_id)) {
|
||||
liveSocket.execJS(event.target, event.detail.on_confirm);
|
||||
} else {
|
||||
titleEl.textContent = title;
|
||||
|
||||
if (html) {
|
||||
descriptionEl.innerHTML = description;
|
||||
} else {
|
||||
descriptionEl.textContent = description;
|
||||
}
|
||||
|
||||
confirmTextEl.textContent = confirm_text;
|
||||
|
||||
if (confirm_icon) {
|
||||
confirmIconEl.className = `align-middle mr-1 ri-${confirm_icon}`;
|
||||
} else {
|
||||
confirmIconEl.className = "hidden";
|
||||
}
|
||||
|
||||
confirmButtonEl.classList.toggle("button-red", danger);
|
||||
confirmButtonEl.classList.toggle("button-blue", !danger);
|
||||
actionsEl.classList.toggle("flex-row-reverse", danger);
|
||||
actionsEl.classList.toggle("space-x-reverse", danger);
|
||||
|
||||
optOutElCheckbox.checked = false;
|
||||
optOutEl.classList.toggle("hidden", !opt_out_id);
|
||||
|
||||
liveSocket.execJS(this.el, this.el.getAttribute("data-js-show"));
|
||||
}
|
||||
};
|
||||
|
||||
// Events dispatched with JS.dispatch
|
||||
window.addEventListener("lb:confirm_request", this.handleConfirmRequest);
|
||||
// Events dispatched with push_event
|
||||
window.addEventListener(
|
||||
"phx:lb:confirm_request",
|
||||
this.handleConfirmRequest
|
||||
);
|
||||
|
||||
this.el.addEventListener("lb:confirm", (event) => {
|
||||
const { opt_out_id } = confirmEvent.detail;
|
||||
|
||||
if (opt_out_id && optOutElCheckbox.checked) {
|
||||
optedOutIds.push(opt_out_id);
|
||||
store(OPT_OUT_IDS_KEY, optedOutIds);
|
||||
}
|
||||
|
||||
// Events dispatched with push_event have window as target,
|
||||
// in which case we pass body, which is an actual element
|
||||
const target =
|
||||
confirmEvent.target === window ? document.body : confirmEvent.target;
|
||||
|
||||
liveSocket.execJS(target, confirmEvent.detail.on_confirm);
|
||||
});
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
window.removeEventListener("lb:confirm_request", this.handleConfirmRequest);
|
||||
window.removeEventListener(
|
||||
"phx:lb:confirm_request",
|
||||
this.handleConfirmRequest
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default ConfirmModal;
|
|
@ -2,7 +2,6 @@ import AppAuth from "./app_auth";
|
|||
import AudioInput from "./audio_input";
|
||||
import Cell from "./cell";
|
||||
import CellEditor from "./cell_editor";
|
||||
import ConfirmModal from "./confirm_modal";
|
||||
import Dropzone from "./dropzone";
|
||||
import EditorSettings from "./editor_settings";
|
||||
import EmojiPicker from "./emoji_picker";
|
||||
|
@ -26,7 +25,6 @@ export default {
|
|||
AudioInput,
|
||||
Cell,
|
||||
CellEditor,
|
||||
ConfirmModal,
|
||||
Dropzone,
|
||||
EditorSettings,
|
||||
EmojiPicker,
|
||||
|
|
|
@ -62,6 +62,7 @@ defmodule LivebookWeb do
|
|||
# Core UI components
|
||||
import LivebookWeb.CoreComponents
|
||||
import LivebookWeb.FormComponents
|
||||
import LivebookWeb.Confirm
|
||||
|
||||
# Shortcut for generating JS commands
|
||||
alias Phoenix.LiveView.JS
|
||||
|
|
155
lib/livebook_web/components/confirm.ex
Normal file
155
lib/livebook_web/components/confirm.ex
Normal file
|
@ -0,0 +1,155 @@
|
|||
defmodule LivebookWeb.Confirm do
|
||||
use Phoenix.Component
|
||||
|
||||
import LivebookWeb.CoreComponents
|
||||
import Phoenix.LiveView
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
@doc """
|
||||
Shows a confirmation modal.
|
||||
|
||||
On confirmation runs `on_confirm`. The function receives a socket
|
||||
and should return the socket. Note that this socket always comes from
|
||||
the root LV level, keep this in mind when using in a component and
|
||||
dealing with assigns.
|
||||
|
||||
Make sure to render `confirm_root/1` in the layout.
|
||||
|
||||
## Options
|
||||
|
||||
* `:title` - title of the confirmation modal. Defaults to `"Are you sure?"`
|
||||
|
||||
* `:description` - content of the confirmation modal. Required
|
||||
|
||||
* `:confirm_text` - text of the confirm button. Defaults to `"Yes"`
|
||||
|
||||
* `:confirm_icon` - icon in the confirm button. Optional
|
||||
|
||||
* `:danger` - whether the action is destructive or regular. Defaults to `true`
|
||||
|
||||
* `:opt_out_id` - enables the "Don't show this message again"
|
||||
checkbox. Once checked by the user, the confirmation with this
|
||||
id is never shown again. Optional
|
||||
|
||||
"""
|
||||
def confirm(socket, on_confirm, opts) do
|
||||
opts =
|
||||
Keyword.validate!(
|
||||
opts,
|
||||
title: "Are you sure?",
|
||||
description: nil,
|
||||
confirm_text: "Yes",
|
||||
confirm_icon: nil,
|
||||
danger: true,
|
||||
opt_out_id: nil
|
||||
)
|
||||
|
||||
send(self(), {:confirm, on_confirm, opts})
|
||||
|
||||
socket
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the confirmation modal for `confirm/3`.
|
||||
"""
|
||||
|
||||
attr :confirm_state, :map, required: true
|
||||
|
||||
def confirm_root(assigns) do
|
||||
~H"""
|
||||
<.confirm_modal :if={@confirm_state} id={"confirm-#{@confirm_state.id}"} {@confirm_state.attrs} />
|
||||
"""
|
||||
end
|
||||
|
||||
defp confirm_modal(assigns) do
|
||||
~H"""
|
||||
<.modal id={@id} width={:medium} show={true}>
|
||||
<form
|
||||
id={"#{@id}-confirm-content"}
|
||||
class="p-6 flex flex-col"
|
||||
phx-submit={JS.push("confirm") |> hide_modal(@id)}
|
||||
data-el-confirm-form
|
||||
>
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
<%= @title %>
|
||||
</h3>
|
||||
<p class="mt-8 text-gray-700">
|
||||
<%= @description %>
|
||||
</p>
|
||||
<label :if={@opt_out_id} class="mt-6 text-gray-700 flex items-center">
|
||||
<input class="checkbox mr-3" type="checkbox" name="opt_out_id" value={@opt_out_id} />
|
||||
<span class="text-sm">
|
||||
Don't show this message again
|
||||
</span>
|
||||
</label>
|
||||
<div class="mt-8 flex justify-end">
|
||||
<div class={["flex gap-2", @danger && "flex-row-reverse"]}>
|
||||
<button class="button-base button-outlined-gray" type="button" phx-click={hide_modal(@id)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class={["button-base", if(@danger, do: "button-red", else: "button-blue")]}
|
||||
type="submit"
|
||||
>
|
||||
<.remix_icon :if={@confirm_icon} icon={@confirm_icon} class="align-middle mr-1" />
|
||||
<span><%= @confirm_text %></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</.modal>
|
||||
"""
|
||||
end
|
||||
|
||||
def on_mount(:default, _params, _session, socket) do
|
||||
connect_params = get_connect_params(socket) || %{}
|
||||
|
||||
# Opting out is a per-user preference, so we store it on the client
|
||||
# and send in the connect params
|
||||
confirm_opt_out_ids = connect_params["confirm_opt_out_ids"] || []
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(confirm_state: nil, confirm_opt_out_ids: MapSet.new(confirm_opt_out_ids))
|
||||
|> attach_hook(:confirm, :handle_event, &handle_event/3)
|
||||
|> attach_hook(:confirm, :handle_info, &handle_info/2)
|
||||
|
||||
{:cont, socket}
|
||||
end
|
||||
|
||||
defp handle_event("confirm", params, socket) do
|
||||
socket =
|
||||
if opt_out_id = params["opt_out_id"] do
|
||||
socket
|
||||
|> update(:confirm_opt_out_ids, &MapSet.put(&1, opt_out_id))
|
||||
|> push_event("add_confirm_opt_out_id", %{opt_out_id: opt_out_id})
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
socket = socket.assigns.confirm_state.on_confirm.(socket)
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
defp handle_event(_event, _params, socket), do: {:cont, socket}
|
||||
|
||||
defp handle_info({:confirm, on_confirm, opts}, socket) do
|
||||
socket =
|
||||
if opts[:opt_out_id] && opts[:opt_out_id] in socket.assigns.confirm_opt_out_ids do
|
||||
on_confirm.(socket)
|
||||
else
|
||||
assign(socket,
|
||||
confirm_state: %{
|
||||
id: Livebook.Utils.random_short_id(),
|
||||
on_confirm: on_confirm,
|
||||
attrs: opts
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
{:cont, socket}
|
||||
end
|
||||
|
||||
defp handle_info(_message, socket), do: {:cont, socket}
|
||||
end
|
|
@ -201,124 +201,6 @@ defmodule LivebookWeb.CoreComponents do
|
|||
|> JS.dispatch("click", to: "##{id}-return")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the confirmation modal for `with_confirm/3`.
|
||||
"""
|
||||
attr :id, :string, required: true
|
||||
|
||||
def confirm_modal(assigns) do
|
||||
~H"""
|
||||
<.modal id={@id} width={:medium} phx-hook="ConfirmModal" data-js-show={show_modal(@id)}>
|
||||
<div id={"#{@id}-confirm-content"} class="p-6 flex flex-col" phx-update="ignore">
|
||||
<h3 class="text-2xl font-semibold text-gray-800" data-title></h3>
|
||||
<p class="mt-8 text-gray-700" data-description></p>
|
||||
<label class="mt-6 text-gray-700 flex items-center" data-opt-out>
|
||||
<input class="checkbox mr-3" type="checkbox" />
|
||||
<span class="text-sm">
|
||||
Don't show this message again
|
||||
</span>
|
||||
</label>
|
||||
<div class="mt-8 flex justify-end">
|
||||
<div class="flex space-x-2" data-actions>
|
||||
<button class="button-base button-outlined-gray" phx-click={hide_modal(@id)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="button-base"
|
||||
phx-click={hide_modal(@id) |> JS.dispatch("lb:confirm", to: "##{@id}")}
|
||||
data-confirm-button
|
||||
>
|
||||
<i aria-hidden="true" data-confirm-icon></i>
|
||||
<span data-confirm-text></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.modal>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shows a confirmation modal before executing the given JS action.
|
||||
|
||||
The modal template must already be on the page, see `confirm_modal/1`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:title` - title of the confirmation modal. Defaults to `"Are you sure?"`
|
||||
|
||||
* `:description` - content of the confirmation modal. Required
|
||||
|
||||
* `:confirm_text` - text of the confirm button. Defaults to `"Yes"`
|
||||
|
||||
* `:confirm_icon` - icon in the confirm button. Optional
|
||||
|
||||
* `:danger` - whether the action is destructive or regular. Defaults to `true`
|
||||
|
||||
* `:html` - whether the `:description` is a raw HTML. Defaults to `false`
|
||||
|
||||
* `:opt_out_id` - enables the "Don't show this message again"
|
||||
checkbox. Once checked by the user, the confirmation with this
|
||||
id is never shown again. Optional
|
||||
|
||||
## Examples
|
||||
|
||||
<button class="..."
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_item", value: %{id: @item_id}),
|
||||
title: "Delete item",
|
||||
description: "Are you sure you want to delete item?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line",
|
||||
opt_out_id: "delete-item"
|
||||
)
|
||||
}>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
"""
|
||||
def with_confirm(js \\ %JS{}, on_confirm, opts) do
|
||||
JS.dispatch(js, "lb:confirm_request", detail: confirm_payload(on_confirm, opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asks the client to show a confirmation modal before executing the
|
||||
given JS action.
|
||||
|
||||
Same as `with_confirm/3`, except it is triggered from the server.
|
||||
"""
|
||||
def confirm(socket, on_confirm, opts) do
|
||||
Phoenix.LiveView.push_event(socket, "lb:confirm_request", confirm_payload(on_confirm, opts))
|
||||
end
|
||||
|
||||
defp confirm_payload(on_confirm, opts) do
|
||||
opts =
|
||||
Keyword.validate!(
|
||||
opts,
|
||||
[
|
||||
:confirm_icon,
|
||||
:description,
|
||||
:opt_out_id,
|
||||
title: "Are you sure?",
|
||||
confirm_text: "Yes",
|
||||
danger: true,
|
||||
html: false
|
||||
]
|
||||
)
|
||||
|
||||
%{
|
||||
on_confirm: Jason.encode!(on_confirm.ops),
|
||||
title: opts[:title],
|
||||
description: Keyword.fetch!(opts, :description),
|
||||
confirm_text: opts[:confirm_text],
|
||||
confirm_icon: opts[:confirm_icon],
|
||||
danger: opts[:danger],
|
||||
html: opts[:html],
|
||||
opt_out_id: opts[:opt_out_id]
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a popup menu that shows up on toggle click.
|
||||
|
||||
|
@ -636,6 +518,46 @@ defmodule LivebookWeb.CoreComponents do
|
|||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the text in singular or plural depending on the quantity.
|
||||
|
||||
## Examples
|
||||
|
||||
<.listing items={@packages}>
|
||||
<:item :let={package}><code><%= package.name %></code></:item>
|
||||
<:singular_suffix>package</:singular_suffix>
|
||||
<:plural_suffix>packages</:plural_suffix>
|
||||
</.listing>
|
||||
|
||||
"""
|
||||
|
||||
attr :items, :list, required: true
|
||||
|
||||
slot :item, required: true
|
||||
slot :plural_suffix
|
||||
slot :singular_suffix
|
||||
|
||||
def listing(%{items: [_]} = assigns) do
|
||||
~H"""
|
||||
<%= render_slot(@item, hd(@items)) %>
|
||||
<%= render_slot(@singular_suffix) %>
|
||||
"""
|
||||
end
|
||||
|
||||
def listing(%{items: [_, _ | _]} = assigns) do
|
||||
{items, assigns} = Map.pop!(assigns, :items)
|
||||
{leading, [second_to_last, last]} = Enum.split(items, -2)
|
||||
assigns = assign(assigns, leading: leading, second_to_last: second_to_last, last: last)
|
||||
|
||||
~H"""
|
||||
<%= for item <- @leading do %>
|
||||
<%= render_slot(@item, item) %>,
|
||||
<% end %>
|
||||
<%= render_slot(@item, @second_to_last) %> and <%= render_slot(@item, @last) %>
|
||||
<%= render_slot(@plural_suffix) %>
|
||||
"""
|
||||
end
|
||||
|
||||
# JS commands
|
||||
|
||||
@doc """
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
<%= @inner_content %>
|
||||
</main>
|
||||
|
||||
<.confirm_modal id="global-confirm" />
|
||||
<.confirm_root confirm_state={@confirm_state} />
|
||||
|
|
|
@ -74,34 +74,4 @@ defmodule LivebookWeb.Helpers do
|
|||
@spec pluralize(non_neg_integer(), String.t(), String.t()) :: String.t()
|
||||
def pluralize(1, singular, _plural), do: "1 #{singular}"
|
||||
def pluralize(count, _singular, plural), do: "#{count} #{plural}"
|
||||
|
||||
@doc """
|
||||
Returns the text in singular or plural depending on the quantity
|
||||
|
||||
## Examples
|
||||
|
||||
iex> LivebookWeb.Helpers.format_items(["tea"])
|
||||
"tea"
|
||||
|
||||
iex> LivebookWeb.Helpers.format_items(["tea", "coffee"])
|
||||
"tea and coffee"
|
||||
|
||||
iex> LivebookWeb.Helpers.format_items(["wine", "tea", "coffee"])
|
||||
"wine, tea and coffee"
|
||||
|
||||
"""
|
||||
@spec format_items(list(String.t())) :: String.t()
|
||||
def format_items([]), do: ""
|
||||
def format_items([item]), do: item
|
||||
|
||||
def format_items(list) do
|
||||
{leading, [last]} = Enum.split(list, -1)
|
||||
Enum.join(leading, ", ") <> " and " <> last
|
||||
end
|
||||
|
||||
@doc """
|
||||
Wraps the given text in a `<code>` tag.
|
||||
"""
|
||||
@spec code_tag(String.t()) :: String.t()
|
||||
def code_tag(text), do: "<code>#{text}</code>"
|
||||
end
|
||||
|
|
|
@ -59,15 +59,7 @@ defmodule LivebookWeb.EnvVarsComponent do
|
|||
<button
|
||||
id={"env-var-#{@env_var.name}-delete"}
|
||||
type="button"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_env_var", value: %{env_var: @env_var.name}),
|
||||
title: "Delete #{@env_var.name}",
|
||||
description: "Are you sure you want to delete environment variable?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)
|
||||
}
|
||||
phx-click={JS.push("delete_env_var", value: %{env_var: @env_var.name})}
|
||||
phx-target={@target}
|
||||
role="menuitem"
|
||||
>
|
||||
|
|
|
@ -120,15 +120,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
<span class="tooltip top" data-tooltip="Unstar">
|
||||
<button
|
||||
aria-label="unstar notebook"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("unstar_notebook", value: %{idx: idx}),
|
||||
title: "Unstar notebook",
|
||||
description: "Once you unstar this notebook, you can always star it again.",
|
||||
confirm_text: "Unstar",
|
||||
opt_out_id: "unstar-notebook"
|
||||
)
|
||||
}
|
||||
phx-click={JS.push("unstar_notebook", value: %{idx: idx})}
|
||||
>
|
||||
<.remix_icon icon="star-fill" class="text-yellow-600" />
|
||||
</button>
|
||||
|
@ -278,9 +270,19 @@ defmodule LivebookWeb.HomeLive do
|
|||
end
|
||||
|
||||
def handle_event("unstar_notebook", %{"idx" => idx}, socket) do
|
||||
%{file: file} = Enum.fetch!(socket.assigns.starred_notebooks, idx)
|
||||
Livebook.NotebookManager.remove_starred_notebook(file)
|
||||
{:noreply, socket}
|
||||
on_confirm = fn socket ->
|
||||
%{file: file} = Enum.fetch!(socket.assigns.starred_notebooks, idx)
|
||||
Livebook.NotebookManager.remove_starred_notebook(file)
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Unstar notebook",
|
||||
description: "Once you unstar this notebook, you can always star it again.",
|
||||
confirm_text: "Unstar",
|
||||
opt_out_id: "unstar-notebook"
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("bulk_action", %{"action" => "disconnect"} = params, socket) do
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule LivebookWeb.SidebarHook do
|
|||
|
||||
import Phoenix.Component
|
||||
import Phoenix.LiveView
|
||||
import LivebookWeb.Confirm
|
||||
|
||||
def on_mount(:default, _params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
|
@ -37,8 +38,18 @@ defmodule LivebookWeb.SidebarHook do
|
|||
defp handle_info(_event, socket), do: {:cont, socket}
|
||||
|
||||
defp handle_event("shutdown", _params, socket) do
|
||||
Livebook.Config.shutdown()
|
||||
{:halt, socket}
|
||||
on_confirm = fn socket ->
|
||||
Livebook.Config.shutdown()
|
||||
socket
|
||||
end
|
||||
|
||||
{:halt,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Shut Down",
|
||||
description: "Are you sure you want to shut down Livebook now?",
|
||||
confirm_text: "Shut Down",
|
||||
confirm_icon: "shut-down-line"
|
||||
)}
|
||||
end
|
||||
|
||||
defp handle_event(_event, _params, socket), do: {:cont, socket}
|
||||
|
|
|
@ -22,15 +22,7 @@ defmodule LivebookWeb.Hub.Edit.EnterpriseComponent do
|
|||
<LayoutHelpers.title text={"#{@hub.hub_emoji} #{@hub.hub_name}"} />
|
||||
|
||||
<button
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_hub", value: %{id: @hub.id}),
|
||||
title: "Delete hub",
|
||||
description: "Are you sure you want to delete this hub?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "close-circle-line"
|
||||
)
|
||||
}
|
||||
phx-click={JS.push("delete_hub", value: %{id: @hub.id})}
|
||||
class="absolute right-0 button-base button-red"
|
||||
>
|
||||
Delete hub
|
||||
|
|
|
@ -40,15 +40,7 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
|||
<LayoutHelpers.title text={"#{@hub.hub_emoji} #{@hub.hub_name}"} />
|
||||
|
||||
<button
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_hub", value: %{id: @hub.id}),
|
||||
title: "Delete hub",
|
||||
description: "Are you sure you want to delete this hub?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "close-circle-line"
|
||||
)
|
||||
}
|
||||
phx-click={JS.push("delete_hub", value: %{id: @hub.id})}
|
||||
class="absolute right-0 button-base button-red"
|
||||
>
|
||||
Delete hub
|
||||
|
@ -195,18 +187,26 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
|||
end
|
||||
|
||||
def handle_event("delete_env_var", %{"env_var" => key}, socket) do
|
||||
case FlyClient.unset_secrets(socket.assigns.hub, [key]) do
|
||||
{:ok, _} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:success, "Environment variable deleted")
|
||||
|> push_navigate(to: ~p"/hub/#{socket.assigns.hub.id}")}
|
||||
on_confirm = fn socket ->
|
||||
case FlyClient.unset_secrets(socket.assigns.hub, [key]) do
|
||||
{:ok, _} ->
|
||||
socket
|
||||
|> put_flash(:success, "Environment variable deleted")
|
||||
|> push_navigate(to: ~p"/hub/#{socket.assigns.hub.id}")
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, "Failed to delete environment variable")
|
||||
|> push_navigate(to: ~p"/hub/#{socket.assigns.hub.id}")}
|
||||
{:error, _} ->
|
||||
socket
|
||||
|> put_flash(:error, "Failed to delete environment variable")
|
||||
|> push_navigate(to: ~p"/hub/#{socket.assigns.hub.id}")
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Delete #{key}",
|
||||
description: "Are you sure you want to delete environment variable?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -214,19 +214,13 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do
|
|||
id={"hub-secret-#{@secret.name}-delete"}
|
||||
type="button"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_hub_secret",
|
||||
value: %{
|
||||
name: @secret.name,
|
||||
value: @secret.value,
|
||||
hub_id: @secret.hub_id
|
||||
},
|
||||
target: @target
|
||||
),
|
||||
title: "Delete hub secret - #{@secret.name}",
|
||||
description: "Are you sure you want to delete this hub secret?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
JS.push("delete_hub_secret",
|
||||
value: %{
|
||||
name: @secret.name,
|
||||
value: @secret.value,
|
||||
hub_id: @secret.hub_id
|
||||
},
|
||||
target: @target
|
||||
)
|
||||
}
|
||||
phx-target={@target}
|
||||
|
@ -265,10 +259,21 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do
|
|||
end
|
||||
|
||||
def handle_event("delete_hub_secret", attrs, socket) do
|
||||
{:ok, secret} = Livebook.Secrets.update_secret(%Livebook.Secrets.Secret{}, attrs)
|
||||
:ok = Livebook.Hubs.delete_secret(socket.assigns.hub, secret)
|
||||
%{hub: hub} = socket.assigns
|
||||
|
||||
{:noreply, socket}
|
||||
on_confirm = fn socket ->
|
||||
{:ok, secret} = Livebook.Secrets.update_secret(%Livebook.Secrets.Secret{}, attrs)
|
||||
:ok = Livebook.Hubs.delete_secret(hub, secret)
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Delete hub secret - #{attrs["name"]}",
|
||||
description: "Are you sure you want to delete this hub secret?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)}
|
||||
end
|
||||
|
||||
defp save(params, changeset_name, socket) do
|
||||
|
|
|
@ -78,12 +78,21 @@ defmodule LivebookWeb.Hub.EditLive do
|
|||
|
||||
@impl true
|
||||
def handle_event("delete_hub", %{"id" => id}, socket) do
|
||||
Hubs.delete_hub(id)
|
||||
on_confirm = fn socket ->
|
||||
Hubs.delete_hub(id)
|
||||
|
||||
socket
|
||||
|> put_flash(:success, "Hub deleted successfully")
|
||||
|> push_navigate(to: "/")
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:success, "Hub deleted successfully")
|
||||
|> push_navigate(to: "/")}
|
||||
confirm(socket, on_confirm,
|
||||
title: "Delete hub",
|
||||
description: "Are you sure you want to delete this hub?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "close-circle-line"
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -114,15 +114,7 @@ defmodule LivebookWeb.LayoutHelpers do
|
|||
:if={Livebook.Config.shutdown_callback()}
|
||||
class="h-7 flex items-center text-gray-400 hover:text-white border-l-4 border-transparent hover:border-white"
|
||||
aria-label="shutdown"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("shutdown"),
|
||||
title: "Shut Down",
|
||||
description: "Are you sure you want to shut down Livebook now?",
|
||||
confirm_text: "Shut Down",
|
||||
confirm_icon: "shut-down-line"
|
||||
)
|
||||
}
|
||||
phx-click="shutdown"
|
||||
>
|
||||
<.remix_icon icon="shut-down-line" class="text-lg leading-6 w-[56px] flex justify-center" />
|
||||
<span class="text-sm font-medium">
|
||||
|
|
|
@ -118,15 +118,7 @@ defmodule LivebookWeb.OpenLive do
|
|||
<span class="tooltip top" data-tooltip="Hide notebook">
|
||||
<button
|
||||
aria-label="hide notebook"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("hide_recent_notebook", value: %{idx: idx}),
|
||||
title: "Hide notebook",
|
||||
description: "The notebook will reappear here when you open it again.",
|
||||
confirm_text: "Hide",
|
||||
opt_out_id: "hide-notebook"
|
||||
)
|
||||
}
|
||||
phx-click={JS.push("hide_recent_notebook", value: %{idx: idx})}
|
||||
>
|
||||
<.remix_icon icon="close-fill" class="text-gray-600 text-lg" />
|
||||
</button>
|
||||
|
@ -194,9 +186,19 @@ defmodule LivebookWeb.OpenLive do
|
|||
end
|
||||
|
||||
def handle_event("hide_recent_notebook", %{"idx" => idx}, socket) do
|
||||
%{file: file} = Enum.fetch!(socket.assigns.recent_notebooks, idx)
|
||||
Livebook.NotebookManager.remove_recent_notebook(file)
|
||||
{:noreply, socket}
|
||||
on_confirm = fn socket ->
|
||||
%{file: file} = Enum.fetch!(socket.assigns.recent_notebooks, idx)
|
||||
Livebook.NotebookManager.remove_recent_notebook(file)
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Hide notebook",
|
||||
description: "The notebook will reappear here when you open it again.",
|
||||
confirm_text: "Hide",
|
||||
opt_out_id: "hide-notebook"
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -920,85 +920,101 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
def handle_event("insert_code_block_below", params, socket) do
|
||||
data = socket.private.data
|
||||
add_dependencies? = params["add_dependencies"] == true
|
||||
%{"section_id" => section_id, "cell_id" => cell_id} = params
|
||||
|
||||
with {:ok, section, index} <-
|
||||
section_with_next_index(data.notebook, params["section_id"], params["cell_id"]),
|
||||
{:ok, definition} <- code_block_definition_by_name(data, params["definition_name"]) do
|
||||
variant = Enum.fetch!(definition.variants, params["variant_idx"])
|
||||
dependencies = Enum.map(variant.packages, & &1.dependency)
|
||||
case code_block_definition_by_name(data, params["definition_name"]) do
|
||||
{:ok, definition} ->
|
||||
variant = Enum.fetch!(definition.variants, params["variant_idx"])
|
||||
dependencies = Enum.map(variant.packages, & &1.dependency)
|
||||
|
||||
has_dependencies? =
|
||||
dependencies == [] or Livebook.Runtime.has_dependencies?(data.runtime, dependencies)
|
||||
has_dependencies? =
|
||||
dependencies == [] or Livebook.Runtime.has_dependencies?(data.runtime, dependencies)
|
||||
|
||||
cond do
|
||||
has_dependencies? or add_dependencies? ->
|
||||
attrs = %{source: variant.source}
|
||||
Session.insert_cell(socket.assigns.session.pid, section.id, index, :code, attrs)
|
||||
cond do
|
||||
has_dependencies? ->
|
||||
insert_code_block_below(socket, variant, section_id, cell_id)
|
||||
{:noreply, socket}
|
||||
|
||||
socket =
|
||||
if has_dependencies? do
|
||||
socket
|
||||
else
|
||||
add_dependencies_and_reevaluate(socket, dependencies)
|
||||
Livebook.Runtime.fixed_dependencies?(data.runtime) ->
|
||||
{:noreply,
|
||||
put_flash(socket, :error, "This runtime doesn't support adding dependencies")}
|
||||
|
||||
true ->
|
||||
on_confirm = fn socket ->
|
||||
case insert_code_block_below(socket, variant, section_id, cell_id) do
|
||||
:ok -> add_dependencies_and_reevaluate(socket, dependencies)
|
||||
:error -> socket
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
socket =
|
||||
confirm_add_packages(socket, on_confirm, variant.packages, definition.name, "block")
|
||||
|
||||
Livebook.Runtime.fixed_dependencies?(data.runtime) ->
|
||||
{:noreply,
|
||||
put_flash(socket, :error, "This runtime doesn't support adding dependencies")}
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
true ->
|
||||
js = JS.push("insert_code_block_below", value: put_in(params["add_dependencies"], true))
|
||||
socket = confirm_add_packages(socket, js, variant.packages, definition.name, "block")
|
||||
{:noreply, socket}
|
||||
end
|
||||
else
|
||||
_ -> {:noreply, socket}
|
||||
_ ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("insert_smart_cell_below", params, socket) do
|
||||
data = socket.private.data
|
||||
add_dependencies? = params["add_dependencies"] == true
|
||||
%{"section_id" => section_id, "cell_id" => cell_id} = params
|
||||
|
||||
with {:ok, section, index} <-
|
||||
section_with_next_index(data.notebook, params["section_id"], params["cell_id"]),
|
||||
{:ok, definition} <- smart_cell_definition_by_kind(data, params["kind"]) do
|
||||
preset =
|
||||
if preset_idx = params["preset_idx"] do
|
||||
Enum.at(definition.requirement_presets, preset_idx)
|
||||
end
|
||||
|
||||
if preset == nil or add_dependencies? do
|
||||
attrs = %{kind: params["kind"]}
|
||||
Session.insert_cell(socket.assigns.session.pid, section.id, index, :smart, attrs)
|
||||
|
||||
socket =
|
||||
if preset == nil do
|
||||
socket
|
||||
else
|
||||
{:ok, preset} = Enum.fetch(definition.requirement_presets, preset_idx)
|
||||
dependencies = Enum.map(preset.packages, & &1.dependency)
|
||||
add_dependencies_and_reevaluate(socket, dependencies)
|
||||
case smart_cell_definition_by_kind(data, params["kind"]) do
|
||||
{:ok, definition} ->
|
||||
preset =
|
||||
if preset_idx = params["preset_idx"] do
|
||||
Enum.at(definition.requirement_presets, preset_idx)
|
||||
end
|
||||
|
||||
if preset == nil do
|
||||
insert_smart_cell_below(socket, definition, section_id, cell_id)
|
||||
{:noreply, socket}
|
||||
else
|
||||
on_confirm = fn socket ->
|
||||
case insert_smart_cell_below(socket, definition, section_id, cell_id) do
|
||||
:ok ->
|
||||
dependencies = Enum.map(preset.packages, & &1.dependency)
|
||||
add_dependencies_and_reevaluate(socket, dependencies)
|
||||
|
||||
:error ->
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
socket =
|
||||
confirm_add_packages(
|
||||
socket,
|
||||
on_confirm,
|
||||
preset.packages,
|
||||
definition.name,
|
||||
"smart cell"
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:noreply, socket}
|
||||
else
|
||||
js = JS.push("insert_smart_cell_below", value: put_in(params["add_dependencies"], true))
|
||||
socket = confirm_add_packages(socket, js, preset.packages, definition.name, "smart cell")
|
||||
{:noreply, socket}
|
||||
end
|
||||
else
|
||||
_ -> {:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
|
||||
Session.delete_cell(socket.assigns.session.pid, cell_id)
|
||||
on_confirm = fn socket ->
|
||||
Session.delete_cell(socket.assigns.session.pid, cell_id)
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Delete cell",
|
||||
description: "Once you delete this cell, it will be moved to the bin.",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line",
|
||||
opt_out_id: "delete-cell"
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("set_notebook_name", %{"value" => name}, socket) do
|
||||
|
@ -1078,9 +1094,20 @@ defmodule LivebookWeb.SessionLive do
|
|||
end
|
||||
|
||||
def handle_event("convert_smart_cell", %{"cell_id" => cell_id}, socket) do
|
||||
Session.convert_smart_cell(socket.assigns.session.pid, cell_id)
|
||||
on_confirm = fn socket ->
|
||||
Session.convert_smart_cell(socket.assigns.session.pid, cell_id)
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Convert cell",
|
||||
description:
|
||||
"Once you convert this Smart cell to a Code cell, the Smart cell will be moved to the bin.",
|
||||
confirm_text: "Convert",
|
||||
confirm_icon: "arrow-up-down-line",
|
||||
opt_out_id: "convert-smart-cell"
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("add_form_cell_dependencies", %{}, socket) do
|
||||
|
@ -1167,14 +1194,25 @@ defmodule LivebookWeb.SessionLive do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("setup_default_runtime", %{}, socket) do
|
||||
{status, socket} = connect_runtime(socket)
|
||||
def handle_event("setup_default_runtime", %{"reason" => reason}, socket) do
|
||||
on_confirm = fn socket ->
|
||||
{status, socket} = connect_runtime(socket)
|
||||
|
||||
if status == :ok do
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, Cell.setup_cell_id())
|
||||
if status == :ok do
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, Cell.setup_cell_id())
|
||||
end
|
||||
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Setup runtime",
|
||||
description: "#{reason} Do you want to connect and setup the default one?",
|
||||
confirm_text: "Setup runtime",
|
||||
confirm_icon: "play-line",
|
||||
danger: false
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("disconnect_runtime", %{}, socket) do
|
||||
|
@ -1855,32 +1893,45 @@ defmodule LivebookWeb.SessionLive do
|
|||
socket
|
||||
end
|
||||
|
||||
defp confirm_add_packages(socket, js, packages, target_name, target_type) do
|
||||
confirm(socket, js,
|
||||
title: "Add packages",
|
||||
description:
|
||||
case packages do
|
||||
[package] ->
|
||||
~s'''
|
||||
The <span class="font-semibold">“#{target_name}“</span> #{target_type} requires
|
||||
the #{code_tag(package.name)} package. Do you want to add it as a dependency
|
||||
and restart?
|
||||
'''
|
||||
defp confirm_add_packages(socket, on_confirm, packages, target_name, target_type) do
|
||||
assigns = %{packages: packages, target_name: target_name, target_type: target_type}
|
||||
|
||||
packages ->
|
||||
~s'''
|
||||
The <span class="font-semibold">“#{target_name}“</span> #{target_type} requires the
|
||||
#{packages |> Enum.map(&code_tag(&1.name)) |> format_items()} packages. Do you want
|
||||
to add them as dependencies and restart?
|
||||
'''
|
||||
end,
|
||||
description = ~H"""
|
||||
The <span class="font-semibold">“<%= @target_name %>“</span> <%= @target_type %> requires the
|
||||
<.listing items={@packages}>
|
||||
<:item :let={package}><code><%= package.name %></code></:item>
|
||||
<:singular_suffix>package. Do you want to add it as a dependency and restart?</:singular_suffix>
|
||||
<:plural_suffix>packages. Do you want to add them as dependencies and restart?</:plural_suffix>
|
||||
</.listing>
|
||||
"""
|
||||
|
||||
confirm(socket, on_confirm,
|
||||
title: "Add packages",
|
||||
description: description,
|
||||
confirm_text: "Add and restart",
|
||||
confirm_icon: "add-line",
|
||||
danger: false,
|
||||
html: true
|
||||
danger: false
|
||||
)
|
||||
end
|
||||
|
||||
defp insert_code_block_below(socket, variant, section_id, cell_id) do
|
||||
with {:ok, section, index} <-
|
||||
section_with_next_index(socket.private.data.notebook, section_id, cell_id) do
|
||||
attrs = %{source: variant.source}
|
||||
Session.insert_cell(socket.assigns.session.pid, section.id, index, :code, attrs)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp insert_smart_cell_below(socket, definition, section_id, cell_id) do
|
||||
with {:ok, section, index} <-
|
||||
section_with_next_index(socket.private.data.notebook, section_id, cell_id) do
|
||||
attrs = %{kind: definition.kind}
|
||||
Session.insert_cell(socket.assigns.session.pid, section.id, index, :smart, attrs)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Builds view-specific structure of data by cherry-picking
|
||||
# only the relevant attributes.
|
||||
# We then use `@data_view` in the templates and consequently
|
||||
|
|
|
@ -446,17 +446,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
class="icon-button"
|
||||
aria-label="toggle source"
|
||||
data-link-package-search
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("convert_smart_cell", value: %{cell_id: @cell_id}),
|
||||
title: "Convert cell",
|
||||
description:
|
||||
"Once you convert this Smart cell to a Code cell, the Smart cell will be moved to the bin.",
|
||||
confirm_text: "Convert",
|
||||
confirm_icon: "arrow-up-down-line",
|
||||
opt_out_id: "convert-smart-cell"
|
||||
)
|
||||
}
|
||||
phx-click={JS.push("convert_smart_cell", value: %{cell_id: @cell_id})}
|
||||
>
|
||||
<.remix_icon icon="pencil-line" class="text-xl" />
|
||||
</button>
|
||||
|
@ -563,16 +553,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
<button
|
||||
class="icon-button"
|
||||
aria-label="delete cell"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_cell", value: %{cell_id: @cell_id}),
|
||||
title: "Delete cell",
|
||||
description: "Once you delete this cell, it will be moved to the bin.",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line",
|
||||
opt_out_id: "delete-cell"
|
||||
)
|
||||
}
|
||||
phx-click={JS.push("delete_cell", value: %{cell_id: @cell_id})}
|
||||
>
|
||||
<.remix_icon icon="delete-bin-6-line" class="text-xl" />
|
||||
</button>
|
||||
|
|
|
@ -92,8 +92,8 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
|||
<button
|
||||
class="button-base button-small"
|
||||
phx-click={
|
||||
setup_runtime_with_confirm(
|
||||
"To see the available smart cells, you need a connected runtime."
|
||||
JS.push("setup_default_runtime",
|
||||
value: %{reason: "To see the available smart cells, you need a connected runtime."}
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -194,21 +194,12 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
|||
}
|
||||
)
|
||||
else
|
||||
setup_runtime_with_confirm("To insert this block, you need a connected runtime.")
|
||||
JS.push("setup_default_runtime",
|
||||
value: %{reason: "To insert this block, you need a connected runtime."}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp setup_runtime_with_confirm(reason) do
|
||||
with_confirm(
|
||||
JS.push("setup_default_runtime"),
|
||||
title: "Setup runtime",
|
||||
description: "#{reason} Do you want to connect and setup the default one?",
|
||||
confirm_text: "Setup runtime",
|
||||
confirm_icon: "play-line",
|
||||
danger: false
|
||||
)
|
||||
end
|
||||
|
||||
defp on_smart_cell_click(definition, section_id, cell_id) do
|
||||
preset_idx = if definition.requirement_presets == [], do: nil, else: 0
|
||||
on_smart_cell_click(definition, preset_idx, section_id, cell_id)
|
||||
|
|
|
@ -46,16 +46,7 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
|
|||
id={"session-secret-#{secret.name}-delete"}
|
||||
type="button"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_session_secret",
|
||||
value: %{secret_name: secret.name},
|
||||
target: @myself
|
||||
),
|
||||
title: "Delete session secret - #{secret.name}",
|
||||
description: "Are you sure you want to delete this session secret?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)
|
||||
JS.push("delete_session_secret", value: %{secret_name: secret.name}, target: @myself)
|
||||
}
|
||||
class="hover:text-gray-900"
|
||||
>
|
||||
|
@ -158,19 +149,13 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
|
|||
id={"#{@id}-delete"}
|
||||
type="button"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_hub_secret",
|
||||
value: %{
|
||||
name: @secret.name,
|
||||
value: @secret.value,
|
||||
hub_id: @secret.hub_id
|
||||
},
|
||||
target: @myself
|
||||
),
|
||||
title: "Delete hub secret - #{@secret.name}",
|
||||
description: "Are you sure you want to delete this hub secret?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
JS.push("delete_hub_secret",
|
||||
value: %{
|
||||
name: @secret.name,
|
||||
value: @secret.value,
|
||||
hub_id: @secret.hub_id
|
||||
},
|
||||
target: @myself
|
||||
)
|
||||
}
|
||||
class="hover:text-gray-900"
|
||||
|
@ -222,15 +207,36 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
|
|||
end
|
||||
|
||||
def handle_event("delete_session_secret", %{"secret_name" => secret_name}, socket) do
|
||||
Session.unset_secret(socket.assigns.session.pid, secret_name)
|
||||
{:noreply, socket}
|
||||
on_confirm = fn socket ->
|
||||
Session.unset_secret(socket.assigns.session.pid, secret_name)
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Delete session secret - #{secret_name}",
|
||||
description: "Are you sure you want to delete this session secret?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("delete_hub_secret", attrs, socket) do
|
||||
{:ok, secret} = Secrets.update_secret(%Secret{}, attrs)
|
||||
:ok = Hubs.delete_secret(socket.assigns.hub, secret)
|
||||
:ok = Session.unset_secret(socket.assigns.session.pid, secret.name)
|
||||
%{hub: hub, session: session} = socket.assigns
|
||||
|
||||
{:noreply, socket}
|
||||
on_confirm = fn socket ->
|
||||
{:ok, secret} = Secrets.update_secret(%Secret{}, attrs)
|
||||
:ok = Hubs.delete_secret(hub, secret)
|
||||
:ok = Session.unset_secret(session.pid, secret.name)
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Delete hub secret - #{attrs["name"]}",
|
||||
description: "Are you sure you want to delete this hub secret?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -305,14 +305,24 @@ defmodule LivebookWeb.SettingsLive do
|
|||
end
|
||||
|
||||
def handle_event("detach_file_system", %{"id" => file_system_id}, socket) do
|
||||
Livebook.Settings.remove_file_system(file_system_id)
|
||||
on_confirm = fn socket ->
|
||||
Livebook.Settings.remove_file_system(file_system_id)
|
||||
|
||||
file_systems = Livebook.Settings.file_systems()
|
||||
file_systems = Livebook.Settings.file_systems()
|
||||
|
||||
assign(socket,
|
||||
file_systems: file_systems,
|
||||
default_file_system_id: Livebook.Settings.default_file_system_id()
|
||||
)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
file_systems: file_systems,
|
||||
default_file_system_id: Livebook.Settings.default_file_system_id()
|
||||
confirm(socket, on_confirm,
|
||||
title: "Detach file system",
|
||||
description:
|
||||
"Are you sure you want to detach this file system? Any sessions using it will keep the access until they get closed.",
|
||||
confirm_text: "Detach",
|
||||
confirm_icon: "close-circle-line"
|
||||
)}
|
||||
end
|
||||
|
||||
|
@ -344,8 +354,18 @@ defmodule LivebookWeb.SettingsLive do
|
|||
end
|
||||
|
||||
def handle_event("delete_env_var", %{"env_var" => key}, socket) do
|
||||
Livebook.Settings.unset_env_var(key)
|
||||
{:noreply, socket}
|
||||
on_confirm = fn socket ->
|
||||
Livebook.Settings.unset_env_var(key)
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Delete #{key}",
|
||||
description: "Are you sure you want to delete environment variable?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -79,16 +79,7 @@ defmodule LivebookWeb.SettingsLive.FileSystemsComponent do
|
|||
type="button"
|
||||
role="menuitem"
|
||||
class="text-red-600"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("detach_file_system", value: %{id: @file_system_id}),
|
||||
title: "Detach file system",
|
||||
description:
|
||||
"Are you sure you want to detach this file system? Any sessions using it will keep the access until they get closed.",
|
||||
confirm_text: "Detach",
|
||||
confirm_icon: "close-circle-line"
|
||||
)
|
||||
}
|
||||
phx-click={JS.push("detach_file_system", value: %{id: @file_system_id})}
|
||||
>
|
||||
<.remix_icon icon="delete-bin-line" />
|
||||
<span>Detach</span>
|
||||
|
|
|
@ -54,7 +54,8 @@ defmodule LivebookWeb.Router do
|
|||
get "/sessions/:id/assets/:hash/*file_parts", SessionController, :show_asset
|
||||
end
|
||||
|
||||
live_session :default, on_mount: [LivebookWeb.AuthHook, LivebookWeb.UserHook] do
|
||||
live_session :default,
|
||||
on_mount: [LivebookWeb.AuthHook, LivebookWeb.UserHook, LivebookWeb.Confirm] do
|
||||
scope "/", LivebookWeb do
|
||||
pipe_through [:browser, :auth]
|
||||
|
||||
|
@ -106,7 +107,8 @@ defmodule LivebookWeb.Router do
|
|||
end
|
||||
end
|
||||
|
||||
live_session :apps, on_mount: [LivebookWeb.AppAuthHook, LivebookWeb.UserHook] do
|
||||
live_session :apps,
|
||||
on_mount: [LivebookWeb.AppAuthHook, LivebookWeb.UserHook, LivebookWeb.Confirm] do
|
||||
scope "/", LivebookWeb do
|
||||
pipe_through [:browser, :user]
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
use LivebookWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Livebook.TestHelpers
|
||||
|
||||
alias Livebook.Hubs
|
||||
|
||||
|
@ -65,9 +66,13 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
view
|
||||
|> element("button", "Delete hub")
|
||||
|> render_click()
|
||||
|
||||
assert {:ok, view, _html} =
|
||||
view
|
||||
|> render_click("delete_hub", %{"id" => hub_id})
|
||||
|> render_confirm()
|
||||
|> follow_redirect(conn)
|
||||
|
||||
hubs_html = view |> element("#hubs") |> render()
|
||||
|
@ -191,10 +196,13 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
|
||||
:ok = Agent.update(pid, fn state -> %{state | type: :mount} end)
|
||||
|
||||
view
|
||||
|> element(~s/#env-var-FOO_ENV_VAR-delete/)
|
||||
|> render_click()
|
||||
|
||||
assert {:ok, _view, html} =
|
||||
view
|
||||
|> with_target("#fly-form-component")
|
||||
|> render_click("delete_env_var", %{"env_var" => "FOO_ENV_VAR"})
|
||||
|> render_confirm()
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert html =~ "Environment variable deleted"
|
||||
|
@ -355,16 +363,14 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
|> has_element?()
|
||||
|
||||
view
|
||||
|> with_target("#personal-form-component")
|
||||
|> render_click("delete_hub_secret", %{
|
||||
name: secret.name,
|
||||
value: secret.value,
|
||||
hub_id: secret.hub_id
|
||||
})
|
||||
|> element("#hub-secret-PERSONAL_DELETE_SECRET-delete", "Delete")
|
||||
|> render_click()
|
||||
|
||||
render_confirm(view)
|
||||
|
||||
assert_receive {:secret_deleted, ^secret}
|
||||
assert render(view) =~ "Secret deleted successfully"
|
||||
refute render(view) =~ secret.name
|
||||
refute render(element(view, "#hub-secrets-list")) =~ secret.name
|
||||
refute secret in Livebook.Hubs.get_secrets(hub)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
use LivebookWeb.ConnCase, async: true
|
||||
|
||||
import Livebook.SessionHelpers
|
||||
import Livebook.TestHelpers
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Livebook.{Sessions, Session, Settings, Runtime, Users, FileSystem}
|
||||
|
@ -246,6 +247,8 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|> element(~s{[data-el-session]})
|
||||
|> render_hook("delete_cell", %{"cell_id" => cell_id})
|
||||
|
||||
render_confirm(view)
|
||||
|
||||
assert %{notebook: %{sections: [%{cells: []}]}} = Session.get_data(session.pid)
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule LivebookWeb.SettingsLiveTest do
|
|||
@moduletag :tmp_dir
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Livebook.TestHelpers
|
||||
|
||||
alias Livebook.Settings
|
||||
|
||||
|
@ -75,9 +76,15 @@ defmodule LivebookWeb.SettingsLiveTest do
|
|||
|
||||
assert html =~ env_var.name
|
||||
|
||||
render_click(view, "delete_env_var", %{"env_var" => env_var.name})
|
||||
view
|
||||
|> element("#env-var-#{env_var.name}-delete")
|
||||
|> render_click()
|
||||
|
||||
refute render(view) =~ env_var.name
|
||||
render_confirm(view)
|
||||
|
||||
refute view
|
||||
|> element("#env-vars")
|
||||
|> render() =~ env_var.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule Livebook.TestHelpers do
|
||||
@moduledoc false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Livebook.Session.Data
|
||||
|
||||
@doc """
|
||||
|
@ -44,4 +46,14 @@ defmodule Livebook.TestHelpers do
|
|||
Converts a Unix-like absolute path into OS-compatible absolute path.
|
||||
"""
|
||||
defmacro p("/" <> path), do: Path.expand("/") <> path
|
||||
|
||||
@doc """
|
||||
Confirms the action guarded by `LivebookWeb.Confirm/3` and
|
||||
returns the rendered result.
|
||||
"""
|
||||
def render_confirm(view) do
|
||||
view
|
||||
|> element(~s/[data-el-confirm-form]/)
|
||||
|> render_submit()
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue