From 52b892bb4f3ebb4870d787220dabb53a5985652f Mon Sep 17 00:00:00 2001 From: Nikos Papadakis Date: Mon, 19 Jun 2023 23:28:24 +0300 Subject: [PATCH] web: new server form --- backend/lib/prymn/servers.ex | 3 +- backend/lib/prymn/servers/server.ex | 7 +- .../prymn_web/components/core_components.ex | 15 ++- .../lib/prymn_web/live/server_live/edit.ex | 25 ----- .../prymn_web/live/server_live/edit.html.heex | 11 --- .../live/server_live/form_component.ex | 88 ------------------ .../lib/prymn_web/live/server_live/index.ex | 14 +-- .../live/server_live/index.html.heex | 26 ++---- .../prymn_web/live/server_live/new_server.ex | 92 +++++++++++++++++++ .../prymn_web/live/server_live/show.html.heex | 37 +++----- backend/lib/prymn_web/router.ex | 4 +- .../20230609164352_create_servers.exs | 2 + 12 files changed, 139 insertions(+), 185 deletions(-) delete mode 100644 backend/lib/prymn_web/live/server_live/edit.ex delete mode 100644 backend/lib/prymn_web/live/server_live/edit.html.heex delete mode 100644 backend/lib/prymn_web/live/server_live/form_component.ex create mode 100644 backend/lib/prymn_web/live/server_live/new_server.ex diff --git a/backend/lib/prymn/servers.ex b/backend/lib/prymn/servers.ex index 1c70860..d6d5bef 100644 --- a/backend/lib/prymn/servers.ex +++ b/backend/lib/prymn/servers.ex @@ -50,7 +50,8 @@ defmodule Prymn.Servers do """ def create_server(attrs \\ %{}) do - %Server{} + # FIXME: Maybe use a cryptographically secure token (if UUID v4 is not one)? + %Server{connection_token: Ecto.UUID.generate()} |> Server.changeset(attrs) |> Repo.insert() end diff --git a/backend/lib/prymn/servers/server.ex b/backend/lib/prymn/servers/server.ex index 13c20e4..21b22ac 100644 --- a/backend/lib/prymn/servers/server.ex +++ b/backend/lib/prymn/servers/server.ex @@ -6,6 +6,8 @@ defmodule Prymn.Servers.Server do field :name, :string field :ipv4, :string field :ipv6, :string + field :provider, Ecto.Enum, values: [:Hetzner, :Custom] + field :connection_token, Ecto.UUID, redact: true timestamps() end @@ -13,7 +15,8 @@ defmodule Prymn.Servers.Server do @doc false def changeset(server, attrs) do server - |> cast(attrs, [:name]) - |> validate_required([:name]) + |> cast(attrs, [:name, :provider]) + |> validate_required([:name, :provider, :connection_token]) + |> validate_inclusion(:provider, [:Custom], message: "Provider not available (yet)") end end diff --git a/backend/lib/prymn_web/components/core_components.ex b/backend/lib/prymn_web/components/core_components.ex index 2840ece..f9c99ca 100644 --- a/backend/lib/prymn_web/components/core_components.ex +++ b/backend/lib/prymn_web/components/core_components.ex @@ -436,6 +436,8 @@ defmodule PrymnWeb.CoreComponents do attr :rows, :list, required: true attr :row_id, :any, default: nil, doc: "the function for generating the row id" attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" + attr :row_indicator, :any, default: nil + attr :indicator_label, :string, default: "Indicator" attr :row_item, :any, default: &Function.identity/1, @@ -458,6 +460,9 @@ defmodule PrymnWeb.CoreComponents do + @@ -468,13 +473,21 @@ defmodule PrymnWeb.CoreComponents do class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700" > +
+
<%= @indicator_label %>
+
<%= col[:label] %> <%= gettext("Actions") %>
+
+ + <%= @row_indicator.(row) %> +
+
- <%= render_slot(col, @row_item.(row)) %> diff --git a/backend/lib/prymn_web/live/server_live/edit.ex b/backend/lib/prymn_web/live/server_live/edit.ex deleted file mode 100644 index 79c4e2c..0000000 --- a/backend/lib/prymn_web/live/server_live/edit.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule PrymnWeb.ServerLive.Edit do - use PrymnWeb, :live_view - - alias Prymn.Servers - - @impl true - def mount(_params, _session, socket) do - {:ok, socket} - end - - @impl true - def handle_params(%{"id" => id}, _, socket) do - server = Servers.get_server!(id) - - {:noreply, - socket - |> assign(:page_title, "Editing #{server.name}") - |> assign(:server, server)} - end - - @impl true - def handle_info({PrymnWeb.ServerLive.FormComponent, {:saved, server}}, socket) do - {:noreply, assign(socket, :server, server)} - end -end diff --git a/backend/lib/prymn_web/live/server_live/edit.html.heex b/backend/lib/prymn_web/live/server_live/edit.html.heex deleted file mode 100644 index 6716cd2..0000000 --- a/backend/lib/prymn_web/live/server_live/edit.html.heex +++ /dev/null @@ -1,11 +0,0 @@ -<.header>Editing server <%= @server.name %> - -<.live_component - module={PrymnWeb.ServerLive.FormComponent} - title="Test" - id={@server.id} - action={@live_action} - server={@server} -/> - -<.back navigate={~p"/servers/#{@server}"}>Go back to server <%= @server.name %> diff --git a/backend/lib/prymn_web/live/server_live/form_component.ex b/backend/lib/prymn_web/live/server_live/form_component.ex deleted file mode 100644 index 9b03703..0000000 --- a/backend/lib/prymn_web/live/server_live/form_component.ex +++ /dev/null @@ -1,88 +0,0 @@ -defmodule PrymnWeb.ServerLive.FormComponent do - use PrymnWeb, :live_component - - alias Prymn.Servers - - @impl true - def render(assigns) do - ~H""" -
- <.simple_form - for={@form} - id="server-form" - phx-target={@myself} - phx-change="validate" - phx-submit="save" - > - <.input field={@form[:name]} type="text" label="Name" /> - <:actions> - <.button phx-disable-with="Saving...">Save Server - - -
- """ - end - - @impl true - def update(%{server: server} = assigns, socket) do - changeset = Servers.change_server(server) - - {:ok, - socket - |> assign(assigns) - |> assign_form(changeset)} - end - - @impl true - def handle_event("validate", %{"server" => server_params}, socket) do - changeset = - socket.assigns.server - |> Servers.change_server(server_params) - |> Map.put(:action, :validate) - - {:noreply, assign_form(socket, changeset)} - end - - def handle_event("save", %{"server" => server_params}, socket) do - save_server(socket, socket.assigns.action, server_params) - end - - defp save_server(socket, :edit, server_params) do - case Servers.update_server(socket.assigns.server, server_params) do - {:ok, server} -> - notify_parent({:saved, server}) - - socket = socket |> put_flash(:info, "Server updated successfully") - - if Map.has_key?(socket.assigns, :patch) do - socket = push_patch(socket, to: socket.assigns.patch) - end - - {:noreply, socket} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign_form(socket, changeset)} - end - end - - defp save_server(socket, :new, server_params) do - case Servers.create_server(server_params) do - {:ok, server} -> - notify_parent({:saved, server}) - - {:noreply, - socket - |> put_flash(:info, "Server created successfully") - |> push_patch(to: socket.assigns.patch || false)} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign_form(socket, changeset)} - end - end - - defp assign_form(socket, %Ecto.Changeset{} = changeset) do - assign(socket, :form, to_form(changeset)) - end - - defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) -end diff --git a/backend/lib/prymn_web/live/server_live/index.ex b/backend/lib/prymn_web/live/server_live/index.ex index e277cf0..99ddd39 100644 --- a/backend/lib/prymn_web/live/server_live/index.ex +++ b/backend/lib/prymn_web/live/server_live/index.ex @@ -2,7 +2,6 @@ defmodule PrymnWeb.ServerLive.Index do use PrymnWeb, :live_view alias Prymn.Servers - alias Prymn.Servers.Server @impl true def mount(_params, _session, socket) do @@ -14,26 +13,21 @@ defmodule PrymnWeb.ServerLive.Index do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end - defp apply_action(socket, :edit, %{"id" => id}) do - socket - |> assign(:page_title, "Edit Server") - |> assign(:server, Servers.get_server!(id)) - end - defp apply_action(socket, :new, _params) do socket |> assign(:page_title, "New Server") - |> assign(:server, %Server{}) end defp apply_action(socket, :index, _params) do socket |> assign(:page_title, "Listing Servers") - |> assign(:server, nil) end @impl true - def handle_info({PrymnWeb.ServerLive.FormComponent, {:saved, server}}, socket) do + def handle_info({:connect, server}, socket) do + # TODO: Connect the new server: + # For custom connections: will need to generate a new prompt for the user to execute + # For API connections: the "prompt" will be automatic, but same execution server-side {:noreply, stream_insert(socket, :servers, server)} end diff --git a/backend/lib/prymn_web/live/server_live/index.html.heex b/backend/lib/prymn_web/live/server_live/index.html.heex index 44fadf3..3f38d52 100644 --- a/backend/lib/prymn_web/live/server_live/index.html.heex +++ b/backend/lib/prymn_web/live/server_live/index.html.heex @@ -11,6 +11,12 @@ id="servers" rows={@streams.servers} row_click={fn {_id, server} -> JS.navigate(~p"/servers/#{server}") end} + row_indicator={ + fn {_id, _server} -> + ~H(Connecting) + end + } + indicator_label="Status" > <:col :let={{_id, server}} label="Name"><%= server.name %> <:col :let={{_id, server}} label="IPv4"><%= server.ipv4 || "N/A" %> @@ -25,22 +31,6 @@ -<.modal - :if={@live_action in [:new, :edit]} - id="server-modal" - show - on_cancel={JS.patch(~p"/servers")} -> - <.header> - Add a new server - <:subtitle>Connect your server to Prymn! - - <.live_component - module={PrymnWeb.ServerLive.FormComponent} - id={:new} - title={@page_title} - action={@live_action} - server={@server} - patch={~p"/servers"} - /> +<.modal :if={@live_action == :new} id="server-modal" show on_cancel={JS.patch(~p"/servers")}> + <.live_component module={PrymnWeb.ServerLive.NewServer} id={:new} patch={~p"/servers"} /> diff --git a/backend/lib/prymn_web/live/server_live/new_server.ex b/backend/lib/prymn_web/live/server_live/new_server.ex new file mode 100644 index 0000000..5e8949b --- /dev/null +++ b/backend/lib/prymn_web/live/server_live/new_server.ex @@ -0,0 +1,92 @@ +defmodule PrymnWeb.ServerLive.NewServer do + use PrymnWeb, :live_component + + alias Prymn.Servers + require Logger + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + Add a new server + <:subtitle>Connect your server to Prymn! + + + <.error :if={assigns[:error]}>There were some errors. + + <.simple_form for={@form} phx-change="validate" phx-submit="connect" phx-target={@myself}> + <.input field={@form[:name]} label="Server Name" phx-debounce={1000} /> + <.input + field={@form[:provider]} + label="Provider" + type="select" + prompt="Select a provider" + options={Ecto.Enum.mappings(Servers.Server, :provider)} + /> + <.provider id="provider" provider={@form[:provider]} /> + <:actions> + <.button>Connect + + +
+ """ + end + + @impl true + def update(assigns, socket) do + changeset = Servers.change_server(%Servers.Server{}) + + socket = + socket + |> assign(assigns) + |> assign(:form, to_form(changeset)) + + {:ok, socket} + end + + @impl true + def handle_event("validate", %{"server" => params}, socket) do + form = + %Servers.Server{} + |> Servers.change_server(params) + |> Map.put(:action, :validate) + |> to_form() + + {:noreply, assign(socket, form: form)} + end + + @impl true + def handle_event("connect", %{"server" => params}, socket) do + socket = + case Servers.create_server(params) do + {:ok, server} -> + # Notify parent + send(self(), {:connect, server}) + + socket + |> put_flash(:info, "Starting new server connection...") + |> push_patch(to: ~p"/servers") + + {:error, %Ecto.Changeset{} = changeset} -> + socket + |> assign(:form, to_form(changeset)) + |> assign(:error, true) + end + + {:noreply, socket} + end + + defp provider(%{provider: %{value: :Custom}} = assigns) do + ~H""" +

