diff --git a/lib/livebook/utils.ex b/lib/livebook/utils.ex index 59d614c66..24a561f0b 100644 --- a/lib/livebook/utils.ex +++ b/lib/livebook/utils.ex @@ -174,7 +174,7 @@ defmodule Livebook.Utils do if valid_url?(url) do [] else - [{:url, "must be a valid URL"}] + [{field, "must be a valid URL"}] end end) end @@ -257,13 +257,16 @@ defmodule Livebook.Utils do iex> Livebook.Utils.expand_url("https://example.com/lib/file.ex?token=supersecret", "../root.ex") "https://example.com/root.ex?token=supersecret" + iex> Livebook.Utils.expand_url("https://example.com", "./root.ex") + "https://example.com/root.ex" + """ @spec expand_url(String.t(), String.t()) :: String.t() def expand_url(url, relative_path) do url |> URI.parse() |> Map.update!(:path, fn path -> - Livebook.FileSystem.Utils.resolve_unix_like_path(path, relative_path) + Livebook.FileSystem.Utils.resolve_unix_like_path(path || "/", relative_path) end) |> URI.to_string() end diff --git a/lib/livebook_web/live/open_live/source_component.ex b/lib/livebook_web/live/open_live/source_component.ex index f06bb9d86..232379284 100644 --- a/lib/livebook_web/live/open_live/source_component.ex +++ b/lib/livebook_web/live/open_live/source_component.ex @@ -1,9 +1,19 @@ defmodule LivebookWeb.OpenLive.SourceComponent do use LivebookWeb, :live_component + import Ecto.Changeset + @impl true def mount(socket) do - {:ok, assign(socket, source: "")} + {:ok, assign(socket, changeset: changeset())} + end + + defp changeset(attrs \\ %{}) do + data = %{source: nil} + types = %{source: :string} + + cast({data, types}, attrs, [:source]) + |> validate_required([:source]) end @impl true @@ -16,7 +26,7 @@ defmodule LivebookWeb.OpenLive.SourceComponent do

<.form :let={f} - for={%{"source" => @source}} + for={@changeset} as={:data} id="import-source" phx-submit="import" @@ -25,7 +35,6 @@ defmodule LivebookWeb.OpenLive.SourceComponent do autocomplete="off" > <.textarea_field - type="textarea" field={f[:source]} label="Notebook source" resizable={false} @@ -34,7 +43,7 @@ defmodule LivebookWeb.OpenLive.SourceComponent do spellcheck="false" rows="5" /> - @@ -43,13 +52,22 @@ defmodule LivebookWeb.OpenLive.SourceComponent do end @impl true - def handle_event("validate", %{"data" => %{"source" => source}}, socket) do - {:noreply, assign(socket, source: source)} + def handle_event("validate", %{"data" => data}, socket) do + changeset = data |> changeset() |> Map.replace!(:action, :validate) + {:noreply, assign(socket, changeset: changeset)} end - def handle_event("import", %{"data" => %{"source" => source}}, socket) do - send(self(), {:import_source, source, []}) + def handle_event("import", %{"data" => data}, socket) do + data + |> changeset() + |> apply_action(:insert) + |> case do + {:ok, data} -> + send(self(), {:import_source, data.source, []}) + {:noreply, socket} - {:noreply, socket} + {:error, changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end end end diff --git a/lib/livebook_web/live/open_live/url_component.ex b/lib/livebook_web/live/open_live/url_component.ex index 4342aaf53..654fcf3a9 100644 --- a/lib/livebook_web/live/open_live/url_component.ex +++ b/lib/livebook_web/live/open_live/url_component.ex @@ -1,17 +1,28 @@ defmodule LivebookWeb.OpenLive.UrlComponent do use LivebookWeb, :live_component + import Ecto.Changeset + alias Livebook.{Utils, Notebook} @impl true def mount(socket) do - {:ok, assign(socket, url: "", error_message: nil)} + {:ok, assign(socket, changeset: changeset(), error_message: nil)} + end + + defp changeset(attrs \\ %{}) do + data = %{url: nil} + types = %{url: :string} + + cast({data, types}, attrs, [:url]) + |> validate_required([:url]) + |> Livebook.Utils.validate_url(:url) end @impl true def update(assigns, socket) do if url = assigns[:url] do - {:ok, do_import(socket, url)} + {:ok, do_import(socket, %{url: url})} else {:ok, socket} end @@ -29,7 +40,7 @@ defmodule LivebookWeb.OpenLive.UrlComponent do

<.form :let={f} - for={%{"url" => @url}} + for={@changeset} as={:data} phx-submit="import" phx-change="validate" @@ -43,11 +54,7 @@ defmodule LivebookWeb.OpenLive.UrlComponent do aria-labelledby="import-from-url" spellcheck="false" /> - @@ -56,27 +63,41 @@ defmodule LivebookWeb.OpenLive.UrlComponent do end @impl true - def handle_event("validate", %{"data" => %{"url" => url}}, socket) do - {:noreply, assign(socket, url: url)} + def handle_event("validate", %{"data" => data}, socket) do + changeset = data |> changeset() |> Map.replace!(:action, :validate) + {:noreply, assign(socket, changeset: changeset)} end - def handle_event("import", %{"data" => %{"url" => url}}, socket) do - {:noreply, do_import(socket, url)} + def handle_event("import", %{"data" => data}, socket) do + {:noreply, do_import(socket, data)} end - defp do_import(socket, url) do - origin = Notebook.ContentLoader.url_to_location(url) - files_url = Livebook.Utils.expand_url(url, "files/") - - origin - |> Notebook.ContentLoader.fetch_content_from_location() + defp do_import(socket, data) do + data + |> changeset() + |> apply_action(:insert) |> case do - {:ok, content} -> - send(self(), {:import_source, content, [origin: origin, files_source: {:url, files_url}]}) - socket + {:ok, data} -> + origin = Notebook.ContentLoader.url_to_location(data.url) + files_url = Livebook.Utils.expand_url(data.url, "files/") - {:error, message} -> - assign(socket, url: url, error_message: Utils.upcase_first(message)) + origin + |> Notebook.ContentLoader.fetch_content_from_location() + |> case do + {:ok, content} -> + opts = [origin: origin, files_source: {:url, files_url}] + send(self(), {:import_source, content, opts}) + socket + + {:error, message} -> + assign(socket, + changeset: changeset(data), + error_message: Utils.upcase_first(message) + ) + end + + {:error, changeset} -> + assign(socket, changeset: changeset) end end end diff --git a/lib/livebook_web/live/session_live/attached_live.ex b/lib/livebook_web/live/session_live/attached_live.ex index b58c13364..3756fd60b 100644 --- a/lib/livebook_web/live/session_live/attached_live.ex +++ b/lib/livebook_web/live/session_live/attached_live.ex @@ -1,6 +1,8 @@ defmodule LivebookWeb.SessionLive.AttachedLive do use LivebookWeb, :live_view + import Ecto.Changeset + alias Livebook.{Session, Runtime} @impl true @@ -24,10 +26,26 @@ defmodule LivebookWeb.SessionLive.AttachedLive do session: session, current_runtime: current_runtime, error_message: nil, - data: initial_data(current_runtime) + changeset: changeset(current_runtime) )} end + defp changeset(runtime, attrs \\ %{}) do + data = + case runtime do + %Runtime.Attached{node: node, cookie: cookie} -> + %{name: Atom.to_string(node), cookie: Atom.to_string(cookie)} + + _ -> + %{name: nil, cookie: nil} + end + + types = %{name: :string, cookie: :string} + + cast({data, types}, attrs, [:name, :cookie]) + |> validate_required([:name, :cookie]) + end + @impl true def render(assigns) do ~H""" @@ -53,7 +71,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do

