app: alpine.js + dropdown
This commit is contained in:
		
							parent
							
								
									c7b9be3ee4
								
							
						
					
					
						commit
						efd5f1d126
					
				
					 8 changed files with 192 additions and 64 deletions
				
			
		| 
						 | 
					@ -2,6 +2,10 @@ root = true
 | 
				
			||||||
charset = utf-8
 | 
					charset = utf-8
 | 
				
			||||||
end_of_line = lf
 | 
					end_of_line = lf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[*.js]
 | 
				
			||||||
 | 
					indent_style = space
 | 
				
			||||||
 | 
					indent_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[*.nix]
 | 
					[*.nix]
 | 
				
			||||||
indent_style = space
 | 
					indent_style = space
 | 
				
			||||||
indent_size = 2
 | 
					indent_size = 2
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,15 +15,26 @@
 | 
				
			||||||
//     import "some-package"
 | 
					//     import "some-package"
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
 | 
					 | 
				
			||||||
import "phoenix_html"
 | 
					import "phoenix_html"
 | 
				
			||||||
// Establish Phoenix Socket and LiveView configuration.
 | 
					 | 
				
			||||||
import {Socket} from "phoenix"
 | 
					import {Socket} from "phoenix"
 | 
				
			||||||
import {LiveSocket} from "phoenix_live_view"
 | 
					import {LiveSocket} from "phoenix_live_view"
 | 
				
			||||||