+ Manual installation: + To connect, you must have root SSH access to your server. When you click + connect, you will be prompted to execute a command on your server to + complete the installation. +

+ """ + end + + defp provider(assigns), do: ~H"" +end diff --git a/backend/lib/prymn_web/live/server_live/show.html.heex b/backend/lib/prymn_web/live/server_live/show.html.heex index c7e1b79..604a5f9 100644 --- a/backend/lib/prymn_web/live/server_live/show.html.heex +++ b/backend/lib/prymn_web/live/server_live/show.html.heex @@ -1,33 +1,18 @@ <.header> - Server <%= @server.name %> - <:subtitle>From here, you can view your server details + <%= @server.name %> + <:subtitle> + IPv4: <%= @server.ipv4 || "Not available" %> + IPv6: <%= @server.ipv6 %> + <:actions> - <.link navigate={~p"/servers/#{@server}/edit"}> - <.button>Edit server - + <.button> + New site + -<.list> - <:item title="Name"><%= @server.name %> - <:item title="IPv4"><%= @server.ipv4 || "Not available" %> - <:item title="IPv6"><%= @server.ipv6 || "Not available" %> - +
+

Sites

+
<.back navigate={~p"/servers"}>Back to servers - -<.modal - :if={@live_action == :edit} - id="server-modal" - show - on_cancel={JS.patch(~p"/servers/#{@server}")} -> - <.live_component - module={PrymnWeb.ServerLive.FormComponent} - id={@server.id} - title={@page_title} - action={@live_action} - server={@server} - patch={~p"/servers/#{@server}"} - /> - diff --git a/backend/lib/prymn_web/router.ex b/backend/lib/prymn_web/router.ex index 76a9fd6..5cb50c4 100644 --- a/backend/lib/prymn_web/router.ex +++ b/backend/lib/prymn_web/router.ex @@ -22,9 +22,7 @@ defmodule PrymnWeb.Router do live "/servers", ServerLive.Index, :index live "/servers/new", ServerLive.Index, :new - live "/servers/:id", ServerLive.Show, :show - - live "/servers/:id/edit", ServerLive.Edit, :edit + live "/servers/:id", ServerLive.Show end # Other scopes may use custom stacks. diff --git a/backend/priv/repo/migrations/20230609164352_create_servers.exs b/backend/priv/repo/migrations/20230609164352_create_servers.exs index b17422a..93e046b 100644 --- a/backend/priv/repo/migrations/20230609164352_create_servers.exs +++ b/backend/priv/repo/migrations/20230609164352_create_servers.exs @@ -6,6 +6,8 @@ defmodule Prymn.Repo.Migrations.CreateServers do add :name, :string add :ipv4, :string add :ipv6, :string + add :provider, :string + add :connection_token, :binary timestamps() end