Use changesets for all forms (#2087)

This commit is contained in:
Jonatan Kłosko 2023-07-19 00:38:26 +02:00 committed by GitHub
parent e335ce6602
commit 7ea0b9278a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 174 additions and 99 deletions

View file

@ -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

View file

@ -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
</p>
<.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"
/>
<button class="mt-5 button-base button-blue" type="submit" disabled={@source == ""}>
<button class="mt-5 button-base button-blue" type="submit" disabled={not @changeset.valid?}>
Import
</button>
</.form>
@ -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

View file

@ -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
</p>
<.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"
/>
<button
class="mt-5 button-base button-blue"
type="submit"
disabled={not Utils.valid_url?(@url)}
>
<button class="mt-5 button-base button-blue" type="submit" disabled={not @changeset.valid?}>
Import
</button>
</.form>
@ -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

View file

@ -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
</p>
<.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" />
</div>
<button class="mt-5 button-base button-blue" type="submit" disabled={not data_valid?(@data)}>
<%= if(matching_runtime?(@current_runtime, @data), do: "Reconnect", else: "Connect") %>
<button class="mt-5 button-base button-blue" type="submit" disabled={not @changeset.valid?}>
<%= if(reconnecting?(@changeset), do: "Reconnect", else: "Connect") %>
</button>
</.form>
</div>
"""
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

View file

@ -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
</div>
<.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" />
<div class="flex space-x-2">
<button class="button-base button-blue" type="submit" disabled={not data_valid?(@data)}>
<button class="button-base button-blue" type="submit" disabled={not @changeset.valid?}>
Add
</button>
<.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