defmodule LivebookWeb.SessionLive.SaveRuntimeConfigComponent do use LivebookWeb, :live_component import Ecto.Changeset @impl true def mount(socket) do {:ok, assign(socket, save_config: nil)} end @impl true def update(assigns, socket) do socket = assign(socket, assigns) socket = case {socket.assigns.save_config_payload, socket.assigns.save_config} do {nil, nil} -> socket {_, nil} -> deafult_name = socket.assigns.secret_prefix changeset = config_secret_changeset(socket, %{name: deafult_name}) save_config = %{changeset: changeset, inflight: false, error: false} assign(socket, save_config: save_config) {nil, _} -> assign(socket, save_config: nil) {_, _} -> socket end {:ok, socket} end @impl true def render(assigns) do ~H"""
<%= if @save_config do %> <.save_config_form save_config={@save_config} hub={@hub} myself={@myself} /> <% else %> <.config_actions secret_prefix={@secret_prefix} hub_secrets={@hub_secrets} myself={@myself} /> <% end %>
""" end defp config_actions(assigns) do ~H"""
<.button color="gray" outlined small type="button" phx-click="open_save_config" phx-target={@myself} > Save config <.menu id="config-secret-menu"> <:toggle> <.button color="gray" outlined small type="button"> Load config <.remix_icon icon="arrow-down-s-line" class="text-base leading-none" />
No configs saved yet
<.menu_item :for={name <- config_secret_names(@hub_secrets, @secret_prefix)}>
""" end defp save_config_form(assigns) do ~H""" <.form :let={f} for={@save_config.changeset} as={:secret} class="mt-4 flex flex-col" phx-change="validate_save_config" phx-submit="save_config" phx-target={@myself} autocomplete="off" spellcheck="false" >
Save config
Store the config in a secret in the <.workspace hub={@hub} /> workspace to reuse it later.
<.message_box kind="error" message={error} />
<.text_field field={f[:name]} label="Secret name" class="uppercase" autofocus />
<.button type="submit" disabled={not @save_config.changeset.valid? or @save_config.inflight}> {if(@save_config.inflight, do: "Saving...", else: "Save")} <.button color="gray" outlined type="button" phx-click="cancel_save_config" phx-target={@myself} > Cancel
""" end defp workspace(assigns) do ~H""" {@hub.hub_emoji} {@hub.hub_name} """ end @impl true def handle_event("open_save_config", %{}, socket) do send_event(socket.assigns.target, :open_save_config) {:noreply, socket} end def handle_event("cancel_save_config", %{}, socket) do send_event(socket.assigns.target, :close_save_config) {:noreply, socket} end def handle_event("validate_save_config", %{"secret" => secret}, socket) do changeset = socket |> config_secret_changeset(secret) |> Map.replace!(:action, :validate) {:noreply, assign_nested(socket, :save_config, changeset: changeset)} end def handle_event("save_config", %{"secret" => secret}, socket) do changeset = config_secret_changeset(socket, secret) case Ecto.Changeset.apply_action(changeset, :insert) do {:ok, secret} -> {:noreply, save_config_secret(socket, secret, changeset)} {:error, changeset} -> {:noreply, assign_nested(socket, :save_config, changeset: changeset)} end end def handle_event("load_config", %{"name" => name}, socket) do secret = Enum.find(socket.assigns.hub_secrets, &(&1.name == name)) case JSON.decode(secret.value) do {:ok, config_defaults} -> send_event(socket.assigns.target, {:load_config, config_defaults}) {:noreply, socket} {:error, _} -> {:noreply, socket} end end @impl true def handle_async(:save_config, {:ok, result}, socket) do socket = case result do :ok -> send_event(socket.assigns.target, :close_save_config) assign_nested(socket, :save_config, inflight: false) {:error, %Ecto.Changeset{} = changeset} -> assign_nested(socket, :save_config, changeset: changeset, inflight: false) {:transport_error, error} -> assign_nested(socket, :save_config, error: error, inflight: false) end {:noreply, socket} end defp config_secret_names(hub_secrets, secret_prefix) do names = for %{name: name} <- hub_secrets, String.starts_with?(name, secret_prefix), do: name Enum.sort(names) end defp config_secret_changeset(socket, attrs) do secret_prefix = socket.assigns.secret_prefix hub = socket.assigns.hub value = JSON.encode!(socket.assigns.save_config_payload) secret = %Livebook.Secrets.Secret{hub_id: hub.id, name: nil, value: value} secret |> Livebook.Secrets.change_secret(attrs) |> validate_format(:name, ~r/^#{secret_prefix}\w+$/, message: "must be in the format #{secret_prefix}*" ) end defp save_config_secret(socket, secret, changeset) do hub = socket.assigns.hub exists? = Enum.any?(socket.assigns.hub_secrets, &(&1.name == secret.name)) socket |> start_async(:save_config, fn -> result = if exists? do Livebook.Hubs.update_secret(hub, secret) else Livebook.Hubs.create_secret(hub, secret) end with {:error, errors} <- result do {:error, changeset |> Livebook.Utils.put_changeset_errors(errors) |> Map.replace!(:action, :validate)} end end) |> assign_nested(:save_config, inflight: true) end end