web: new server form
This commit is contained in:
parent
b5cab545e6
commit
52b892bb4f
12 changed files with 139 additions and 185 deletions
|
@ -50,7 +50,8 @@ defmodule Prymn.Servers do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def create_server(attrs \\ %{}) 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)
|
|> Server.changeset(attrs)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,8 @@ defmodule Prymn.Servers.Server do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :ipv4, :string
|
field :ipv4, :string
|
||||||
field :ipv6, :string
|
field :ipv6, :string
|
||||||
|
field :provider, Ecto.Enum, values: [:Hetzner, :Custom]
|
||||||
|
field :connection_token, Ecto.UUID, redact: true
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -13,7 +15,8 @@ defmodule Prymn.Servers.Server do
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(server, attrs) do
|
def changeset(server, attrs) do
|
||||||
server
|
server
|
||||||
|> cast(attrs, [:name])
|
|> cast(attrs, [:name, :provider])
|
||||||
|> validate_required([:name])
|
|> validate_required([:name, :provider, :connection_token])
|
||||||
|
|> validate_inclusion(:provider, [:Custom], message: "Provider not available (yet)")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -436,6 +436,8 @@ defmodule PrymnWeb.CoreComponents do
|
||||||
attr :rows, :list, required: true
|
attr :rows, :list, required: true
|
||||||
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
|
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_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,
|
attr :row_item, :any,
|
||||||
default: &Function.identity/1,
|
default: &Function.identity/1,
|
||||||
|
@ -458,6 +460,9 @@ defmodule PrymnWeb.CoreComponents do
|
||||||
<table class="w-[40rem] mt-11 sm:w-full">
|
<table class="w-[40rem] mt-11 sm:w-full">
|
||||||
<thead class="text-left text-sm leading-6 text-zinc-500">
|
<thead class="text-left text-sm leading-6 text-zinc-500">
|
||||||
<tr>
|
<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 :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>
|
<th class="relative p-0 pb-4"><span class="sr-only"><%= gettext("Actions") %></span></th>
|
||||||
</tr>
|
</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"
|
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">
|
<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
|
<td
|
||||||
:for={{col, i} <- Enum.with_index(@col)}
|
:for={{col, i} <- Enum.with_index(@col)}
|
||||||
phx-click={@row_click && @row_click.(row)}
|
phx-click={@row_click && @row_click.(row)}
|
||||||
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
|
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
|
||||||
>
|
>
|
||||||
<div class="block py-4 pr-6">
|
<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"]}>
|
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
|
||||||
<%= render_slot(col, @row_item.(row)) %>
|
<%= render_slot(col, @row_item.(row)) %>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -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
|
|
|
@ -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>
|
|
|
@ -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
|
|
|
@ -2,7 +2,6 @@ defmodule PrymnWeb.ServerLive.Index do
|
||||||
use PrymnWeb, :live_view
|
use PrymnWeb, :live_view
|
||||||
|
|
||||||
alias Prymn.Servers
|
alias Prymn.Servers
|
||||||
alias Prymn.Servers.Server
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
@ -14,26 +13,21 @@ defmodule PrymnWeb.ServerLive.Index do
|
||||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||||
end
|
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
|
defp apply_action(socket, :new, _params) do
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "New Server")
|
|> assign(:page_title, "New Server")
|
||||||
|> assign(:server, %Server{})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_action(socket, :index, _params) do
|
defp apply_action(socket, :index, _params) do
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "Listing Servers")
|
|> assign(:page_title, "Listing Servers")
|
||||||
|> assign(:server, nil)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@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)}
|
{:noreply, stream_insert(socket, :servers, server)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,12 @@
|
||||||
id="servers"
|
id="servers"
|
||||||
rows={@streams.servers}
|
rows={@streams.servers}
|
||||||
row_click={fn {_id, server} -> JS.navigate(~p"/servers/#{server}") end}
|
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="Name"><%= server.name %></:col>
|
||||||
<:col :let={{_id, server}} label="IPv4"><%= server.ipv4 || "N/A" %></:col>
|
<:col :let={{_id, server}} label="IPv4"><%= server.ipv4 || "N/A" %></:col>
|
||||||
|
@ -25,22 +31,6 @@
|
||||||
</:action>
|
</:action>
|
||||||
</.table>
|
</.table>
|
||||||
|
|
||||||
<.modal
|
<.modal :if={@live_action == :new} id="server-modal" show on_cancel={JS.patch(~p"/servers")}>
|
||||||
:if={@live_action in [:new, :edit]}
|
<.live_component module={PrymnWeb.ServerLive.NewServer} id={:new} patch={~p"/servers"} />
|
||||||
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>
|
</.modal>
|
||||||
|
|
92
backend/lib/prymn_web/live/server_live/new_server.ex
Normal file
92
backend/lib/prymn_web/live/server_live/new_server.ex
Normal 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
|
|
@ -1,33 +1,18 @@
|
||||||
<.header>
|
<.header>
|
||||||
Server <%= @server.name %>
|
<%= @server.name %>
|
||||||
<:subtitle>From here, you can view your server details</:subtitle>
|
<:subtitle>
|
||||||
|
<span>IPv4: <%= @server.ipv4 || "Not available" %></span>
|
||||||
|
<span :if={@server.ipv6}>IPv6: <%= @server.ipv6 %></span>
|
||||||
|
</:subtitle>
|
||||||
<:actions>
|
<:actions>
|
||||||
<.link navigate={~p"/servers/#{@server}/edit"}>
|
<.button>
|
||||||
<.button>Edit server</.button>
|
New site
|
||||||
</.link>
|
</.button>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
<.list>
|
<div class="mt-10">
|
||||||
<:item title="Name"><%= @server.name %></:item>
|
<h2 class="text-xl font-bold">Sites</h2>
|
||||||
<:item title="IPv4"><%= @server.ipv4 || "Not available" %></:item>
|
</div>
|
||||||
<:item title="IPv6"><%= @server.ipv6 || "Not available" %></:item>
|
|
||||||
</.list>
|
|
||||||
|
|
||||||
<.back navigate={~p"/servers"}>Back to servers</.back>
|
<.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>
|
|
||||||
|
|
|
@ -22,9 +22,7 @@ defmodule PrymnWeb.Router do
|
||||||
live "/servers", ServerLive.Index, :index
|
live "/servers", ServerLive.Index, :index
|
||||||
live "/servers/new", ServerLive.Index, :new
|
live "/servers/new", ServerLive.Index, :new
|
||||||
|
|
||||||
live "/servers/:id", ServerLive.Show, :show
|
live "/servers/:id", ServerLive.Show
|
||||||
|
|
||||||
live "/servers/:id/edit", ServerLive.Edit, :edit
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
|
|
|
@ -6,6 +6,8 @@ defmodule Prymn.Repo.Migrations.CreateServers do
|
||||||
add :name, :string
|
add :name, :string
|
||||||
add :ipv4, :string
|
add :ipv4, :string
|
||||||
add :ipv6, :string
|
add :ipv6, :string
|
||||||
|
add :provider, :string
|
||||||
|
add :connection_token, :binary
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue