From 1e6ef4ef45fcf7d6cebb9cb4c3af493dd495c128 Mon Sep 17 00:00:00 2001 From: Nikos Papadakis Date: Fri, 9 Jun 2023 22:13:27 +0300 Subject: [PATCH] Add servers liveview --- backend/lib/prymn/servers.ex | 104 ++++++++++++++++++ backend/lib/prymn/servers/server.ex | 19 ++++ .../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 | 47 ++++++++ .../live/server_live/index.html.heex | 46 ++++++++ .../lib/prymn_web/live/server_live/show.ex | 20 ++++ .../prymn_web/live/server_live/show.html.heex | 33 ++++++ backend/lib/prymn_web/router.ex | 7 ++ .../20230609164352_create_servers.exs | 13 +++ backend/test/prymn/servers_test.exs | 59 ++++++++++ .../controllers/page_controller_test.exs | 2 +- .../test/prymn_web/live/server_live_test.exs | 86 +++++++++++++++ .../test/support/fixtures/servers_fixtures.ex | 22 ++++ 15 files changed, 581 insertions(+), 1 deletion(-) create mode 100644 backend/lib/prymn/servers.ex create mode 100644 backend/lib/prymn/servers/server.ex create mode 100644 backend/lib/prymn_web/live/server_live/edit.ex create mode 100644 backend/lib/prymn_web/live/server_live/edit.html.heex create mode 100644 backend/lib/prymn_web/live/server_live/form_component.ex create mode 100644 backend/lib/prymn_web/live/server_live/index.ex create mode 100644 backend/lib/prymn_web/live/server_live/index.html.heex create mode 100644 backend/lib/prymn_web/live/server_live/show.ex create mode 100644 backend/lib/prymn_web/live/server_live/show.html.heex create mode 100644 backend/priv/repo/migrations/20230609164352_create_servers.exs create mode 100644 backend/test/prymn/servers_test.exs create mode 100644 backend/test/prymn_web/live/server_live_test.exs create mode 100644 backend/test/support/fixtures/servers_fixtures.ex 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