import topbar from "../vendor/topbar"
 | 
					import topbar from "../vendor/topbar"
 | 
				
			||||||
 | 
					import Alpine from "alpinejs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Alpine.start()
 | 
				
			||||||
 | 
					window.Alpine = Alpine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
 | 
					let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
 | 
				
			||||||
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
 | 
					let liveSocket = new LiveSocket("/live", Socket, {
 | 
				
			||||||
 | 
					  params: {_csrf_token: csrfToken},
 | 
				
			||||||
 | 
					  dom: {
 | 
				
			||||||
 | 
					    onBeforeElUpdated(from, to) {
 | 
				
			||||||
 | 
					      if (from._x_dataStack) {
 | 
				
			||||||
 | 
					        window.Alpine.clone(from, to)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Show progress bar on live navigation and form submits
 | 
					// Show progress bar on live navigation and form submits
 | 
				
			||||||
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
 | 
					topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										33
									
								
								app/assets/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/assets/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "assets",
 | 
				
			||||||
 | 
					  "lockfileVersion": 3,
 | 
				
			||||||
 | 
					  "requires": true,
 | 
				
			||||||
 | 
					  "packages": {
 | 
				
			||||||
 | 
					    "": {
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "alpinejs": "^3.13.3"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@vue/reactivity": {
 | 
				
			||||||
 | 
					      "version": "3.1.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@vue/shared": "3.1.5"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@vue/shared": {
 | 
				
			||||||
 | 
					      "version": "3.1.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/alpinejs": {
 | 
				
			||||||
 | 
					      "version": "3.13.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@vue/reactivity": "~3.1.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/assets/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/assets/package.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "alpinejs": "^3.13.3"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ defmodule PrymnWeb.Button do
 | 
				
			||||||
      assigns,
 | 
					      assigns,
 | 
				
			||||||
      :style,
 | 
					      :style,
 | 
				
			||||||
      [
 | 
					      [
 | 
				
			||||||
        "inline-flex justify-center items-end rounded-2xl shadow transition-colors active:shadow-sm",
 | 
					        "inline-flex justify-center items-center rounded-2xl shadow transition-colors active:shadow-sm",
 | 
				
			||||||
        by_variant(assigns.variant),
 | 
					        by_variant(assigns.variant),
 | 
				
			||||||
        by_size(assigns.size),
 | 
					        by_size(assigns.size),
 | 
				
			||||||
        assigns[:class]
 | 
					        assigns[:class]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -587,6 +587,62 @@ defmodule PrymnWeb.CoreComponents do
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  A simple dropdown menu
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :title, :string, default: nil
 | 
				
			||||||
 | 
					  attr :position, :string, default: "left", values: ~w(left right)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  slot :button
 | 
				
			||||||
 | 
					  slot :item
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def dropdown(assigns) do
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      class="relative inline-block"
 | 
				
			||||||
 | 
					      x-data="{
 | 
				
			||||||
 | 
					        open: false,
 | 
				
			||||||
 | 
					        close(focusAfter) {
 | 
				
			||||||
 | 
					          this.open = false
 | 
				
			||||||
 | 
					          focusAfter && focusAfter.focus()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }"
 | 
				
			||||||
 | 
					      x-id="['dropdown-button']"
 | 
				
			||||||
 | 
					      x-on:keydown.escape.prevent.stop="close($refs.button)"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        title={@title}
 | 
				
			||||||
 | 
					        type="button"
 | 
				
			||||||
 | 
					        x-ref="button"
 | 
				
			||||||
 | 
					        x-bind:aria-expanded="open"
 | 
				
			||||||
 | 
					        x-on:click="open = !open"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <%= render_slot(@button) %>
 | 
				
			||||||
 | 
					        <.icon class="ml-2 h-4 w-4" name="hero-chevron-down" />
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        class="absolute left-0 z-10 w-56 rounded-xl bg-white shadow-lg ring-1 ring-black ring-opacity-5"
 | 
				
			||||||
 | 
					        role="menu"
 | 
				
			||||||
 | 
					        aria-orientation="vertical"
 | 
				
			||||||
 | 
					        tabindex="-1"
 | 
				
			||||||
 | 
					        x-show="open"
 | 
				
			||||||
 | 
					        x-on:click.outside="close($refs.button)"
 | 
				
			||||||
 | 
					        x-transition.origin.top.left
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          :for={item <- @item}
 | 
				
			||||||
 | 
					          class="my-2 px-4 py-2 hover:bg-slate-100"
 | 
				
			||||||
 | 
					          role="none"
 | 
				
			||||||
 | 
					          x-on:click="close()"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <%= render_slot(item, item) %>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## JS Commands
 | 
					  ## JS Commands
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show(js \\ %JS{}, selector) do
 | 
					  def show(js \\ %JS{}, selector) do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,18 +16,17 @@
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="flex items-center text-zinc-900">
 | 
					    <div class="flex items-center text-zinc-900">
 | 
				
			||||||
      <.link class="border-r border-l px-3 py-4 hover:bg-white" href={~p"/users/settings"}>
 | 
					      <.dropdown>
 | 
				
			||||||
        <.icon name="hero-user" />
 | 
					        <:button>
 | 
				
			||||||
        <.icon name="hero-chevron-down" />
 | 
					          <.icon name="hero-user-solid" />
 | 
				
			||||||
      </.link>
 | 
					        </:button>
 | 
				
			||||||
      <.link
 | 
					        <:item>
 | 
				
			||||||
        title="Log out"
 | 
					          <.link href={~p"/users/settings"}>Settings</.link>
 | 
				
			||||||
        class="border-r px-3 py-4 hover:bg-white"
 | 
					        </:item>
 | 
				
			||||||
        method="DELETE"
 | 
					        <:item>
 | 
				
			||||||
        href={~p"/auth/log_out"}
 | 
					          <.link method="DELETE" href={~p"/auth/log_out"}>Log out</.link>
 | 
				
			||||||
      >
 | 
					        </:item>
 | 
				
			||||||
        <.icon name="hero-x-mark" />
 | 
					      </.dropdown>
 | 
				
			||||||
      </.link>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</header>
 | 
					</header>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,4 @@
 | 
				
			||||||
defmodule PrymnWeb.ServerLive.Show do
 | 
					defmodule PrymnWeb.ServerLive.Show do
 | 
				
			||||||
  alias DBConnection.App
 | 
					 | 
				
			||||||
  use PrymnWeb, :live_view
 | 
					  use PrymnWeb, :live_view
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  require Logger
 | 
					  require Logger
 | 
				
			||||||
| 
						 | 
					@ -7,44 +6,38 @@ defmodule PrymnWeb.ServerLive.Show do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def mount(_params, _session, socket) do
 | 
					  def mount(_params, _session, socket) do
 | 
				
			||||||
    {:ok, socket}
 | 
					    # TODO: A more lightweight call instead of listing all data?
 | 
				
			||||||
 | 
					    servers = Servers.list_servers()
 | 
				
			||||||
 | 
					    {:ok, assign(socket, :servers, servers)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def render(assigns) do
 | 
					  def render(assigns) do
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div class="mx-auto max-w-2xl">
 | 
					    <div class="mx-auto max-w-2xl">
 | 
				
			||||||
      <.header>
 | 
					      <div class="flex justify-between">
 | 
				
			||||||
        <span id="server-name" class="relative flex items-center">
 | 
					        <div id="server-name" class="relative flex items-center">
 | 
				
			||||||
          <button title="Select a different server">
 | 
					          <.dropdown title="Select a different server">
 | 
				
			||||||
            Server <%= @server.name %>
 | 
					            <:button>Server <%= @server.name %></:button>
 | 
				
			||||||
            <.icon name="hero-chevron-down" />
 | 
					            <:item :for={server <- Enum.filter(@servers, fn s -> s.id != @server.id end)}>
 | 
				
			||||||
          </button>
 | 
					              <.link
 | 
				
			||||||
          <button class="ml-4" title="Edit server name" phx-click={show_edit_server_name()}>
 | 
					                patch={~p"/servers/#{server}"}
 | 
				
			||||||
            <.icon class="h-3 w-3" name="hero-pencil" />
 | 
					                class="block text-sm text-gray-700"
 | 
				
			||||||
          </button>
 | 
					                role="menuitem"
 | 
				
			||||||
          <span
 | 
					              >
 | 
				
			||||||
            role="tooltip"
 | 
					                <%= server.name %>
 | 
				
			||||||
            class={[
 | 
					              </.link>
 | 
				
			||||||
              "absolute -left-6 inline-flex h-3 w-3 before:-translate-x-1/2 before:-translate-y-full",
 | 
					            </:item>
 | 
				
			||||||
              "before:-top-2 before:left-1/2 before:absolute before:text-sm before:text-white",
 | 
					          </.dropdown>
 | 
				
			||||||
              "before:font-normal before:content-[attr(data-tip)] before:opacity-0",
 | 
					          <button
 | 
				
			||||||
              "hover:before:opacity-100 before:py-1 before:px-2 before:bg-black",
 | 
					            class="ml-4 inline-flex items-center"
 | 
				
			||||||
              "before:rounded before:pointer-events-none before:transition-opacity"
 | 
					            title="Edit server name"
 | 
				
			||||||
            ]}
 | 
					            phx-click={show_edit_server_name()}
 | 
				
			||||||
            data-tip={@health.message}
 | 
					 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <%= case @health.message do %>
 | 
					            <.icon class="h-4 w-4" name="hero-pencil-solid" />
 | 
				
			||||||
              <% "Connected" -> %>
 | 
					          </button>
 | 
				
			||||||
                <span class="absolute top-0 left-0 h-full w-full animate-ping rounded-full bg-green-400 opacity-75" />
 | 
					          <.indicator message={@health.message} />
 | 
				
			||||||
                <span class="h-3 w-3 rounded-full bg-green-500" />
 | 
					        </div>
 | 
				
			||||||
              <% "Disconnected" -> %>
 | 
					 | 
				
			||||||
                <span class="h-3 w-3 rounded-full bg-red-500" />
 | 
					 | 
				
			||||||
              <% _ -> %>
 | 
					 | 
				
			||||||
                <span class="h-3 w-3 rounded-full bg-yellow-500" />
 | 
					 | 
				
			||||||
            <% end %>
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
        <form class="hidden" id="server-name-edit" phx-submit={submit_edit_server_name()}>
 | 
					        <form class="hidden" id="server-name-edit" phx-submit={submit_edit_server_name()}>
 | 
				
			||||||
          <input
 | 
					          <input
 | 
				
			||||||
            class="border-0 border-b border-gray-500"
 | 
					            class="border-0 border-b border-gray-500"
 | 
				
			||||||
| 
						 | 
					@ -53,25 +46,26 @@ defmodule PrymnWeb.ServerLive.Show do
 | 
				
			||||||
            value={@server.name}
 | 
					            value={@server.name}
 | 
				
			||||||
            required
 | 
					            required
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <button type="submit" title="Confirm" class="ml-4">
 | 
					          <button type="submit" title="Confirm">
 | 
				
			||||||
            <.icon class="h-4 w-4" name="hero-check" />
 | 
					            <.icon name="hero-check" />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
        </form>
 | 
					        </form>
 | 
				
			||||||
        <:subtitle>
 | 
					        <div class="space-x-2">
 | 
				
			||||||
          <%= @server.public_ip %>
 | 
					 | 
				
			||||||
        </:subtitle>
 | 
					 | 
				
			||||||
        <:actions>
 | 
					 | 
				
			||||||
          <Button.primary size="sm">New App</Button.primary>
 | 
					          <Button.primary size="sm">New App</Button.primary>
 | 
				
			||||||
          <Button.secondary size="sm">
 | 
					          <.dropdown>
 | 
				
			||||||
            Quick actions <.icon name="hero-chevron-down" class="ml-1" />
 | 
					            <:button>
 | 
				
			||||||
          </Button.secondary>
 | 
					              Quick actions
 | 
				
			||||||
        </:actions>
 | 
					            </:button>
 | 
				
			||||||
      </.header>
 | 
					            <:item>
 | 
				
			||||||
      <div class="my-3 text-sm text-slate-700">
 | 
					              Test
 | 
				
			||||||
        <%= for {name, task} <- @health.tasks do %>
 | 
					            </:item>
 | 
				
			||||||
          Background task in progress: <%= name %>
 | 
					          </.dropdown>
 | 
				
			||||||
          <p><%= task.progress %> complete</p>
 | 
					        </div>
 | 
				
			||||||
        <% end %>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <span class="text-sm opacity-75"><%= @server.public_ip %></span>
 | 
				
			||||||
 | 
					      <div :for={{name, task} <- @health.tasks} class="my-3 text-sm text-slate-700">
 | 
				
			||||||
 | 
					        <p>Background task in progress: <%= name %></p>
 | 
				
			||||||
 | 
					        <p><%= task.progress %> complete</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div :if={@server.status == :unregistered} class="my-10">
 | 
					      <div :if={@server.status == :unregistered} class="my-10">
 | 
				
			||||||
        <p class="mb-9">
 | 
					        <p class="mb-9">
 | 
				
			||||||
| 
						 | 
					@ -229,4 +223,30 @@ defmodule PrymnWeb.ServerLive.Show do
 | 
				
			||||||
    |> JS.hide()
 | 
					    |> JS.hide()
 | 
				
			||||||
    |> JS.show(to: "#server-name", display: "flex")
 | 
					    |> JS.show(to: "#server-name", display: "flex")
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp indicator(assigns) do
 | 
				
			||||||
 | 
					    ~H"""
 | 
				
			||||||
 | 
					    <span
 | 
				
			||||||
 | 
					      role="tooltip"
 | 
				
			||||||
 | 
					      class={[
 | 
				
			||||||
 | 
					        "absolute -left-6 inline-flex h-3 w-3 before:-translate-x-1/2 before:-translate-y-full",
 | 
				
			||||||
 | 
					        "before:-top-2 before:left-1/2 before:absolute before:text-sm before:text-white",
 | 
				
			||||||
 | 
					        "before:font-normal before:content-[attr(data-tip)] before:opacity-0",
 | 
				
			||||||
 | 
					        "hover:before:opacity-100 before:py-1 before:px-2 before:bg-black",
 | 
				
			||||||
 | 
					        "before:rounded before:pointer-events-none before:transition-opacity"
 | 
				
			||||||
 | 
					      ]}
 | 
				
			||||||
 | 
					      data-tip={@message}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <%= case @message do %>
 | 
				
			||||||
 | 
					        <% "Connected" -> %>
 | 
				
			||||||
 | 
					          <span class="absolute top-0 left-0 h-full w-full animate-ping rounded-full bg-green-400 opacity-75" />
 | 
				
			||||||
 | 
					          <span class="h-3 w-3 rounded-full bg-green-500" />
 | 
				
			||||||
 | 
					        <% "Disconnected" -> %>
 | 
				
			||||||
 | 
					          <span class="h-3 w-3 rounded-full bg-red-500" />
 | 
				
			||||||
 | 
					        <% _ -> %>
 | 
				
			||||||
 | 
					          <span class="h-3 w-3 rounded-full bg-yellow-500" />
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue