defmodule LivebookWeb.Hub.FileSystemFormComponent do use LivebookWeb, :live_component alias Livebook.FileSystem alias Livebook.FileSystems @impl true def mount(socket) do {:ok, assign(socket, error_message: nil)} end @impl true def update(assigns, socket) do {file_system, assigns} = Map.pop!(assigns, :file_system) mode = mode(file_system) title = title(file_system) button = button_attrs(file_system) file_system = file_system || %FileSystem.S3{hub_id: assigns.hub.id} changeset = FileSystems.change_file_system(file_system) {:ok, socket |> assign_new(:hub, fn -> assigns.hub end) |> assign_new(:file_system, fn -> file_system end) |> assign_new(:type, fn -> FileSystems.type(file_system) end) |> assign_new(:mode, fn -> mode end) |> assign_new(:title, fn -> title end) |> assign_new(:button, fn -> button end) |> assign_new(:return_to, fn -> assigns.return_to end) |> assign_new(:disabled, fn -> assigns.disabled end) |> assign_form(changeset)} end @impl true def render(assigns) do ~H"""

{@title}

{@error_message}
<.radio_card_input id="file_system_type-s3" name="file_system[type]" title="S3" phx-click={JS.push("select_type", value: %{value: :s3})} phx-target={@myself} value={@type} checked_value={:s3} disabled={@mode == :edit} > Configure an AWS S3 bucket as a Livebook file storage. Many storage services offer an S3-compatible API and those work as well. <.radio_card_input id="file_system_type-git" name="file_system[type]" title="Git" phx-click={JS.push("select_type", value: %{value: :git})} phx-target={@myself} value={@type} checked_value={:git} disabled={@mode == :edit} > Configure a Git repository as a read-only Livebook file storage. You will need a valid SSH key to access your Git repository.
<.form id="file-systems-form" for={@form} phx-target={@myself} phx-submit="save" phx-change="validate" autocomplete="off" spellcheck="false" >
<.file_system_form_fields {assigns} />
<.button type="submit" disabled={@disabled or not @form.source.valid?} > <.spinner class="hidden phx-submit-loading:block mr-1" /> <.remix_icon icon={@button.icon} /> {@button.label} <.button color="gray" outlined patch={@return_to}> Cancel
""" end defp file_system_form_fields(%{file_system: %FileSystem.S3{}} = assigns) do ~H"""
<.text_field field={@form[:bucket_url]} label="Bucket URL" placeholder="https://s3.[region].amazonaws.com/[bucket]" /> <.text_field field={@form[:region]} label="Region (optional)" placeholder={FileSystem.S3.region_from_url(@form[:bucket_url].value || "")} /> <%= if Livebook.Config.aws_credentials?() do %> <.password_field field={@form[:access_key_id]} label="Access Key ID (optional)" /> <.password_field field={@form[:secret_access_key]} label="Secret Access Key (optional)" />

You may leave Access Key fields empty. In such cases, they will be automatically read from your environment variables, AWS credentials, or Amazon EC2/ECS metadata.

<% else %> <.password_field field={@form[:access_key_id]} label="Access Key ID" /> <.password_field field={@form[:secret_access_key]} label="Secret Access Key" />

Start Livebook with LIVEBOOK_AWS_CREDENTIALS environment variable set if you want to automatically read credentials from environment variables, AWS credentials, or Amazon EC2/ECS metadata.

<% end %>
""" end defp file_system_form_fields(%{file_system: %FileSystem.Git{}} = assigns) do ~H"""
<.text_field field={@form[:repo_url]} label="Repository URL" placeholder="git@[provider_host]:[owner]/[repo_name].git" /> <.text_field field={@form[:branch]} label="Branch" placeholder="main" /> <.password_field field={@form[:key]} label="SSH Private Key" />

An SSH private key with repository access. You may use a personal key or a deploy key. See your Git provider’s documentation to create a deploy key.

""" end @impl true def handle_event("select_type", %{"value" => type}, socket) do if to_string(socket.assigns.type) == to_string(type) do {:noreply, socket} else file_system = FileSystems.load(type, %{"hub_id" => socket.assigns.hub.id}) changeset = FileSystems.change_file_system(file_system) {:noreply, socket |> assign(type: type, file_system: file_system) |> assign_form(changeset)} end end def handle_event("validate", %{"file_system" => attrs}, socket) do changeset = FileSystems.change_file_system(socket.assigns.file_system, attrs) {:noreply, assign_form(socket, changeset)} end def handle_event("save", %{"file_system" => attrs}, socket) do changeset = prepare_to_save(socket.assigns.file_system, attrs) with {:ok, file_system} <- Ecto.Changeset.apply_action(changeset, :update), :ok <- check_file_system_connectivity(file_system, socket), :ok <- save_file_system(file_system, changeset, socket) do message = case socket.assigns.mode do :new -> "File storage added successfully" :edit -> "File storage updated successfully" end {:noreply, socket |> put_flash(:success, message) |> push_patch(to: socket.assigns.return_to)} else {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign_form(socket, changeset)} {:transport_error, message} -> {:noreply, assign(socket, error_message: message)} {:error, message} -> {:noreply, assign(socket, error_message: message)} end end defp check_file_system_connectivity(file_system, socket) do default_path = FileSystem.default_path(file_system) with :ok <- FileSystem.mount(file_system), {:ok, _} <- FileSystem.list(file_system, default_path, false) do if socket.assigns.mode == :new do FileSystem.unmount(file_system) end :ok else {:error, message} -> {:error, "Connection test failed: " <> message} end end defp save_file_system(file_system, changeset, socket) do result = case socket.assigns.mode do :new -> Livebook.Hubs.create_file_system(socket.assigns.hub, file_system) :edit -> Livebook.Hubs.update_file_system(socket.assigns.hub, file_system) end with {:error, errors} <- result do {:error, changeset |> Livebook.Utils.put_changeset_errors(errors) |> Map.replace!(:action, :validate)} end end defp prepare_to_save(file_system, attrs) do changeset = FileSystems.change_file_system(file_system, attrs) if FileSystems.type(file_system) == "s3" do FileSystem.S3.maybe_put_region_from_url(changeset) else changeset end end defp assign_form(socket, changeset) do changeset = Map.replace!(changeset, :action, :validate) assign(socket, :form, to_form(changeset, as: :file_system)) end defp mode(nil), do: :new defp mode(_), do: :edit defp title(nil), do: "Add file storage" defp title(_), do: "Edit file storage" defp button_attrs(nil), do: %{icon: "add-line", label: "Add"} defp button_attrs(_), do: %{icon: "save-line", label: "Save"} end