Current section

Files

Jump to
livebook lib livebook_web live hub file_system_form_component.ex
Raw

lib/livebook_web/live/hub/file_system_form_component.ex

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"""
<div class="flex flex-col space-y-5">
<h3 class="text-2xl font-semibold text-gray-800">
{@title}
</h3>
<div :if={@error_message} class="error-box">
{@error_message}
</div>
<div>
<label class="mb-2 flex items-center gap-1 text-sm text-gray-800 font-medium">
Type
</label>
<div class="flex gap-y-6 sm:gap-x-4">
<.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>
<.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.
</.radio_card_input>
</div>
</div>
<.form
id="file-systems-form"
for={@form}
phx-target={@myself}
phx-submit="save"
phx-change="validate"
autocomplete="off"
spellcheck="false"
>
<div class="flex flex-col space-y-4">
<.file_system_form_fields {assigns} />
<div class="flex space-x-2">
<.button
type="submit"
disabled={@disabled or not @form.source.valid?}
>
<.spinner class="hidden phx-submit-loading:block mr-1" />
<.remix_icon icon={@button.icon} />
<span class="font-normal">{@button.label}</span>
</.button>
<.button color="gray" outlined patch={@return_to}>
Cancel
</.button>
</div>
</div>
</.form>
</div>
"""
end
defp file_system_form_fields(%{file_system: %FileSystem.S3{}} = assigns) do
~H"""
<div class="flex flex-col space-y-4">
<.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)" />
<p class="text-xs text-gray-700">
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.
</p>
<% else %>
<.password_field field={@form[:access_key_id]} label="Access Key ID" />
<.password_field field={@form[:secret_access_key]} label="Secret Access Key" />
<p class="text-xs text-gray-700">
Start Livebook with <code>LIVEBOOK_AWS_CREDENTIALS</code> environment
variable set if you want to automatically read credentials from
environment variables, AWS credentials, or Amazon EC2/ECS metadata.
</p>
<% end %>
</div>
"""
end
defp file_system_form_fields(%{file_system: %FileSystem.Git{}} = assigns) do
~H"""
<div class="flex flex-col space-y-4">
<.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" />
<p class="text-xs text-gray-700">
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.
</p>
</div>
"""
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