diff --git a/backend/lib/prymn/servers.ex b/backend/lib/prymn/servers.ex
new file mode 100644
index 0000000..1c70860
--- /dev/null
+++ b/backend/lib/prymn/servers.ex
@@ -0,0 +1,104 @@
+defmodule Prymn.Servers do
+ @moduledoc """
+ The Servers context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Prymn.Repo
+
+ alias Prymn.Servers.Server
+
+ @doc """
+ Returns the list of servers.
+
+ ## Examples
+
+ iex> list_servers()
+ [%Server{}, ...]
+
+ """
+ def list_servers do
+ Repo.all(Server)
+ end
+
+ @doc """
+ Gets a single server.
+
+ Raises `Ecto.NoResultsError` if the Server does not exist.
+
+ ## Examples
+
+ iex> get_server!(123)
+ %Server{}
+
+ iex> get_server!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_server!(id), do: Repo.get!(Server, id)
+
+ @doc """
+ Creates a server.
+
+ ## Examples
+
+ iex> create_server(%{field: value})
+ {:ok, %Server{}}
+
+ iex> create_server(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_server(attrs \\ %{}) do
+ %Server{}
+ |> Server.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a server.
+
+ ## Examples
+
+ iex> update_server(server, %{field: new_value})
+ {:ok, %Server{}}
+
+ iex> update_server(server, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_server(%Server{} = server, attrs) do
+ server
+ |> Server.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a server.
+
+ ## Examples
+
+ iex> delete_server(server)
+ {:ok, %Server{}}
+
+ iex> delete_server(server)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_server(%Server{} = server) do
+ Repo.delete(server)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking server changes.
+
+ ## Examples
+
+ iex> change_server(server)
+ %Ecto.Changeset{data: %Server{}}
+
+ """
+ def change_server(%Server{} = server, attrs \\ %{}) do
+ Server.changeset(server, attrs)
+ end
+end
diff --git a/backend/lib/prymn/servers/server.ex b/backend/lib/prymn/servers/server.ex
new file mode 100644
index 0000000..13c20e4
--- /dev/null
+++ b/backend/lib/prymn/servers/server.ex
@@ -0,0 +1,19 @@
+defmodule Prymn.Servers.Server do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "servers" do
+ field :name, :string
+ field :ipv4, :string
+ field :ipv6, :string
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(server, attrs) do
+ server
+ |> cast(attrs, [:name])
+ |> validate_required([:name])
+ end
+end
diff --git a/backend/lib/prymn_web/live/server_live/edit.ex b/backend/lib/prymn_web/live/server_live/edit.ex
new file mode 100644
index 0000000..79c4e2c
--- /dev/null
+++ b/backend/lib/prymn_web/live/server_live/edit.ex
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000..6716cd2
--- /dev/null
+++ b/backend/lib/prymn_web/live/server_live/edit.html.heex
@@ -0,0 +1,11 @@
+<.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
new file mode 100644
index 0000000..9b03703
--- /dev/null
+++ b/backend/lib/prymn_web/live/server_live/form_component.ex
@@ -0,0 +1,88 @@
+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
new file mode 100644
index 0000000..e277cf0
--- /dev/null
+++ b/backend/lib/prymn_web/live/server_live/index.ex
@@ -0,0 +1,47 @@
+defmodule PrymnWeb.ServerLive.Index do
+ use PrymnWeb, :live_view
+
+ alias Prymn.Servers
+ alias Prymn.Servers.Server
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, stream(socket, :servers, Servers.list_servers())}
+ end
+
+ @impl true
+ def handle_params(params, _url, socket) 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
+ {:noreply, stream_insert(socket, :servers, server)}
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ server = Servers.get_server!(id)
+ {:ok, _} = Servers.delete_server(server)
+
+ {:noreply, stream_delete(socket, :servers, server)}
+ end
+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
new file mode 100644
index 0000000..44fadf3
--- /dev/null
+++ b/backend/lib/prymn_web/live/server_live/index.html.heex
@@ -0,0 +1,46 @@
+<.header>
+ All available servers to you
+ <:actions>
+ <.link patch={~p"/servers/new"}>
+ <.button>Connect a Server
+
+
+
+
+<.table
+ id="servers"
+ rows={@streams.servers}
+ row_click={fn {_id, server} -> JS.navigate(~p"/servers/#{server}") end}
+>
+ <:col :let={{_id, server}} label="Name"><%= server.name %>
+ <:col :let={{_id, server}} label="IPv4"><%= server.ipv4 || "N/A" %>
+ <:col :let={{_id, server}} label="IPv6"><%= server.ipv6 || "N/A" %>
+ <:action :let={{id, server}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: server.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ Delete
+
+
+
+
+<.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"}
+ />
+
diff --git a/backend/lib/prymn_web/live/server_live/show.ex b/backend/lib/prymn_web/live/server_live/show.ex
new file mode 100644
index 0000000..ece3909
--- /dev/null
+++ b/backend/lib/prymn_web/live/server_live/show.ex
@@ -0,0 +1,20 @@
+defmodule PrymnWeb.ServerLive.Show 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, server.name)
+ |> assign(:server, server)}
+ end
+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
new file mode 100644
index 0000000..c7e1b79
--- /dev/null
+++ b/backend/lib/prymn_web/live/server_live/show.html.heex
@@ -0,0 +1,33 @@
+<.header>
+ Server <%= @server.name %>
+ <:subtitle>From here, you can view your server details
+ <:actions>
+ <.link navigate={~p"/servers/#{@server}/edit"}>
+ <.button>Edit server
+
+
+
+
+<.list>
+ <:item title="Name"><%= @server.name %>
+ <:item title="IPv4"><%= @server.ipv4 || "Not available" %>
+ <:item title="IPv6"><%= @server.ipv6 || "Not available" %>
+
+
+<.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 0a8221f..76a9fd6 100644
--- a/backend/lib/prymn_web/router.ex
+++ b/backend/lib/prymn_web/router.ex
@@ -18,6 +18,13 @@ defmodule PrymnWeb.Router do
pipe_through :browser
get "/", PageController, :home
+
+ live "/servers", ServerLive.Index, :index
+ live "/servers/new", ServerLive.Index, :new
+
+ live "/servers/:id", ServerLive.Show, :show
+
+ live "/servers/:id/edit", ServerLive.Edit, :edit
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
new file mode 100644
index 0000000..b17422a
--- /dev/null
+++ b/backend/priv/repo/migrations/20230609164352_create_servers.exs
@@ -0,0 +1,13 @@
+defmodule Prymn.Repo.Migrations.CreateServers do
+ use Ecto.Migration
+
+ def change do
+ create table(:servers) do
+ add :name, :string
+ add :ipv4, :string
+ add :ipv6, :string
+
+ timestamps()
+ end
+ end
+end
diff --git a/backend/test/prymn/servers_test.exs b/backend/test/prymn/servers_test.exs
new file mode 100644
index 0000000..7ff5053
--- /dev/null
+++ b/backend/test/prymn/servers_test.exs
@@ -0,0 +1,59 @@
+defmodule Prymn.ServersTest do
+ use Prymn.DataCase
+
+ alias Prymn.Servers
+
+ describe "servers" do
+ alias Prymn.Servers.Server
+
+ import Prymn.ServersFixtures
+
+ @invalid_attrs %{name: nil}
+
+ test "list_servers/0 returns all servers" do
+ server = server_fixture()
+ assert Servers.list_servers() == [server]
+ end
+
+ test "get_server!/1 returns the server with given id" do
+ server = server_fixture()
+ assert Servers.get_server!(server.id) == server
+ end
+
+ test "create_server/1 with valid data creates a server" do
+ valid_attrs = %{name: "some name"}
+
+ assert {:ok, %Server{} = server} = Servers.create_server(valid_attrs)
+ assert server.name == "some name"
+ end
+
+ test "create_server/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Servers.create_server(@invalid_attrs)
+ end
+
+ test "update_server/2 with valid data updates the server" do
+ server = server_fixture()
+ update_attrs = %{name: "some updated name"}
+
+ assert {:ok, %Server{} = server} = Servers.update_server(server, update_attrs)
+ assert server.name == "some updated name"
+ end
+
+ test "update_server/2 with invalid data returns error changeset" do
+ server = server_fixture()
+ assert {:error, %Ecto.Changeset{}} = Servers.update_server(server, @invalid_attrs)
+ assert server == Servers.get_server!(server.id)
+ end
+
+ test "delete_server/1 deletes the server" do
+ server = server_fixture()
+ assert {:ok, %Server{}} = Servers.delete_server(server)
+ assert_raise Ecto.NoResultsError, fn -> Servers.get_server!(server.id) end
+ end
+
+ test "change_server/1 returns a server changeset" do
+ server = server_fixture()
+ assert %Ecto.Changeset{} = Servers.change_server(server)
+ end
+ end
+end
diff --git a/backend/test/prymn_web/controllers/page_controller_test.exs b/backend/test/prymn_web/controllers/page_controller_test.exs
index a8db8d3..db2159c 100644
--- a/backend/test/prymn_web/controllers/page_controller_test.exs
+++ b/backend/test/prymn_web/controllers/page_controller_test.exs
@@ -3,6 +3,6 @@ defmodule PrymnWeb.PageControllerTest do
test "GET /", %{conn: conn} do
conn = get(conn, ~p"/")
- assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
+ assert html_response(conn, 200) =~ "Welcome"
end
end
diff --git a/backend/test/prymn_web/live/server_live_test.exs b/backend/test/prymn_web/live/server_live_test.exs
new file mode 100644
index 0000000..f468176
--- /dev/null
+++ b/backend/test/prymn_web/live/server_live_test.exs
@@ -0,0 +1,86 @@
+defmodule PrymnWeb.ServerLiveTest do
+ use PrymnWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import Prymn.ServersFixtures
+
+ @create_attrs %{name: "some name"}
+ @update_attrs %{name: "some updated name"}
+ @invalid_attrs %{name: nil}
+
+ defp create_server(_) do
+ server = server_fixture()
+ %{server: server}
+ end
+
+ describe "Index" do
+ setup [:create_server]
+
+ test "lists all servers", %{conn: conn, server: server} do
+ {:ok, _index_live, html} = live(conn, ~p"/servers")
+
+ assert html =~ "Listing Servers"
+ assert html =~ server.name
+ end
+
+ test "saves new server", %{conn: conn} do
+ {:ok, index_live, _html} = live(conn, ~p"/servers")
+
+ assert index_live |> element("a", "Connect a Server") |> render_click() =~
+ "Add a new server"
+
+ assert_patch(index_live, ~p"/servers/new")
+
+ assert index_live
+ |> form("#server-form", server: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert index_live
+ |> form("#server-form", server: @create_attrs)
+ |> render_submit()
+
+ assert_patch(index_live, ~p"/servers")
+
+ html = render(index_live)
+ assert html =~ "Server created successfully"
+ assert html =~ "some name"
+ end
+
+ test "deletes server in listing", %{conn: conn, server: server} do
+ {:ok, index_live, _html} = live(conn, ~p"/servers")
+
+ assert index_live |> element("#servers-#{server.id} a", "Delete") |> render_click()
+ refute has_element?(index_live, "#servers-#{server.id}")
+ end
+ end
+
+ describe "Show" do
+ setup [:create_server]
+
+ test "displays server", %{conn: conn, server: server} do
+ {:ok, _show_live, html} = live(conn, ~p"/servers/#{server}")
+
+ assert html =~ "Server some name"
+ end
+ end
+
+ describe "Edit" do
+ setup [:create_server]
+
+ test "updates server", %{conn: conn, server: server} do
+ {:ok, edit_live, _html} = live(conn, ~p"/servers/#{server}/edit")
+
+ assert edit_live
+ |> form("#server-form", server: @invalid_attrs)
+ |> render_change() =~
+ "can't be blank"
+
+ assert edit_live
+ |> form("#server-form", server: @update_attrs)
+ |> render_submit()
+
+ html = render(edit_live)
+ assert html =~ "some updated name"
+ end
+ end
+end
diff --git a/backend/test/support/fixtures/servers_fixtures.ex b/backend/test/support/fixtures/servers_fixtures.ex
new file mode 100644
index 0000000..8ea8614
--- /dev/null
+++ b/backend/test/support/fixtures/servers_fixtures.ex
@@ -0,0 +1,22 @@
+defmodule Prymn.ServersFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `Prymn.Servers` context.
+ """
+
+ @doc """
+ Generate a server.
+ """
+ def server_fixture(attrs \\ %{}) do
+ {:ok, server} =
+ attrs
+ |> Enum.into(%{
+ name: "some name",
+ ipv4: "192.168.1.1",
+ ipv6: "[1234:5678::1]"
+ })
+ |> Prymn.Servers.create_server()
+
+ server
+ end
+end