<.form :let={f} - for={@data} + for={@changeset} as={:data} phx-submit="init" phx-change="validate" @@ -64,42 +82,48 @@ defmodule LivebookWeb.SessionLive.AttachedLive do <.text_field field={f[:name]} label="Name" placeholder={name_placeholder()} /> <.text_field field={f[:cookie]} label="Cookie" placeholder="mycookie" /> - """ end - defp matching_runtime?(%Runtime.Attached{} = runtime, data) do - initial_data(runtime) == data - end - - defp matching_runtime?(_runtime, _data), do: false - @impl true def handle_event("validate", %{"data" => data}, socket) do - {:noreply, assign(socket, data: data)} + changeset = + socket.assigns.current_runtime |> changeset(data) |> Map.replace!(:action, :validate) + + {:noreply, assign(socket, changeset: changeset)} end def handle_event("init", %{"data" => data}, socket) do - node = String.to_atom(data["name"]) - cookie = String.to_atom(data["cookie"]) + socket.assigns.current_runtime + |> changeset(data) + |> apply_action(:insert) + |> case do + {:ok, data} -> + node = String.to_atom(data.name) + cookie = String.to_atom(data.cookie) - runtime = Runtime.Attached.new(node, cookie) + runtime = Runtime.Attached.new(node, cookie) - case Runtime.connect(runtime) do - {:ok, runtime} -> - Session.set_runtime(socket.assigns.session.pid, runtime) - {:noreply, assign(socket, data: initial_data(runtime), error_message: nil)} + case Runtime.connect(runtime) do + {:ok, runtime} -> + Session.set_runtime(socket.assigns.session.pid, runtime) + {:noreply, assign(socket, changeset: changeset(runtime), error_message: nil)} - {:error, message} -> - {:noreply, - assign(socket, - data: data, - error_message: Livebook.Utils.upcase_first(message) - )} + {:error, message} -> + {:noreply, + assign(socket, + changeset: changeset(socket.assigns.current_runtime, data), + error_message: Livebook.Utils.upcase_first(message) + )} + end + + {:error, changeset} -> + {:noreply, assign(socket, changeset: changeset)} end end @@ -110,17 +134,8 @@ defmodule LivebookWeb.SessionLive.AttachedLive do def handle_info(_message, socket), do: {:noreply, socket} - defp initial_data(%Runtime.Attached{node: node, cookie: cookie}) do - %{ - "name" => Atom.to_string(node), - "cookie" => Atom.to_string(cookie) - } - end - - defp initial_data(_runtime), do: %{"name" => "", "cookie" => ""} - - defp data_valid?(data) do - data["name"] != "" and data["cookie"] != "" + defp reconnecting?(changeset) do + changeset.valid? and changeset.data == apply_changes(changeset) end defp name_placeholder do diff --git a/lib/livebook_web/live/settings_live/add_file_system_component.ex b/lib/livebook_web/live/settings_live/add_file_system_component.ex index 955f751ea..df4d5c09a 100644 --- a/lib/livebook_web/live/settings_live/add_file_system_component.ex +++ b/lib/livebook_web/live/settings_live/add_file_system_component.ex @@ -1,11 +1,28 @@ defmodule LivebookWeb.SettingsLive.AddFileSystemComponent do use LivebookWeb, :live_component + import Ecto.Changeset + alias Livebook.FileSystem @impl true def mount(socket) do - {:ok, assign(socket, data: empty_data(), error_message: nil)} + {:ok, assign(socket, changeset: changeset(), error_message: nil)} + end + + defp changeset(attrs \\ %{}) do + data = %{bucket_url: nil, region: nil, access_key_id: nil, secret_access_key: nil} + + types = %{ + bucket_url: :string, + region: :string, + access_key_id: :string, + secret_access_key: :string + } + + cast({data, types}, attrs, [:bucket_url, :region, :access_key_id, :secret_access_key]) + |> validate_required([:bucket_url, :access_key_id, :secret_access_key]) + |> Livebook.Utils.validate_url(:bucket_url) end @impl true @@ -25,7 +42,7 @@ defmodule LivebookWeb.SettingsLive.AddFileSystemComponent do <.form :let={f} - for={@data} + for={@changeset} as={:data} phx-target={@myself} phx-submit="add" @@ -43,7 +60,7 @@ defmodule LivebookWeb.SettingsLive.AddFileSystemComponent do <.password_field field={f[:access_key_id]} label="Access Key ID" /> <.password_field field={f[:secret_access_key]} label="Access Key ID" />
- <.link patch={@return_to} class="button-base button-outlined-gray"> @@ -58,38 +75,39 @@ defmodule LivebookWeb.SettingsLive.AddFileSystemComponent do @impl true def handle_event("validate", %{"data" => data}, socket) do - {:noreply, assign(socket, :data, data)} + changeset = data |> changeset() |> Map.replace!(:action, :validate) + {:noreply, assign(socket, changeset: changeset)} end def handle_event("add", %{"data" => data}, socket) do - file_system = data_to_file_system(data) - default_dir = FileSystem.File.new(file_system) + data + |> changeset() + |> apply_action(:insert) + |> case do + {:ok, data} -> + file_system = + FileSystem.S3.new(data.bucket_url, data.access_key_id, data.secret_access_key, + region: data.region + ) - case FileSystem.File.list(default_dir) do - {:ok, _} -> - Livebook.Settings.save_file_system(file_system) - send(self(), {:file_systems_updated, Livebook.Settings.file_systems()}) - {:noreply, push_patch(socket, to: socket.assigns.return_to)} + default_dir = FileSystem.File.new(file_system) - {:error, message} -> - {:noreply, assign(socket, error_message: "Connection test failed: " <> message)} + case FileSystem.File.list(default_dir) do + {:ok, _} -> + Livebook.Settings.save_file_system(file_system) + send(self(), {:file_systems_updated, Livebook.Settings.file_systems()}) + {:noreply, push_patch(socket, to: socket.assigns.return_to)} + + {:error, message} -> + {:noreply, + assign(socket, + changeset: changeset(data), + error_message: "Connection test failed: " <> message + )} + end + + {:error, changeset} -> + {:noreply, assign(socket, changeset: changeset)} end end - - defp empty_data() do - %{"bucket_url" => "", "region" => "", "access_key_id" => "", "secret_access_key" => ""} - end - - defp data_valid?(data) do - Livebook.Utils.valid_url?(data["bucket_url"]) and data["access_key_id"] != "" and - data["secret_access_key"] != "" - end - - defp data_to_file_system(data) do - region = if(data["region"] != "", do: data["region"]) - - FileSystem.S3.new(data["bucket_url"], data["access_key_id"], data["secret_access_key"], - region: region - ) - end end