web: new server form

This commit is contained in:
Nikos Papadakis 2023-06-19 23:28:24 +03:00
parent b5cab545e6
commit 52b892bb4f
Signed by untrusted user who does not match committer: nikos
GPG key ID: 78871F9905ADFF02
12 changed files with 139 additions and 185 deletions

View file

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

View file

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

View file

@ -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
<table class="w-[40rem] mt-11 sm:w-full">
<thead class="text-left text-sm leading-6 text-zinc-500">
<tr>
<th class="p-0 pb-4 font-normal">
<div class="w-min"><%= @indicator_label %></div>
</th>
<th :for={col <- @col} class="p-0 pr-6 pb-4 font-normal"><%= col[:label] %></th>
<th class="relative p-0 pb-4"><span class="sr-only"><%= gettext("Actions") %></span></th>
</tr>
@ -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"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
<td
class="relative w-min p-0 hover:cursor-pointer"
phx-click={@row_click && @row_click.(row)}
>
<div class="block w-min py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class="relative hover:cursor-pointer"><%= @row_indicator.(row) %></span>
</div>
</td>
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
>
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
<%= render_slot(col, @row_item.(row)) %>
</span>

View file

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

View file

@ -1,11 +0,0 @@
<.header>Editing server <%= @server.name %></.header>
<.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 %></.back>

View file

@ -1,88 +0,0 @@
defmodule PrymnWeb.ServerLive.FormComponent do
use PrymnWeb, :live_component
alias Prymn.Servers
@impl true
def render(assigns) do
~H"""
<div>
<.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</.button>
</:actions>
</.simple_form>
</div>
"""
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

View file

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

View file

@ -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(<span class="text-indigo-600">Connecting</span>)
end
}
indicator_label="Status"
>
<:col :let={{_id, server}} label="Name"><%= server.name %></:col>
<:col :let={{_id, server}} label="IPv4"><%= server.ipv4 || "N/A" %></:col>
@ -25,22 +31,6 @@
</:action>
</.table>
<.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!</:subtitle>
</.header>
<.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"} />
</.modal>

View file

@ -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"""
<div>
<.header>
Add a new server
<:subtitle>Connect your server to Prymn!</:subtitle>
</.header>
<.error :if={assigns[:error]}>There were some errors.</.error>
<.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</.button>
</:actions>
</.simple_form>
</div>
"""
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"""
<p>
<bold class="font-bold">Manual installation:</bold>
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.
</p>
"""
end
defp provider(assigns), do: ~H""
end

View file

@ -1,33 +1,18 @@
<.header>
Server <%= @server.name %>
<:subtitle>From here, you can view your server details</:subtitle>
<%= @server.name %>
<:subtitle>
<span>IPv4: <%= @server.ipv4 || "Not available" %></span>
<span :if={@server.ipv6}>IPv6: <%= @server.ipv6 %></span>
</:subtitle>
<:actions>
<.link navigate={~p"/servers/#{@server}/edit"}>
<.button>Edit server</.button>
</.link>
<.button>
New site
</.button>
</:actions>
</.header>
<.list>
<:item title="Name"><%= @server.name %></:item>
<:item title="IPv4"><%= @server.ipv4 || "Not available" %></:item>
<:item title="IPv6"><%= @server.ipv6 || "Not available" %></:item>
</.list>
<div class="mt-10">
<h2 class="text-xl font-bold">Sites</h2>
</div>
<.back navigate={~p"/servers"}>Back to servers</.back>
<.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}"}
/>
</.modal>

View file

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

View file

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