Improve UI styling and remove DaisyUI dependencies
- Remove default Phoenix branding and create custom layouts - Replace DaisyUI classes with custom Tailwind components throughout - Add authenticated layout with navigation bar for organization pages - Redesign all core components (buttons, forms, tables, alerts) - Make dashboard stat cards clickable to navigate to respective pages - Update alert acknowledge button to only show for equipment down alerts - Add consistent dark mode support across all pages - Implement modern design with zinc color palette and improved spacing
This commit is contained in:
parent
da42d64494
commit
dffa46dfde
12 changed files with 700 additions and 527 deletions
|
|
@ -8,12 +8,8 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
with doc strings and declarative assigns. You may customize and style
|
||||
them in any way you want, based on your application growth and needs.
|
||||
|
||||
The foundation for styling is Tailwind CSS, a utility-first CSS framework,
|
||||
augmented with daisyUI, a Tailwind CSS plugin that provides UI components
|
||||
and themes. Here are useful references:
|
||||
|
||||
* [daisyUI](https://daisyui.com/docs/intro/) - a good place to get
|
||||
started and see the available components.
|
||||
The foundation for styling is Tailwind CSS, a utility-first CSS framework.
|
||||
Here are useful references:
|
||||
|
||||
* [Tailwind CSS](https://tailwindcss.com) - the foundational framework
|
||||
we build on. You will use it for layout, sizing, flexbox, grid, and
|
||||
|
|
@ -57,24 +53,62 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
id={@id}
|
||||
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
|
||||
role="alert"
|
||||
class="toast toast-top toast-end z-50"
|
||||
class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50"
|
||||
{@rest}
|
||||
>
|
||||
<div class={[
|
||||
"alert w-80 sm:w-96 max-w-80 sm:max-w-96 text-wrap",
|
||||
@kind == :info && "alert-info",
|
||||
@kind == :error && "alert-error"
|
||||
]}>
|
||||
<.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
|
||||
<.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
|
||||
<div>
|
||||
<p :if={@title} class="font-semibold">{@title}</p>
|
||||
<p>{msg}</p>
|
||||
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
|
||||
<div class={[
|
||||
"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5",
|
||||
@kind == :info && "bg-blue-50 dark:bg-blue-950",
|
||||
@kind == :error && "bg-red-50 dark:bg-red-950"
|
||||
]}>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<.icon
|
||||
:if={@kind == :info}
|
||||
name="hero-information-circle"
|
||||
class="h-6 w-6 text-blue-600 dark:text-blue-400"
|
||||
/>
|
||||
<.icon
|
||||
:if={@kind == :error}
|
||||
name="hero-exclamation-circle"
|
||||
class="h-6 w-6 text-red-600 dark:text-red-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p :if={@title} class={[
|
||||
"text-sm font-medium",
|
||||
@kind == :info && "text-blue-900 dark:text-blue-100",
|
||||
@kind == :error && "text-red-900 dark:text-red-100"
|
||||
]}>
|
||||
{@title}
|
||||
</p>
|
||||
<p class={[
|
||||
"text-sm",
|
||||
@title && "mt-1",
|
||||
@kind == :info && "text-blue-700 dark:text-blue-200",
|
||||
@kind == :error && "text-red-700 dark:text-red-200"
|
||||
]}>
|
||||
{msg}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4 flex flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class={[
|
||||
"inline-flex rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2",
|
||||
@kind == :info && "text-blue-500 hover:text-blue-600 focus:ring-blue-500 dark:text-blue-400",
|
||||
@kind == :error && "text-red-500 hover:text-red-600 focus:ring-red-500 dark:text-red-400"
|
||||
]}
|
||||
aria-label={gettext("close")}
|
||||
>
|
||||
<.icon name="hero-x-mark" class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
<button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}>
|
||||
<.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
@ -95,11 +129,19 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
slot :inner_block, required: true
|
||||
|
||||
def button(%{rest: rest} = assigns) do
|
||||
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
|
||||
variants = %{
|
||||
"primary" =>
|
||||
"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600",
|
||||
nil =>
|
||||
"bg-white text-zinc-900 ring-1 ring-inset ring-zinc-300 hover:bg-zinc-50 focus:ring-blue-500 dark:bg-zinc-800 dark:text-zinc-100 dark:ring-zinc-700 dark:hover:bg-zinc-700"
|
||||
}
|
||||
|
||||
assigns =
|
||||
assign_new(assigns, :class, fn ->
|
||||
["btn", Map.fetch!(variants, assigns[:variant])]
|
||||
[
|
||||
"inline-flex items-center gap-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
Map.fetch!(variants, assigns[:variant])
|
||||
]
|
||||
end)
|
||||
|
||||
if rest[:href] || rest[:navigate] || rest[:patch] do
|
||||
|
|
@ -204,8 +246,8 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
end)
|
||||
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
<label>
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center gap-3">
|
||||
<input
|
||||
type="hidden"
|
||||
name={@name}
|
||||
|
|
@ -213,17 +255,19 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
disabled={@rest[:disabled]}
|
||||
form={@rest[:form]}
|
||||
/>
|
||||
<span class="label">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={@id}
|
||||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class={@class || "checkbox checkbox-sm"}
|
||||
{@rest}
|
||||
/>{@label}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={@id}
|
||||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class={
|
||||
@class ||
|
||||
"h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800"
|
||||
}
|
||||
{@rest}
|
||||
/>
|
||||
<span class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{@label}</span>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}>{msg}</.error>
|
||||
</div>
|
||||
|
|
@ -232,13 +276,23 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
|
||||
def input(%{type: "select"} = assigns) do
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
<div class="mb-4">
|
||||
<label>
|
||||
<span :if={@label} class="label mb-1">{@label}</span>
|
||||
<span :if={@label} class="block text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-2">
|
||||
{@label}
|
||||
</span>
|
||||
<select
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[@class || "w-full select", @errors != [] && (@error_class || "select-error")]}
|
||||
class={[
|
||||
@class ||
|
||||
"block w-full rounded-lg border-0 py-2 px-3 text-zinc-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 dark:bg-zinc-800 dark:text-zinc-100",
|
||||
@errors == [] &&
|
||||
"ring-zinc-300 focus:ring-blue-600 dark:ring-zinc-700 dark:focus:ring-blue-500",
|
||||
@errors != [] &&
|
||||
(@error_class ||
|
||||
"ring-red-300 focus:ring-red-500 dark:ring-red-700 dark:focus:ring-red-500")
|
||||
]}
|
||||
multiple={@multiple}
|
||||
{@rest}
|
||||
>
|
||||
|
|
@ -253,15 +307,22 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
|
||||
def input(%{type: "textarea"} = assigns) do
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
<div class="mb-4">
|
||||
<label>
|
||||
<span :if={@label} class="label mb-1">{@label}</span>
|
||||
<span :if={@label} class="block text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-2">
|
||||
{@label}
|
||||
</span>
|
||||
<textarea
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[
|
||||
@class || "w-full textarea",
|
||||
@errors != [] && (@error_class || "textarea-error")
|
||||
@class ||
|
||||
"block w-full rounded-lg border-0 py-2 px-3 text-zinc-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 dark:bg-zinc-800 dark:text-zinc-100",
|
||||
@errors == [] &&
|
||||
"ring-zinc-300 focus:ring-blue-600 dark:ring-zinc-700 dark:focus:ring-blue-500",
|
||||
@errors != [] &&
|
||||
(@error_class ||
|
||||
"ring-red-300 focus:ring-red-500 dark:ring-red-700 dark:focus:ring-red-500")
|
||||
]}
|
||||
{@rest}
|
||||
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
|
||||
|
|
@ -274,17 +335,24 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
||||
def input(assigns) do
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
<div class="mb-4">
|
||||
<label>
|
||||
<span :if={@label} class="label mb-1">{@label}</span>
|
||||
<span :if={@label} class="block text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-2">
|
||||
{@label}
|
||||
</span>
|
||||
<input
|
||||
type={@type}
|
||||
name={@name}
|
||||
id={@id}
|
||||
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
||||
class={[
|
||||
@class || "w-full input",
|
||||
@errors != [] && (@error_class || "input-error")
|
||||
@class ||
|
||||
"block w-full rounded-lg border-0 py-2 px-3 text-zinc-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 dark:bg-zinc-800 dark:text-zinc-100",
|
||||
@errors == [] &&
|
||||
"ring-zinc-300 focus:ring-blue-600 dark:ring-zinc-700 dark:focus:ring-blue-500",
|
||||
@errors != [] &&
|
||||
(@error_class ||
|
||||
"ring-red-300 focus:ring-red-500 dark:ring-red-700 dark:focus:ring-red-500")
|
||||
]}
|
||||
{@rest}
|
||||
/>
|
||||
|
|
@ -297,8 +365,8 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
# Helper used by inputs to generate form errors
|
||||
defp error(assigns) do
|
||||
~H"""
|
||||
<p class="mt-1.5 flex gap-2 items-center text-sm text-error">
|
||||
<.icon name="hero-exclamation-circle" class="size-5" />
|
||||
<p class="mt-2 flex gap-2 items-center text-sm text-red-600 dark:text-red-400">
|
||||
<.icon name="hero-exclamation-circle" class="h-5 w-5 flex-shrink-0" />
|
||||
{render_slot(@inner_block)}
|
||||
</p>
|
||||
"""
|
||||
|
|
@ -313,12 +381,12 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
|
||||
def header(assigns) do
|
||||
~H"""
|
||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4"]}>
|
||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", "mb-8"]}>
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold leading-8">
|
||||
<h1 class="text-3xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{render_slot(@inner_block)}
|
||||
</h1>
|
||||
<p :if={@subtitle != []} class="text-sm text-base-content/70">
|
||||
<p :if={@subtitle != []} class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{render_slot(@subtitle)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -359,34 +427,52 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
end
|
||||
|
||||
~H"""
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :for={col <- @col}>{col[:label]}</th>
|
||||
<th :if={@action != []}>
|
||||
<span class="sr-only">{gettext("Actions")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
|
||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
|
||||
<td
|
||||
:for={col <- @col}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={@row_click && "hover:cursor-pointer"}
|
||||
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
|
||||
<table class="min-w-full divide-y divide-zinc-300 dark:divide-zinc-700">
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-900">
|
||||
<tr>
|
||||
<th
|
||||
:for={col <- @col}
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-700 dark:text-zinc-300"
|
||||
>
|
||||
{col[:label]}
|
||||
</th>
|
||||
<th :if={@action != []} class="px-6 py-3">
|
||||
<span class="sr-only">{gettext("Actions")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
id={@id}
|
||||
phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}
|
||||
class="divide-y divide-zinc-200 bg-white dark:divide-zinc-800 dark:bg-zinc-950"
|
||||
>
|
||||
<tr
|
||||
:for={row <- @rows}
|
||||
id={@row_id && @row_id.(row)}
|
||||
class="hover:bg-zinc-50 dark:hover:bg-zinc-900"
|
||||
>
|
||||
{render_slot(col, @row_item.(row))}
|
||||
</td>
|
||||
<td :if={@action != []} class="w-0 font-semibold">
|
||||
<div class="flex gap-4">
|
||||
<%= for action <- @action do %>
|
||||
{render_slot(action, @row_item.(row))}
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<td
|
||||
:for={col <- @col}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={[
|
||||
"px-6 py-4 whitespace-nowrap text-sm text-zinc-900 dark:text-zinc-100",
|
||||
@row_click && "cursor-pointer"
|
||||
]}
|
||||
>
|
||||
{render_slot(col, @row_item.(row))}
|
||||
</td>
|
||||
<td :if={@action != []} class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex gap-4 justify-end">
|
||||
<%= for action <- @action do %>
|
||||
{render_slot(action, @row_item.(row))}
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
@ -406,14 +492,12 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
|
||||
def list(assigns) do
|
||||
~H"""
|
||||
<ul class="list">
|
||||
<li :for={item <- @item} class="list-row">
|
||||
<div class="list-col-grow">
|
||||
<div class="font-bold">{item.title}</div>
|
||||
<div>{render_slot(item)}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="overflow-hidden bg-white shadow rounded-lg divide-y divide-zinc-200 dark:bg-zinc-900 dark:divide-zinc-800">
|
||||
<div :for={item <- @item} class="px-6 py-4">
|
||||
<dt class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{item.title}</dt>
|
||||
<dd class="mt-1 text-sm text-zinc-700 dark:text-zinc-300">{render_slot(item)}</dd>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -35,35 +35,8 @@ defmodule ToweropsWeb.Layouts do
|
|||
|
||||
def app(assigns) do
|
||||
~H"""
|
||||
<header class="navbar px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex-1">
|
||||
<a href="/" class="flex-1 flex w-fit items-center gap-2">
|
||||
<img src={~p"/images/logo.svg"} width="36" />
|
||||
<span class="text-sm font-semibold">v{Application.spec(:phoenix, :vsn)}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<ul class="flex flex-column px-1 space-x-4 items-center">
|
||||
<li>
|
||||
<a href="https://phoenixframework.org/" class="btn btn-ghost">Website</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/phoenixframework/phoenix" class="btn btn-ghost">GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<.theme_toggle />
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://hexdocs.pm/phoenix/overview.html" class="btn btn-primary">
|
||||
Get Started <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl space-y-4">
|
||||
<main class="min-h-screen bg-zinc-50 px-4 py-12 sm:px-6 lg:px-8 dark:bg-zinc-950">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -72,6 +45,106 @@ defmodule ToweropsWeb.Layouts do
|
|||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the authenticated app layout with navigation.
|
||||
|
||||
Use this for pages that require organization context.
|
||||
|
||||
## Examples
|
||||
|
||||
<Layouts.authenticated flash={@flash} current_organization={@current_organization}>
|
||||
<h1>Content</h1>
|
||||
</Layouts.authenticated>
|
||||
|
||||
"""
|
||||
attr :flash, :map, required: true, doc: "the map of flash messages"
|
||||
|
||||
attr :current_organization, :map,
|
||||
default: nil,
|
||||
doc: "the current organization"
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def authenticated(assigns) do
|
||||
~H"""
|
||||
<div class="min-h-screen bg-zinc-50 dark:bg-zinc-950">
|
||||
<nav class="border-b border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 justify-between">
|
||||
<div class="flex">
|
||||
<div class="flex flex-shrink-0 items-center">
|
||||
<.link navigate={~p"/orgs"} class="text-xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
Towerops
|
||||
</.link>
|
||||
</div>
|
||||
<div :if={@current_organization} class="ml-10 flex space-x-8">
|
||||
<.nav_link navigate={~p"/orgs/#{@current_organization.slug}"}>
|
||||
Dashboard
|
||||
</.nav_link>
|
||||
<.nav_link navigate={~p"/orgs/#{@current_organization.slug}/sites"}>
|
||||
Sites
|
||||
</.nav_link>
|
||||
<.nav_link navigate={~p"/orgs/#{@current_organization.slug}/equipment"}>
|
||||
Equipment
|
||||
</.nav_link>
|
||||
<.nav_link navigate={~p"/orgs/#{@current_organization.slug}/alerts"}>
|
||||
Alerts
|
||||
</.nav_link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-6">
|
||||
<span :if={@current_organization} class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{@current_organization.name}
|
||||
</span>
|
||||
<.link
|
||||
navigate={~p"/orgs"}
|
||||
class="text-sm font-medium text-zinc-700 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
|
||||
>
|
||||
Switch Org
|
||||
</.link>
|
||||
<.link
|
||||
navigate={~p"/users/settings"}
|
||||
class="text-sm font-medium text-zinc-700 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
|
||||
>
|
||||
Settings
|
||||
</.link>
|
||||
<.link
|
||||
href={~p"/users/log-out"}
|
||||
method="delete"
|
||||
class="text-sm font-medium text-zinc-700 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
|
||||
>
|
||||
Log out
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="py-10">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
"""
|
||||
end
|
||||
|
||||
attr :navigate, :string, required: true
|
||||
slot :inner_block, required: true
|
||||
|
||||
defp nav_link(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
navigate={@navigate}
|
||||
class="inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-zinc-700 hover:border-zinc-300 hover:text-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-700 dark:hover:text-zinc-100"
|
||||
>
|
||||
{render_slot(@inner_block)}
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shows the flash group with standard titles and content.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="csrf-token" content={get_csrf_token()} />
|
||||
<.live_title default="Towerops" suffix=" · Phoenix Framework">
|
||||
<.live_title default="Towerops">
|
||||
{assigns[:page_title]}
|
||||
</.live_title>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
||||
|
|
@ -31,26 +31,6 @@
|
|||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<ul class="menu menu-horizontal w-full relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
|
||||
<%= if @current_scope do %>
|
||||
<li>
|
||||
{@current_scope.user.email}
|
||||
</li>
|
||||
<li>
|
||||
<.link href={~p"/users/settings"}>Settings</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link href={~p"/users/log-out"} method="delete">Log out</.link>
|
||||
</li>
|
||||
<% else %>
|
||||
<li>
|
||||
<.link href={~p"/users/register"}>Register</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link href={~p"/users/log-in"}>Log in</.link>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
{@inner_content}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
Register for an account
|
||||
<:subtitle>
|
||||
Already registered?
|
||||
<.link navigate={~p"/users/log-in"} class="font-semibold text-brand hover:underline">
|
||||
<.link
|
||||
navigate={~p"/users/log-in"}
|
||||
class="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Log in
|
||||
</.link>
|
||||
to your account now.
|
||||
|
|
@ -13,19 +16,21 @@
|
|||
</.header>
|
||||
</div>
|
||||
|
||||
<.form :let={f} for={@changeset} action={~p"/users/register"}>
|
||||
<.input
|
||||
field={f[:email]}
|
||||
type="email"
|
||||
label="Email"
|
||||
autocomplete="email"
|
||||
required
|
||||
phx-mounted={JS.focus()}
|
||||
/>
|
||||
<div class="mt-8">
|
||||
<.form :let={f} for={@changeset} action={~p"/users/register"}>
|
||||
<.input
|
||||
field={f[:email]}
|
||||
type="email"
|
||||
label="Email"
|
||||
autocomplete="email"
|
||||
required
|
||||
phx-mounted={JS.focus()}
|
||||
/>
|
||||
|
||||
<.button phx-disable-with="Creating account..." class="btn btn-primary w-full">
|
||||
Create an account
|
||||
</.button>
|
||||
</.form>
|
||||
<.button phx-disable-with="Creating account..." class="w-full" variant="primary">
|
||||
Create an account
|
||||
</.button>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
|
|
@ -4,55 +4,66 @@
|
|||
<.header>Welcome {@user.email}</.header>
|
||||
</div>
|
||||
|
||||
<.form
|
||||
:if={!@user.confirmed_at}
|
||||
for={@form}
|
||||
id="confirmation_form"
|
||||
action={~p"/users/log-in?_action=confirmed"}
|
||||
phx-mounted={JS.focus_first()}
|
||||
>
|
||||
<input type="hidden" name={@form[:token].name} value={@form[:token].value} />
|
||||
<.button
|
||||
name={@form[:remember_me].name}
|
||||
value="true"
|
||||
phx-disable-with="Confirming..."
|
||||
class="btn btn-primary w-full"
|
||||
<div class="mt-8 space-y-3">
|
||||
<.form
|
||||
:if={!@user.confirmed_at}
|
||||
for={@form}
|
||||
id="confirmation_form"
|
||||
action={~p"/users/log-in?_action=confirmed"}
|
||||
phx-mounted={JS.focus_first()}
|
||||
>
|
||||
Confirm and stay logged in
|
||||
</.button>
|
||||
<.button phx-disable-with="Confirming..." class="btn btn-primary btn-soft w-full mt-2">
|
||||
Confirm and log in only this time
|
||||
</.button>
|
||||
</.form>
|
||||
<input type="hidden" name={@form[:token].name} value={@form[:token].value} />
|
||||
<div class="space-y-3">
|
||||
<.button
|
||||
name={@form[:remember_me].name}
|
||||
value="true"
|
||||
phx-disable-with="Confirming..."
|
||||
class="w-full"
|
||||
variant="primary"
|
||||
>
|
||||
Confirm and stay logged in
|
||||
</.button>
|
||||
<.button phx-disable-with="Confirming..." class="w-full">
|
||||
Confirm and log in only this time
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
||||
<.form
|
||||
:if={@user.confirmed_at}
|
||||
for={@form}
|
||||
id="login_form"
|
||||
action={~p"/users/log-in"}
|
||||
phx-mounted={JS.focus_first()}
|
||||
<.form
|
||||
:if={@user.confirmed_at}
|
||||
for={@form}
|
||||
id="login_form"
|
||||
action={~p"/users/log-in"}
|
||||
phx-mounted={JS.focus_first()}
|
||||
>
|
||||
<input type="hidden" name={@form[:token].name} value={@form[:token].value} />
|
||||
<%= if @current_scope do %>
|
||||
<.button variant="primary" phx-disable-with="Logging in..." class="w-full">
|
||||
Log in
|
||||
</.button>
|
||||
<% else %>
|
||||
<div class="space-y-3">
|
||||
<.button
|
||||
name={@form[:remember_me].name}
|
||||
value="true"
|
||||
phx-disable-with="Logging in..."
|
||||
class="w-full"
|
||||
variant="primary"
|
||||
>
|
||||
Log in and stay logged in
|
||||
</.button>
|
||||
<.button phx-disable-with="Logging in..." class="w-full">
|
||||
Log in only this time
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
</.form>
|
||||
</div>
|
||||
|
||||
<p
|
||||
:if={!@user.confirmed_at}
|
||||
class="mt-6 rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200"
|
||||
>
|
||||
<input type="hidden" name={@form[:token].name} value={@form[:token].value} />
|
||||
<%= if @current_scope do %>
|
||||
<.button variant="primary" phx-disable-with="Logging in..." class="btn btn-primary w-full">
|
||||
Log in
|
||||
</.button>
|
||||
<% else %>
|
||||
<.button
|
||||
name={@form[:remember_me].name}
|
||||
value="true"
|
||||
phx-disable-with="Logging in..."
|
||||
class="btn btn-primary w-full"
|
||||
>
|
||||
Keep me logged in on this device
|
||||
</.button>
|
||||
<.button phx-disable-with="Logging in..." class="btn btn-primary btn-soft w-full mt-2">
|
||||
Log me in only this time
|
||||
</.button>
|
||||
<% end %>
|
||||
</.form>
|
||||
|
||||
<p :if={!@user.confirmed_at} class="alert alert-outline mt-8">
|
||||
Tip: If you prefer passwords, you can enable them in the user settings.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,70 +1,89 @@
|
|||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||
<div class="mx-auto max-w-sm space-y-4">
|
||||
<div class="mx-auto max-w-sm">
|
||||
<div class="text-center">
|
||||
<.header>
|
||||
<p>Log in</p>
|
||||
Log in
|
||||
<:subtitle>
|
||||
<%= if @current_scope do %>
|
||||
You need to reauthenticate to perform sensitive actions on your account.
|
||||
<% else %>
|
||||
Don't have an account? <.link
|
||||
Don't have an account?
|
||||
<.link
|
||||
navigate={~p"/users/register"}
|
||||
class="font-semibold text-brand hover:underline"
|
||||
phx-no-format
|
||||
>Sign up</.link> for an account now.
|
||||
class="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Sign up
|
||||
</.link>
|
||||
for an account now.
|
||||
<% end %>
|
||||
</:subtitle>
|
||||
</.header>
|
||||
</div>
|
||||
|
||||
<div :if={local_mail_adapter?()} class="alert alert-info">
|
||||
<.icon name="hero-information-circle" class="size-6 shrink-0" />
|
||||
<div>
|
||||
<p>You are running the local mail adapter.</p>
|
||||
<p>
|
||||
To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
|
||||
</p>
|
||||
<div
|
||||
:if={local_mail_adapter?()}
|
||||
class="mt-6 rounded-lg bg-blue-50 p-4 dark:bg-blue-950"
|
||||
>
|
||||
<div class="flex">
|
||||
<.icon name="hero-information-circle" class="h-6 w-6 flex-shrink-0 text-blue-600 dark:text-blue-400" />
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-200">
|
||||
You are running the local mail adapter.
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-blue-700 dark:text-blue-200">
|
||||
To see sent emails, visit
|
||||
<.link href="/dev/mailbox" class="font-medium underline">the mailbox page</.link>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.form :let={f} for={@form} as={:user} id="login_form_magic" action={~p"/users/log-in"}>
|
||||
<.input
|
||||
readonly={!!@current_scope}
|
||||
field={f[:email]}
|
||||
type="email"
|
||||
label="Email"
|
||||
autocomplete="email"
|
||||
required
|
||||
phx-mounted={JS.focus()}
|
||||
/>
|
||||
<.button class="btn btn-primary w-full">
|
||||
Log in with email <span aria-hidden="true">→</span>
|
||||
</.button>
|
||||
</.form>
|
||||
<div class="mt-8 space-y-6">
|
||||
<.form :let={f} for={@form} as={:user} id="login_form_magic" action={~p"/users/log-in"}>
|
||||
<.input
|
||||
readonly={!!@current_scope}
|
||||
field={f[:email]}
|
||||
type="email"
|
||||
label="Email"
|
||||
autocomplete="email"
|
||||
required
|
||||
phx-mounted={JS.focus()}
|
||||
/>
|
||||
<.button class="w-full" variant="primary">
|
||||
Log in with email <span aria-hidden="true">→</span>
|
||||
</.button>
|
||||
</.form>
|
||||
|
||||
<div class="divider">or</div>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-zinc-300 dark:border-zinc-700"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="bg-zinc-50 px-2 text-zinc-600 dark:bg-zinc-950 dark:text-zinc-400">
|
||||
or
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.form :let={f} for={@form} as={:user} id="login_form_password" action={~p"/users/log-in"}>
|
||||
<.input
|
||||
readonly={!!@current_scope}
|
||||
field={f[:email]}
|
||||
type="email"
|
||||
label="Email"
|
||||
autocomplete="email"
|
||||
required
|
||||
/>
|
||||
<.input
|
||||
field={f[:password]}
|
||||
type="password"
|
||||
label="Password"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<.button class="btn btn-primary w-full" name={@form[:remember_me].name} value="true">
|
||||
Log in and stay logged in <span aria-hidden="true">→</span>
|
||||
</.button>
|
||||
<.button class="btn btn-primary btn-soft w-full mt-2">
|
||||
Log in only this time
|
||||
</.button>
|
||||
</.form>
|
||||
<.form :let={f} for={@form} as={:user} id="login_form_password" action={~p"/users/log-in"}>
|
||||
<.input
|
||||
readonly={!!@current_scope}
|
||||
field={f[:email]}
|
||||
type="email"
|
||||
label="Email"
|
||||
autocomplete="email"
|
||||
required
|
||||
/>
|
||||
<.input field={f[:password]} type="password" label="Password" autocomplete="current-password" />
|
||||
<div class="space-y-3">
|
||||
<.button class="w-full" variant="primary" name={@form[:remember_me].name} value="true">
|
||||
Log in and stay logged in <span aria-hidden="true">→</span>
|
||||
</.button>
|
||||
<.button class="w-full">
|
||||
Log in only this time
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,43 @@
|
|||
<.header>
|
||||
Alerts
|
||||
<:subtitle>Monitor and acknowledge system alerts</:subtitle>
|
||||
</.header>
|
||||
<Layouts.authenticated flash={@flash} current_organization={@current_organization}>
|
||||
<.header>
|
||||
Alerts
|
||||
<:subtitle>Monitor and acknowledge system alerts</:subtitle>
|
||||
</.header>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="tabs tabs-boxed">
|
||||
<.link
|
||||
patch={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
class={["tab", @filter == "all" && "tab-active"]}
|
||||
>
|
||||
All Alerts
|
||||
</.link>
|
||||
<.link
|
||||
patch={~p"/orgs/#{@current_organization.slug}/alerts?filter=active"}
|
||||
class={["tab", @filter == "active" && "tab-active"]}
|
||||
>
|
||||
Active Alerts
|
||||
</.link>
|
||||
<div class="mb-6">
|
||||
<div class="inline-flex rounded-lg border border-zinc-200 dark:border-zinc-800">
|
||||
<.link
|
||||
patch={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
class={[
|
||||
"px-4 py-2 text-sm font-medium rounded-l-lg",
|
||||
@filter == "all" &&
|
||||
"bg-blue-600 text-white dark:bg-blue-500",
|
||||
@filter != "all" &&
|
||||
"bg-white text-zinc-700 hover:bg-zinc-50 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
||||
]}
|
||||
>
|
||||
All Alerts
|
||||
</.link>
|
||||
<.link
|
||||
patch={~p"/orgs/#{@current_organization.slug}/alerts?filter=active"}
|
||||
class={[
|
||||
"px-4 py-2 text-sm font-medium rounded-r-lg border-l border-zinc-200 dark:border-zinc-800",
|
||||
@filter == "active" &&
|
||||
"bg-blue-600 text-white dark:bg-blue-500",
|
||||
@filter != "active" &&
|
||||
"bg-white text-zinc-700 hover:bg-zinc-50 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
||||
]}
|
||||
>
|
||||
Active Alerts
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<%= if Enum.empty?(@alerts) do %>
|
||||
<div class="text-center py-12">
|
||||
<.icon name="hero-bell-slash" class="w-12 h-12 mx-auto text-base-content/40" />
|
||||
<h3 class="mt-2 text-sm font-semibold text-base-content/80">No alerts</h3>
|
||||
<p class="mt-1 text-sm text-base-content/60">
|
||||
<div class="text-center py-16">
|
||||
<.icon name="hero-bell-slash" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
|
||||
<h3 class="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">No alerts</h3>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<%= if @filter == "active" do %>
|
||||
There are no active alerts for this organization.
|
||||
<% else %>
|
||||
|
|
@ -37,104 +49,102 @@
|
|||
<div class="space-y-4">
|
||||
<%= for alert <- @alerts do %>
|
||||
<div class={[
|
||||
"card bg-base-200",
|
||||
"rounded-lg border p-6 shadow-sm",
|
||||
alert.resolved_at && "opacity-60",
|
||||
alert.alert_type == :equipment_down && is_nil(alert.resolved_at) &&
|
||||
"border-l-4 border-error"
|
||||
"border-l-4 border-red-500 bg-red-50 dark:bg-red-950 dark:border-red-700",
|
||||
(alert.alert_type != :equipment_down || alert.resolved_at) &&
|
||||
"border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900"
|
||||
]}>
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class={[
|
||||
"badge badge-lg",
|
||||
alert.alert_type == :equipment_down && "badge-error",
|
||||
alert.alert_type == :equipment_up && "badge-success"
|
||||
]}>
|
||||
<%= case alert.alert_type do %>
|
||||
<% :equipment_down -> %>
|
||||
<.icon name="hero-exclamation-triangle" class="w-4 h-4 mr-1" />
|
||||
Equipment Down
|
||||
<% :equipment_up -> %>
|
||||
<.icon name="hero-check-circle" class="w-4 h-4 mr-1" />
|
||||
Equipment Recovered
|
||||
<% end %>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<span class={[
|
||||
"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium",
|
||||
alert.alert_type == :equipment_down &&
|
||||
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
||||
alert.alert_type == :equipment_up &&
|
||||
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
||||
]}>
|
||||
<%= case alert.alert_type do %>
|
||||
<% :equipment_down -> %>
|
||||
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
|
||||
Equipment Down
|
||||
<% :equipment_up -> %>
|
||||
<.icon name="hero-check-circle" class="h-4 w-4" />
|
||||
Equipment Recovered
|
||||
<% end %>
|
||||
</span>
|
||||
|
||||
<%= if alert.resolved_at do %>
|
||||
<span class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
|
||||
Resolved
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<%= if alert.resolved_at do %>
|
||||
<span class="badge badge-ghost">Resolved</span>
|
||||
<% end %>
|
||||
|
||||
<%= if alert.acknowledged_at do %>
|
||||
<span class="badge badge-info badge-sm">Acknowledged</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<h3 class="font-semibold mt-3">
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/equipment/#{alert.equipment.id}"
|
||||
}
|
||||
class="link link-hover"
|
||||
>
|
||||
{alert.equipment.name}
|
||||
</.link>
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-base-content/70 mt-1">{alert.message}</p>
|
||||
|
||||
<div class="text-xs text-base-content/50 mt-2 space-y-1">
|
||||
<div>
|
||||
<strong>Triggered:</strong>
|
||||
{Calendar.strftime(alert.triggered_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||
</div>
|
||||
|
||||
<%= if alert.acknowledged_at do %>
|
||||
<div>
|
||||
<strong>Acknowledged:</strong>
|
||||
{Calendar.strftime(alert.acknowledged_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||
<%= if alert.acknowledged_by do %>
|
||||
by {alert.acknowledged_by.email}
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if alert.resolved_at do %>
|
||||
<div>
|
||||
<strong>Resolved:</strong>
|
||||
{Calendar.strftime(alert.resolved_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<strong>Site:</strong>
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@current_organization.slug}/sites/#{alert.equipment.site.id}"
|
||||
}
|
||||
class="link link-hover"
|
||||
>
|
||||
{alert.equipment.site.name}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<%= if is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %>
|
||||
<.button
|
||||
phx-click="acknowledge"
|
||||
phx-value-id={alert.id}
|
||||
class="btn-sm btn-primary"
|
||||
>
|
||||
<.icon name="hero-check" class="w-4 h-4" /> Acknowledge
|
||||
</.button>
|
||||
<%= if alert.acknowledged_at do %>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
Acknowledged
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-3 font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{alert.equipment.id}"}
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{alert.equipment.name}
|
||||
</.link>
|
||||
</h3>
|
||||
|
||||
<p class="mt-1 text-sm text-zinc-700 dark:text-zinc-300">{alert.message}</p>
|
||||
|
||||
<div class="mt-3 space-y-1 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
<div>
|
||||
<strong class="font-medium">Triggered:</strong>
|
||||
{Calendar.strftime(alert.triggered_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||
</div>
|
||||
|
||||
<%= if alert.acknowledged_at do %>
|
||||
<div>
|
||||
<strong class="font-medium">Acknowledged:</strong>
|
||||
{Calendar.strftime(alert.acknowledged_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||
<%= if alert.acknowledged_by do %>
|
||||
by {alert.acknowledged_by.email}
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if alert.resolved_at do %>
|
||||
<div>
|
||||
<strong class="font-medium">Resolved:</strong>
|
||||
{Calendar.strftime(alert.resolved_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<strong class="font-medium">Site:</strong>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites/#{alert.equipment.site.id}"}
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{alert.equipment.site.name}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<%= if alert.alert_type == :equipment_down && is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %>
|
||||
<.button phx-click="acknowledge" phx-value-id={alert.id} variant="primary">
|
||||
<.icon name="hero-check" class="h-4 w-4" /> Acknowledge
|
||||
</.button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</Layouts.authenticated>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
<.header>
|
||||
Dashboard
|
||||
<:subtitle>Welcome to {@current_organization.name}</:subtitle>
|
||||
</.header>
|
||||
<Layouts.authenticated flash={@flash} current_organization={@current_organization}>
|
||||
<.header>
|
||||
Dashboard
|
||||
<:subtitle>Welcome to {@current_organization.name}</:subtitle>
|
||||
</.header>
|
||||
|
||||
<div class="mt-8 grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites"}
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Sites</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-zinc-100">{@sites_count}</p>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">Total sites</p>
|
||||
</div>
|
||||
</.link>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment"}
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Equipment</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-zinc-100">{@equipment_count}</p>
|
||||
<div class="mt-3 space-y-1.5 text-sm">
|
||||
|
|
@ -27,9 +34,12 @@
|
|||
{@equipment_unknown} Unknown
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.link>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/alerts"}
|
||||
class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Active Alerts</h3>
|
||||
<p class={[
|
||||
"mt-2 text-3xl font-bold",
|
||||
|
|
@ -39,7 +49,7 @@
|
|||
{length(@active_alerts)}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">Requires attention</p>
|
||||
</div>
|
||||
</.link>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400">System Status</h3>
|
||||
|
|
@ -130,3 +140,4 @@
|
|||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</Layouts.authenticated>
|
||||
|
|
|
|||
|
|
@ -1,84 +1,71 @@
|
|||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>Monitor and manage all your network equipment</:subtitle>
|
||||
<:actions>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/new"}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<.icon name="hero-plus" class="w-5 h-5" /> New Equipment
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
<Layouts.authenticated flash={@flash} current_organization={@current_organization}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>Monitor and manage all your network equipment</:subtitle>
|
||||
<:actions>
|
||||
<.button navigate={~p"/orgs/#{@current_organization.slug}/equipment/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Equipment
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<div class="mt-8">
|
||||
<%= if @equipment == [] do %>
|
||||
<div class="text-center py-12">
|
||||
<.icon name="hero-server" class="mx-auto w-12 h-12 text-base-content/40" />
|
||||
<h3 class="mt-2 text-sm font-semibold text-base-content">No equipment</h3>
|
||||
<p class="mt-1 text-sm text-base-content/60">Get started by adding your first equipment.</p>
|
||||
<div class="text-center py-16">
|
||||
<.icon name="hero-server" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
|
||||
<h3 class="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">No equipment</h3>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Get started by adding your first equipment.
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/new"}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<.icon name="hero-plus" class="w-5 h-5" /> New Equipment
|
||||
</.link>
|
||||
<.button navigate={~p"/orgs/#{@current_organization.slug}/equipment/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Equipment
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>IP Address</th>
|
||||
<th>Site</th>
|
||||
<th>Status</th>
|
||||
<th>Last Checked</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :for={eq <- @equipment}>
|
||||
<td class="font-medium">{eq.name}</td>
|
||||
<td class="font-mono text-sm">{eq.ip_address}</td>
|
||||
<td>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites/#{eq.site.id}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
{eq.site.name}
|
||||
</.link>
|
||||
</td>
|
||||
<td>
|
||||
<span class={[
|
||||
"badge",
|
||||
eq.status == :up && "badge-success",
|
||||
eq.status == :down && "badge-error",
|
||||
eq.status == :unknown && "badge-ghost"
|
||||
]}>
|
||||
{eq.status |> to_string() |> String.upcase()}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-sm text-base-content/60">
|
||||
<%= if eq.last_checked_at do %>
|
||||
{Calendar.strftime(eq.last_checked_at, "%Y-%m-%d %H:%M")}
|
||||
<% else %>
|
||||
Never
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{eq.id}"}
|
||||
class="btn btn-xs"
|
||||
>
|
||||
View
|
||||
</.link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<.table id="equipment" rows={@equipment}>
|
||||
<:col :let={eq} label="Name">
|
||||
<span class="font-medium">{eq.name}</span>
|
||||
</:col>
|
||||
<:col :let={eq} label="IP Address">
|
||||
<span class="font-mono text-sm">{eq.ip_address}</span>
|
||||
</:col>
|
||||
<:col :let={eq} label="Site">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites/#{eq.site.id}"}
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{eq.site.name}
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={eq} label="Status">
|
||||
<span class={[
|
||||
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
|
||||
eq.status == :up && "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
||||
eq.status == :down && "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
||||
eq.status == :unknown &&
|
||||
"bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
|
||||
]}>
|
||||
{eq.status |> to_string() |> String.upcase()}
|
||||
</span>
|
||||
</:col>
|
||||
<:col :let={eq} label="Last Checked">
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<%= if eq.last_checked_at do %>
|
||||
{Calendar.strftime(eq.last_checked_at, "%Y-%m-%d %H:%M")}
|
||||
<% else %>
|
||||
Never
|
||||
<% end %>
|
||||
</span>
|
||||
</:col>
|
||||
<:action :let={eq}>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{eq.id}"}
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
View
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
<% end %>
|
||||
</div>
|
||||
</Layouts.authenticated>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,40 @@
|
|||
<.header>
|
||||
{@page_title}
|
||||
<:actions>
|
||||
<.link navigate={~p"/orgs/new"} class="btn btn-primary">
|
||||
<.icon name="hero-plus" class="w-5 h-5" /> New Organization
|
||||
</.link>
|
||||
<.button navigate={~p"/orgs/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Organization
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<div class="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div :for={org <- @organizations} class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{org.name}</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
{Enum.find(org.memberships, &(&1.user_id == @current_scope.user.id)).role
|
||||
|> to_string()
|
||||
|> String.capitalize()}
|
||||
</p>
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<.link navigate={~p"/orgs/#{org.slug}"} class="btn btn-primary">
|
||||
Open
|
||||
</.link>
|
||||
</div>
|
||||
<div :if={@organizations != []} class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
:for={org <- @organizations}
|
||||
class="relative overflow-hidden rounded-lg border border-zinc-200 bg-white p-6 shadow-sm hover:shadow-md transition-shadow dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<h2 class="text-xl font-semibold text-zinc-900 dark:text-zinc-100">{org.name}</h2>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{Enum.find(org.memberships, &(&1.user_id == @current_scope.user.id)).role
|
||||
|> to_string()
|
||||
|> String.capitalize()}
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<.button navigate={~p"/orgs/#{org.slug}"} variant="primary">
|
||||
Open
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if @organizations == [] do %>
|
||||
<div class="text-center py-12">
|
||||
<.icon name="hero-building-office" class="mx-auto w-12 h-12 text-base-content/40" />
|
||||
<h3 class="mt-2 text-sm font-semibold text-base-content">No organizations</h3>
|
||||
<p class="mt-1 text-sm text-base-content/60">Get started by creating a new organization.</p>
|
||||
<div class="mt-6">
|
||||
<.link navigate={~p"/orgs/new"} class="btn btn-primary">
|
||||
<.icon name="hero-plus" class="w-5 h-5" /> New Organization
|
||||
</.link>
|
||||
</div>
|
||||
<div :if={@organizations == []} class="text-center py-16">
|
||||
<.icon name="hero-building-office" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
|
||||
<h3 class="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">No organizations</h3>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Get started by creating a new organization.
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<.button navigate={~p"/orgs/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Organization
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@
|
|||
<:subtitle>Create a new organization to manage your sites and equipment</:subtitle>
|
||||
</.header>
|
||||
|
||||
<div class="max-w-2xl mt-8">
|
||||
<div class="max-w-2xl">
|
||||
<.form for={@form} id="organization-form" phx-change="validate" phx-submit="save">
|
||||
<div class="space-y-4">
|
||||
<.input field={@form[:name]} type="text" label="Organization Name" required />
|
||||
<.input field={@form[:name]} type="text" label="Organization Name" required />
|
||||
|
||||
<div class="flex gap-2">
|
||||
<.button phx-disable-with="Creating...">Create Organization</.button>
|
||||
<.link navigate={~p"/orgs"} class="btn">Cancel</.link>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<.button phx-disable-with="Creating..." variant="primary">Create Organization</.button>
|
||||
<.button navigate={~p"/orgs"}>Cancel</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,56 +1,50 @@
|
|||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>Manage your site locations and hierarchy</:subtitle>
|
||||
<:actions>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites/new"}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<.icon name="hero-plus" class="w-5 h-5" /> New Site
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
<Layouts.authenticated flash={@flash} current_organization={@current_organization}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>Manage your site locations and hierarchy</:subtitle>
|
||||
<:actions>
|
||||
<.button navigate={~p"/orgs/#{@current_organization.slug}/sites/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Site
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<div class="mt-8">
|
||||
<%= if @sites == [] do %>
|
||||
<div class="text-center py-12">
|
||||
<.icon name="hero-building-office" class="mx-auto w-12 h-12 text-base-content/40" />
|
||||
<h3 class="mt-2 text-sm font-semibold text-base-content">No sites</h3>
|
||||
<p class="mt-1 text-sm text-base-content/60">Get started by creating your first site.</p>
|
||||
<div class="text-center py-16">
|
||||
<.icon name="hero-building-office" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
|
||||
<h3 class="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">No sites</h3>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Get started by creating your first site.
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites/new"}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<.icon name="hero-plus" class="w-5 h-5" /> New Site
|
||||
</.link>
|
||||
<.button navigate={~p"/orgs/#{@current_organization.slug}/sites/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Site
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div :for={site <- @sites} class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{site.name}</h3>
|
||||
<%= if site.location do %>
|
||||
<p class="text-sm text-base-content/60">
|
||||
<.icon name="hero-map-pin" class="w-4 h-4 inline" /> {site.location}
|
||||
</p>
|
||||
<% end %>
|
||||
<%= if site.parent_site do %>
|
||||
<p class="text-xs text-base-content/40">
|
||||
Parent: {site.parent_site.name}
|
||||
</p>
|
||||
<% end %>
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@current_organization.slug}/sites/#{site.id}"}
|
||||
class="btn btn-sm"
|
||||
>
|
||||
View
|
||||
</.link>
|
||||
</div>
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
:for={site <- @sites}
|
||||
class="relative overflow-hidden rounded-lg border border-zinc-200 bg-white p-6 shadow-sm hover:shadow-md transition-shadow dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<h3 class="text-xl font-semibold text-zinc-900 dark:text-zinc-100">{site.name}</h3>
|
||||
<%= if site.location do %>
|
||||
<p class="mt-2 flex items-center gap-1.5 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<.icon name="hero-map-pin" class="h-4 w-4" /> {site.location}
|
||||
</p>
|
||||
<% end %>
|
||||
<%= if site.parent_site do %>
|
||||
<p class="mt-2 text-xs text-zinc-500 dark:text-zinc-500">
|
||||
Parent: {site.parent_site.name}
|
||||
</p>
|
||||
<% end %>
|
||||
<div class="mt-6">
|
||||
<.button navigate={~p"/orgs/#{@current_organization.slug}/sites/#{site.id}"}>
|
||||
View
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</Layouts.authenticated>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue