mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-11-10 17:15:09 +08:00
Use changesets for all forms (#2087)
This commit is contained in:
parent
e335ce6602
commit
7ea0b9278a
5 changed files with 174 additions and 99 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue