refactor: remove daisyUI, use straight Tailwind v4 with custom color palette
- Remove daisyUI plugins and theme from app.css - Define 5 custom color palettes: sweet-salmon, desert-sand, wheat, cool-steel, cerulean - Add @utility card, badge, btn classes to replace daisyUI components - Replace all daisyUI classes across 16+ template files - Update tests for new class return values - Set light mode as default theme
This commit is contained in:
parent
785f077c50
commit
ebcd54be5b
18 changed files with 410 additions and 276 deletions
|
|
@ -6,92 +6,78 @@
|
|||
@source "../js";
|
||||
@source "../../lib/towerops_web";
|
||||
|
||||
/* A Tailwind plugin that makes "hero-#{ICON}" classes available.
|
||||
The heroicons installation itself is managed by your mix.exs */
|
||||
/* ── Custom color palette ── */
|
||||
@theme {
|
||||
--color-sweet-salmon-50: #ffe7e6;
|
||||
--color-sweet-salmon-100: #fed0cd;
|
||||
--color-sweet-salmon-200: #fea19a;
|
||||
--color-sweet-salmon-300: #fd7268;
|
||||
--color-sweet-salmon-400: #fd4235;
|
||||
--color-sweet-salmon-500: #fc1303;
|
||||
--color-sweet-salmon-600: #ca0f02;
|
||||
--color-sweet-salmon-700: #970c02;
|
||||
--color-sweet-salmon-800: #650801;
|
||||
--color-sweet-salmon-900: #320401;
|
||||
--color-sweet-salmon-950: #230300;
|
||||
|
||||
--color-desert-sand-50: #faf0eb;
|
||||
--color-desert-sand-100: #f5e2d6;
|
||||
--color-desert-sand-200: #ebc5ad;
|
||||
--color-desert-sand-300: #e0a885;
|
||||
--color-desert-sand-400: #d68b5c;
|
||||
--color-desert-sand-500: #cc6e33;
|
||||
--color-desert-sand-600: #a35829;
|
||||
--color-desert-sand-700: #7a421f;
|
||||
--color-desert-sand-800: #522c14;
|
||||
--color-desert-sand-900: #29160a;
|
||||
--color-desert-sand-950: #1d0f07;
|
||||
|
||||
--color-wheat-50: #faf4eb;
|
||||
--color-wheat-100: #f5e9d6;
|
||||
--color-wheat-200: #ebd3ad;
|
||||
--color-wheat-300: #e0bd85;
|
||||
--color-wheat-400: #d6a75c;
|
||||
--color-wheat-500: #cc9133;
|
||||
--color-wheat-600: #a37429;
|
||||
--color-wheat-700: #7a571f;
|
||||
--color-wheat-800: #523a14;
|
||||
--color-wheat-900: #291d0a;
|
||||
--color-wheat-950: #1d1407;
|
||||
|
||||
--color-cool-steel-50: #f0f3f4;
|
||||
--color-cool-steel-100: #e1e7ea;
|
||||
--color-cool-steel-200: #c4cfd4;
|
||||
--color-cool-steel-300: #a6b7bf;
|
||||
--color-cool-steel-400: #89a0a9;
|
||||
--color-cool-steel-500: #6b8894;
|
||||
--color-cool-steel-600: #566d76;
|
||||
--color-cool-steel-700: #405159;
|
||||
--color-cool-steel-800: #2b363b;
|
||||
--color-cool-steel-900: #151b1e;
|
||||
--color-cool-steel-950: #0f1315;
|
||||
|
||||
--color-cerulean-50: #edf4f8;
|
||||
--color-cerulean-100: #dbe9f0;
|
||||
--color-cerulean-200: #b6d3e2;
|
||||
--color-cerulean-300: #92bcd3;
|
||||
--color-cerulean-400: #6da6c5;
|
||||
--color-cerulean-500: #4990b6;
|
||||
--color-cerulean-600: #3a7392;
|
||||
--color-cerulean-700: #2c566d;
|
||||
--color-cerulean-800: #1d3a49;
|
||||
--color-cerulean-900: #0f1d24;
|
||||
--color-cerulean-950: #0a141a;
|
||||
|
||||
/* Design tokens (replaces daisyUI semantic variables) */
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.25rem;
|
||||
--radius-box: 0.5rem;
|
||||
}
|
||||
|
||||
/* Heroicons plugin */
|
||||
@plugin "../vendor/heroicons";
|
||||
|
||||
/* daisyUI Tailwind Plugin. You can update this file by fetching the latest version with:
|
||||
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
|
||||
Make sure to look at the daisyUI changelog: https://daisyui.com/docs/changelog/ */
|
||||
@plugin "../vendor/daisyui" {
|
||||
themes: false;
|
||||
}
|
||||
|
||||
/* daisyUI theme plugin. You can update this file by fetching the latest version with:
|
||||
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js
|
||||
We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
|
||||
on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
|
||||
@plugin "../vendor/daisyui-theme" {
|
||||
name: "dark";
|
||||
default: false;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--color-base-100: oklch(30.33% 0.016 252.42);
|
||||
--color-base-200: oklch(25.26% 0.014 253.1);
|
||||
--color-base-300: oklch(20.15% 0.012 254.09);
|
||||
--color-base-content: oklch(97.807% 0.029 256.847);
|
||||
--color-primary: oklch(58% 0.233 277.117);
|
||||
--color-primary-content: oklch(96% 0.018 272.314);
|
||||
--color-secondary: oklch(58% 0.233 277.117);
|
||||
--color-secondary-content: oklch(96% 0.018 272.314);
|
||||
--color-accent: oklch(60% 0.25 292.717);
|
||||
--color-accent-content: oklch(96% 0.016 293.756);
|
||||
--color-neutral: oklch(37% 0.044 257.287);
|
||||
--color-neutral-content: oklch(98% 0.003 247.858);
|
||||
--color-info: oklch(58% 0.158 241.966);
|
||||
--color-info-content: oklch(97% 0.013 236.62);
|
||||
--color-success: oklch(60% 0.118 184.704);
|
||||
--color-success-content: oklch(98% 0.014 180.72);
|
||||
--color-warning: oklch(66% 0.179 58.318);
|
||||
--color-warning-content: oklch(98% 0.022 95.277);
|
||||
--color-error: oklch(58% 0.253 17.585);
|
||||
--color-error-content: oklch(96% 0.015 12.422);
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.25rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.21875rem;
|
||||
--size-field: 0.21875rem;
|
||||
--border: 1.5px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
@plugin "../vendor/daisyui-theme" {
|
||||
name: "light";
|
||||
default: true;
|
||||
prefersdark: false;
|
||||
color-scheme: "light";
|
||||
--color-base-100: oklch(98% 0 0);
|
||||
--color-base-200: oklch(96% 0.001 286.375);
|
||||
--color-base-300: oklch(92% 0.004 286.32);
|
||||
--color-base-content: oklch(21% 0.006 285.885);
|
||||
--color-primary: oklch(70% 0.213 47.604);
|
||||
--color-primary-content: oklch(98% 0.016 73.684);
|
||||
--color-secondary: oklch(55% 0.027 264.364);
|
||||
--color-secondary-content: oklch(98% 0.002 247.839);
|
||||
--color-accent: oklch(0% 0 0);
|
||||
--color-accent-content: oklch(100% 0 0);
|
||||
--color-neutral: oklch(44% 0.017 285.786);
|
||||
--color-neutral-content: oklch(98% 0 0);
|
||||
--color-info: oklch(62% 0.214 259.815);
|
||||
--color-info-content: oklch(97% 0.014 254.604);
|
||||
--color-success: oklch(70% 0.14 182.503);
|
||||
--color-success-content: oklch(98% 0.014 180.72);
|
||||
--color-warning: oklch(66% 0.179 58.318);
|
||||
--color-warning-content: oklch(98% 0.022 95.277);
|
||||
--color-error: oklch(58% 0.253 17.585);
|
||||
--color-error-content: oklch(96% 0.015 12.422);
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.25rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.21875rem;
|
||||
--size-field: 0.21875rem;
|
||||
--border: 1.5px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
/* Add variants based on LiveView classes */
|
||||
/* ── Custom variants ── */
|
||||
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
|
||||
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
|
||||
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
|
||||
|
|
@ -99,6 +85,56 @@
|
|||
/* Use the data attribute for dark mode */
|
||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||
|
||||
/* ── Reusable utility classes (replaces daisyUI component classes) ── */
|
||||
|
||||
/* Card — replaces daisyUI `card card-body bg-base-100 border-base-200` */
|
||||
@utility card {
|
||||
border-radius: var(--radius-box);
|
||||
border: 1px solid var(--color-cool-steel-200);
|
||||
background: var(--color-cool-steel-50);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
[data-theme="dark"] .card {
|
||||
border-color: var(--color-cool-steel-700);
|
||||
background: var(--color-cool-steel-900);
|
||||
}
|
||||
|
||||
/* Badge — replaces daisyUI `badge` */
|
||||
@utility badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 9999px;
|
||||
padding-inline: 0.625rem;
|
||||
padding-block: 0.125rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
/* Button base — replaces daisyUI `btn` (for standalone usage outside core_components) */
|
||||
@utility btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: var(--radius-field);
|
||||
padding-inline: 1rem;
|
||||
padding-block: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.25rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
background: var(--color-cool-steel-200);
|
||||
color: var(--color-cool-steel-900);
|
||||
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
[data-theme="dark"] .btn {
|
||||
background: var(--color-cool-steel-700);
|
||||
color: var(--color-cool-steel-50);
|
||||
}
|
||||
|
||||
/* ── Base element styles (replaces daisyUI resets) ── */
|
||||
|
||||
/* Make LiveView wrapper divs transparent for layout */
|
||||
[data-phx-session], [data-phx-teleported-src] { display: contents }
|
||||
|
||||
|
|
@ -107,10 +143,11 @@
|
|||
|
||||
/* Sidebar layout */
|
||||
#app-sidebar {
|
||||
@apply w-56 transition-all duration-200;
|
||||
width: 14rem;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
html.sidebar-collapsed #app-sidebar {
|
||||
@apply w-16;
|
||||
width: 4rem;
|
||||
}
|
||||
html.sidebar-collapsed [data-sidebar-text] {
|
||||
display: none;
|
||||
|
|
@ -144,10 +181,10 @@ html.sidebar-collapsed [data-sidebar-tooltip]:hover::after {
|
|||
pointer-events: none;
|
||||
}
|
||||
#main-wrapper {
|
||||
@apply transition-all duration-200;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
/* This file is for your main application CSS */
|
||||
/* ── Animations ── */
|
||||
|
||||
/* Page transitions */
|
||||
@keyframes fadeIn {
|
||||
|
|
@ -157,7 +194,7 @@ html.sidebar-collapsed [data-sidebar-tooltip]:hover::after {
|
|||
.page-fade-in { animation: fadeIn 0.15s ease-out; }
|
||||
|
||||
/* Smooth transitions on interactive elements */
|
||||
.card, .btn, .badge, .alert {
|
||||
.card, .btn, .badge {
|
||||
transition: box-shadow 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
|
|
@ -179,40 +216,6 @@ html.sidebar-collapsed [data-sidebar-tooltip]:hover::after {
|
|||
}
|
||||
.status-pulse { animation: statusPulse 2s ease-in-out infinite; }
|
||||
|
||||
/* Scrollbar styling for webkit */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.15); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.25); }
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); }
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.25); }
|
||||
|
||||
/* Table row hover highlight */
|
||||
tbody tr:hover td { background-color: rgba(59, 130, 246, 0.04); }
|
||||
[data-theme="dark"] tbody tr:hover td { background-color: rgba(59, 130, 246, 0.08); }
|
||||
|
||||
/* Focus ring styling */
|
||||
:focus-visible {
|
||||
outline: 2px solid oklch(58% 0.233 277.117);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Global smooth transitions for interactive elements */
|
||||
a, button, input, select, textarea {
|
||||
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
/* Dark mode body transition */
|
||||
body, .bg-gray-50, .bg-white, .dark\:bg-gray-950, .dark\:bg-gray-900 {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Password strength indicator (JS-driven via classes) */
|
||||
.password-strength-bar.strength-weak { background-color: #ef4444; }
|
||||
.password-strength-bar.strength-fair { background-color: #f59e0b; }
|
||||
.password-strength-bar.strength-good { background-color: #22c55e; }
|
||||
.password-strength-bar.strength-strong { background-color: #16a34a; }
|
||||
|
||||
/* Skeleton shimmer effect */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
|
|
@ -223,3 +226,34 @@ body, .bg-gray-50, .bg-white, .dark\:bg-gray-950, .dark\:bg-gray-900 {
|
|||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ── Global element styles ── */
|
||||
|
||||
/* Scrollbar styling for webkit */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.15); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.25); }
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); }
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.25); }
|
||||
|
||||
/* Table row hover highlight */
|
||||
tbody tr:hover td { background-color: rgba(73, 144, 182, 0.06); }
|
||||
[data-theme="dark"] tbody tr:hover td { background-color: rgba(73, 144, 182, 0.12); }
|
||||
|
||||
/* Focus ring styling */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-cerulean-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Global smooth transitions for interactive elements */
|
||||
a, button, input, select, textarea {
|
||||
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
/* Password strength indicator (JS-driven via classes) */
|
||||
.password-strength-bar.strength-weak { background-color: var(--color-sweet-salmon-500); }
|
||||
.password-strength-bar.strength-fair { background-color: var(--color-wheat-500); }
|
||||
.password-strength-bar.strength-good { background-color: #22c55e; }
|
||||
.password-strength-bar.strength-strong { background-color: #16a34a; }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule ToweropsWeb.Components.Breadcrumbs do
|
||||
@moduledoc """
|
||||
Reusable breadcrumb navigation component using DaisyUI.
|
||||
Reusable breadcrumb navigation component.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ defmodule ToweropsWeb.Components.Breadcrumbs do
|
|||
|
||||
def breadcrumb(assigns) do
|
||||
~H"""
|
||||
<div class="breadcrumbs text-sm mb-4">
|
||||
<div class="flex items-center gap-2 text-sm mb-4 text-cool-steel-600 dark:text-cool-steel-400">
|
||||
<ul>
|
||||
<%= for {item, idx} <- Enum.with_index(@items) do %>
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
</.button>
|
||||
</.form>
|
||||
|
||||
<div class="divider" />
|
||||
<div class="my-4 border-t border-cool-steel-200 dark:border-cool-steel-700" />
|
||||
|
||||
<.form :let={f} for={@password_changeset} action={~p"/users/settings"} id="update_password">
|
||||
<input type="hidden" name="action" value="update_password" />
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
</.button>
|
||||
</.form>
|
||||
|
||||
<div class="divider" />
|
||||
<div class="my-4 border-t border-cool-steel-200 dark:border-cool-steel-700" />
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
value={@search}
|
||||
placeholder={t("Search by device name, IP, site, or keyword...")}
|
||||
phx-debounce="300"
|
||||
class="input input-bordered w-full pl-10 pr-10 bg-white dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:placeholder-gray-500"
|
||||
class="w-full pl-10 pr-10 rounded-field border border-cool-steel-300 bg-cool-steel-50 px-3 py-2 text-sm text-cool-steel-900 placeholder:text-cool-steel-400 focus:border-cerulean-500 focus:ring-1 focus:ring-cerulean-500 dark:border-cool-steel-600 dark:bg-cool-steel-900 dark:text-cool-steel-100 dark:placeholder:text-cool-steel-500"
|
||||
/>
|
||||
<button
|
||||
:if={@search != ""}
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
<button
|
||||
type="button"
|
||||
phx-click="clear_search"
|
||||
class="btn btn-sm btn-outline gap-2"
|
||||
class="btn text-sm !py-1 !px-3 border border-cool-steel-300 bg-transparent dark:border-cool-steel-600 gap-2"
|
||||
>
|
||||
<.icon name="hero-x-mark" class="h-4 w-4" /> Clear search
|
||||
</button>
|
||||
|
|
@ -285,7 +285,7 @@
|
|||
<button
|
||||
type="button"
|
||||
phx-click="load_more"
|
||||
class="btn btn-outline btn-sm gap-2"
|
||||
class="btn text-sm !py-1 !px-3 border border-cool-steel-300 bg-transparent dark:border-cool-steel-600 gap-2"
|
||||
>
|
||||
<.icon name="hero-arrow-down" class="h-4 w-4" /> Load more events
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -19,20 +19,25 @@ defmodule ToweropsWeb.ChangelogLive do
|
|||
>
|
||||
<div class="mx-auto max-w-3xl px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-2">What's New</h1>
|
||||
<p class="text-base-content/60 mb-8">Latest updates and improvements to TowerOps.</p>
|
||||
<p class="text-cool-steel-900/60 dark:text-cool-steel-100/60 mb-8">
|
||||
Latest updates and improvements to TowerOps.
|
||||
</p>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div :for={entry <- @entries} class="relative pl-8 border-l-2 border-primary/20">
|
||||
<div class="absolute -left-[9px] top-1 w-4 h-4 rounded-full bg-primary border-2 border-base-100" />
|
||||
<div
|
||||
:for={entry <- @entries}
|
||||
class="relative pl-8 border-l-2 border-sweet-salmon-500/20 dark:border-sweet-salmon-600/20"
|
||||
>
|
||||
<div class="absolute -left-[9px] top-1 w-4 h-4 rounded-full bg-sweet-salmon-500 dark:bg-sweet-salmon-600 border-2 border-cool-steel-50 dark:border-cool-steel-900" />
|
||||
<div class="mb-1">
|
||||
<span class="badge badge-ghost badge-sm font-mono">
|
||||
<span class="badge text-cool-steel-700 dark:text-cool-steel-300 font-mono">
|
||||
{Calendar.strftime(entry.date, "%b %d, %Y")}
|
||||
</span>
|
||||
</div>
|
||||
<h2 :if={entry.title} class="text-lg font-semibold mb-2">{entry.title}</h2>
|
||||
<ul
|
||||
:if={entry.items != []}
|
||||
class="list-disc list-inside space-y-1 text-base-content/80 text-sm"
|
||||
class="list-disc list-inside space-y-1 text-cool-steel-900/80 dark:text-cool-steel-100/80 text-sm"
|
||||
>
|
||||
<li :for={item <- entry.items}>{item}</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -89,17 +89,20 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Config Timeline</h1>
|
||||
<p class="text-base-content/60">
|
||||
<.link navigate={~p"/devices/#{@device.id}?tab=overview"} class="link link-hover">
|
||||
<p class="text-cool-steel-900/60 dark:text-cool-steel-100/60">
|
||||
<.link
|
||||
navigate={~p"/devices/#{@device.id}?tab=overview"}
|
||||
class="text-cerulean-600 hover:text-cerulean-700 hover:underline dark:text-cerulean-400 dark:hover:text-cerulean-300"
|
||||
>
|
||||
{@device.name}
|
||||
</.link>
|
||||
— Config changes vs performance metrics
|
||||
</p>
|
||||
</div>
|
||||
<div class="join">
|
||||
<div class="inline-flex">
|
||||
<button
|
||||
:for={r <- ["24h", "7d", "30d"]}
|
||||
class={"join-item btn btn-sm #{if @range == r, do: "btn-primary", else: "btn-ghost"}"}
|
||||
class={"btn text-sm !py-1 !px-3 #{if @range == r, do: "bg-sweet-salmon-500 text-white hover:bg-sweet-salmon-600 dark:bg-sweet-salmon-600 dark:hover:bg-sweet-salmon-700", else: "bg-transparent hover:bg-cool-steel-200 dark:hover:bg-cool-steel-700"}"}
|
||||
phx-click="select_range"
|
||||
phx-value-range={r}
|
||||
>
|
||||
|
|
@ -109,9 +112,9 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
</div>
|
||||
|
||||
<%!-- Timeline chart area --%>
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-lg">Performance & Config Changes</h2>
|
||||
<div class="card shadow-sm">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h2 class="text-lg font-semibold">Performance & Config Changes</h2>
|
||||
|
||||
<%!-- QoE metrics chart placeholder — rendered via hook --%>
|
||||
<div
|
||||
|
|
@ -122,7 +125,7 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
data-checks={Jason.encode!(@check_data)}
|
||||
class="h-64 w-full"
|
||||
>
|
||||
<div class="flex items-center justify-center h-full text-base-content/40">
|
||||
<div class="flex items-center justify-center h-full text-cool-steel-900/40 dark:text-cool-steel-100/40">
|
||||
Loading chart...
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -130,15 +133,17 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
</div>
|
||||
|
||||
<%!-- Change events list --%>
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-lg">Config Changes ({length(@change_events)})</h2>
|
||||
<div class="card shadow-sm">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h2 class="text-lg font-semibold">Config Changes ({length(@change_events)})</h2>
|
||||
|
||||
<%= if Enum.empty?(@change_events) do %>
|
||||
<p class="text-base-content/50 py-4">No config changes in this period.</p>
|
||||
<p class="text-cool-steel-900/50 dark:text-cool-steel-100/50 py-4">
|
||||
No config changes in this period.
|
||||
</p>
|
||||
<% else %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>When</th>
|
||||
|
|
@ -150,7 +155,7 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
<tbody>
|
||||
<tr
|
||||
:for={event <- @change_events}
|
||||
class="hover:bg-base-200/50 cursor-pointer"
|
||||
class="hover:bg-cool-steel-100/50 dark:hover:bg-cool-steel-800/50 cursor-pointer"
|
||||
phx-click="select_event"
|
||||
phx-value-id={event.id}
|
||||
>
|
||||
|
|
@ -160,13 +165,13 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
<td>
|
||||
<span
|
||||
:for={section <- event.sections_changed}
|
||||
class="badge badge-sm badge-ghost mr-1"
|
||||
class="badge text-cool-steel-700 dark:text-cool-steel-300 mr-1"
|
||||
>
|
||||
{section}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class={"badge badge-sm #{change_size_class(event.change_size)}"}>
|
||||
<span class={"badge bg-cool-steel-200 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300 #{change_size_class(event.change_size)}"}>
|
||||
{event.change_size} lines
|
||||
</span>
|
||||
</td>
|
||||
|
|
@ -175,7 +180,7 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
navigate={
|
||||
~p"/devices/#{@device.id}/backups/compare?before=#{event.backup_before_id}&after=#{event.backup_after_id}"
|
||||
}
|
||||
class="btn btn-xs btn-ghost"
|
||||
class="btn text-xs !py-0.5 !px-2 bg-transparent hover:bg-cool-steel-200 dark:hover:bg-cool-steel-700"
|
||||
>
|
||||
View Diff
|
||||
</.link>
|
||||
|
|
@ -190,35 +195,44 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
|
||||
<%!-- Selected event detail modal --%>
|
||||
<%= if @selected_event do %>
|
||||
<div class="card bg-base-100 shadow-sm border border-primary/20">
|
||||
<div class="card-body">
|
||||
<div class="card shadow-sm border border-sweet-salmon-500/20 dark:border-sweet-salmon-600/20">
|
||||
<div class="p-4 sm:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-lg">
|
||||
<h2 class="text-lg font-semibold">
|
||||
Change Details — {Calendar.strftime(@selected_event.changed_at, "%b %d, %H:%M UTC")}
|
||||
</h2>
|
||||
<button class="btn btn-sm btn-ghost" phx-click="close_event">✕</button>
|
||||
<button
|
||||
class="btn text-sm !py-1 !px-3 bg-transparent hover:bg-cool-steel-200 dark:hover:bg-cool-steel-700"
|
||||
phx-click="close_event"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
|
||||
<div>
|
||||
<span class="text-sm font-medium text-base-content/60">Sections Changed</span>
|
||||
<span class="text-sm font-medium text-cool-steel-900/60 dark:text-cool-steel-100/60">
|
||||
Sections Changed
|
||||
</span>
|
||||
<div class="mt-1">
|
||||
<span
|
||||
:for={s <- @selected_event.sections_changed}
|
||||
class="badge badge-sm badge-outline mr-1"
|
||||
class="badge border border-cool-steel-300 bg-transparent dark:border-cool-steel-600 mr-1"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
<span
|
||||
:if={Enum.empty?(@selected_event.sections_changed)}
|
||||
class="text-base-content/40"
|
||||
class="text-cool-steel-900/40 dark:text-cool-steel-100/40"
|
||||
>
|
||||
Unknown
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-base-content/60">Lines Changed</span>
|
||||
<span class="text-sm font-medium text-cool-steel-900/60 dark:text-cool-steel-100/60">
|
||||
Lines Changed
|
||||
</span>
|
||||
<p class="font-mono">{@selected_event.change_size}</p>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -226,7 +240,7 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
navigate={
|
||||
~p"/devices/#{@device.id}/backups/compare?before=#{@selected_event.backup_before_id}&after=#{@selected_event.backup_after_id}"
|
||||
}
|
||||
class="btn btn-sm btn-primary"
|
||||
class="btn text-sm !py-1 !px-3 bg-sweet-salmon-500 text-white hover:bg-sweet-salmon-600 dark:bg-sweet-salmon-600 dark:hover:bg-sweet-salmon-700"
|
||||
>
|
||||
Full Diff View →
|
||||
</.link>
|
||||
|
|
@ -235,8 +249,10 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
|
||||
<%= if @selected_event.diff_summary do %>
|
||||
<div class="mt-4">
|
||||
<span class="text-sm font-medium text-base-content/60">Diff Summary</span>
|
||||
<pre class="bg-base-200 p-3 rounded-lg mt-1 text-xs overflow-x-auto max-h-48 overflow-y-auto"><code>{@selected_event.diff_summary}</code></pre>
|
||||
<span class="text-sm font-medium text-cool-steel-900/60 dark:text-cool-steel-100/60">
|
||||
Diff Summary
|
||||
</span>
|
||||
<pre class="bg-cool-steel-100 dark:bg-cool-steel-800 p-3 rounded-lg mt-1 text-xs overflow-x-auto max-h-48 overflow-y-auto"><code>{@selected_event.diff_summary}</code></pre>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -267,7 +283,11 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
end
|
||||
|
||||
@doc false
|
||||
def change_size_class(size) when is_integer(size) and size > 50, do: "badge-error"
|
||||
def change_size_class(size) when is_integer(size) and size > 20, do: "badge-warning"
|
||||
def change_size_class(_), do: "badge-info"
|
||||
def change_size_class(size) when is_integer(size) and size > 50,
|
||||
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
|
||||
|
||||
def change_size_class(size) when is_integer(size) and size > 20,
|
||||
do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
|
||||
def change_size_class(_), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -231,7 +231,10 @@
|
|||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<.link navigate={~p"/coverage"} class="btn btn-outline">
|
||||
<.link
|
||||
navigate={~p"/coverage"}
|
||||
class="btn border border-cool-steel-300 bg-transparent dark:border-cool-steel-600"
|
||||
>
|
||||
{t("Cancel")}
|
||||
</.link>
|
||||
<.button type="submit" variant="primary">
|
||||
|
|
|
|||
|
|
@ -21,18 +21,20 @@
|
|||
<%= cond do %>
|
||||
<% @sites == [] -> %>
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div class="card bg-base-100 shadow-md border border-base-200 dark:border-white/10 max-w-md w-full">
|
||||
<div class="card-body items-center text-center">
|
||||
<div class="card shadow-md border dark:border-white/10 max-w-md w-full">
|
||||
<div class="items-center text-center">
|
||||
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
|
||||
<.icon name="hero-signal" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
<h3 class="card-title text-gray-900 dark:text-white">{t("Add a site first")}</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t("Add a site first")}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t(
|
||||
"Coverage predictions are computed from a site's location. Add a site, then return here to plan its coverage."
|
||||
)}
|
||||
</p>
|
||||
<div class="card-actions mt-2">
|
||||
<div class="mt-4 flex gap-2">
|
||||
<.button navigate={~p"/sites/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> {t("Add Site")}
|
||||
</.button>
|
||||
|
|
@ -42,16 +44,18 @@
|
|||
</div>
|
||||
<% @coverages == [] -> %>
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div class="card bg-base-100 shadow-md border border-base-200 dark:border-white/10 max-w-md w-full">
|
||||
<div class="card-body items-center text-center">
|
||||
<div class="card shadow-md border dark:border-white/10 max-w-md w-full">
|
||||
<div class="items-center text-center">
|
||||
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
|
||||
<.icon name="hero-signal" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
<h3 class="card-title text-gray-900 dark:text-white">{t("No coverages yet")}</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t("No coverages yet")}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t("Create your first coverage to predict where a tower's antenna reaches.")}
|
||||
</p>
|
||||
<div class="card-actions mt-2">
|
||||
<div class="mt-4 flex gap-2">
|
||||
<.button navigate={~p"/coverage/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> {t("New Coverage")}
|
||||
</.button>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@
|
|||
{t("All ready coverages overlaid. Click any point to query each tower's predicted RSSI.")}
|
||||
</:subtitle>
|
||||
<:actions>
|
||||
<.link navigate={~p"/coverage/list"} class="btn btn-outline btn-sm gap-1.5">
|
||||
<.link
|
||||
navigate={~p"/coverage/list"}
|
||||
class="btn text-sm !py-1 !px-3 border border-cool-steel-300 bg-transparent dark:border-cool-steel-600 gap-1.5"
|
||||
>
|
||||
<.icon name="hero-list-bullet" class="h-4 w-4" /> {t("List view")}
|
||||
</.link>
|
||||
<.button navigate={~p"/coverage/new"} variant="primary">
|
||||
|
|
@ -21,18 +24,20 @@
|
|||
|
||||
<%= if @coverages == [] do %>
|
||||
<div class="mt-6 flex items-center justify-center py-16">
|
||||
<div class="card bg-base-100 shadow-md border border-base-200 dark:border-white/10 max-w-md w-full">
|
||||
<div class="card-body items-center text-center">
|
||||
<div class="card shadow-md border dark:border-white/10 max-w-md w-full">
|
||||
<div class="items-center text-center">
|
||||
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
|
||||
<.icon name="hero-signal" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
<h3 class="card-title text-gray-900 dark:text-white">{t("No ready coverages")}</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t("No ready coverages")}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t(
|
||||
"Coverage maps appear here once the background compute finishes. Create or wait for a coverage to finish."
|
||||
)}
|
||||
</p>
|
||||
<div class="card-actions mt-2">
|
||||
<div class="mt-4 flex gap-2">
|
||||
<.button navigate={~p"/coverage/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> {t("New Coverage")}
|
||||
</.button>
|
||||
|
|
|
|||
|
|
@ -33,13 +33,16 @@
|
|||
<button
|
||||
type="button"
|
||||
phx-click="toggle_params"
|
||||
class="btn btn-outline btn-sm gap-1.5"
|
||||
class="btn text-sm !py-1 !px-3 border border-cool-steel-300 bg-transparent dark:border-cool-steel-600 gap-1.5"
|
||||
title={t("Toggle parameters panel")}
|
||||
>
|
||||
<.icon name="hero-adjustments-horizontal" class="h-4 w-4" />
|
||||
{if @show_params, do: t("Hide details"), else: t("Details")}
|
||||
</button>
|
||||
<.link navigate={~p"/coverage/#{@coverage.id}/edit"} class="btn btn-outline btn-sm gap-1.5">
|
||||
<.link
|
||||
navigate={~p"/coverage/#{@coverage.id}/edit"}
|
||||
class="btn text-sm !py-1 !px-3 border border-cool-steel-300 bg-transparent dark:border-cool-steel-600 gap-1.5"
|
||||
>
|
||||
<.icon name="hero-pencil-square" class="h-4 w-4" /> {t("Edit")}
|
||||
</.link>
|
||||
<.button phx-click="recompute" variant="primary">
|
||||
|
|
|
|||
|
|
@ -770,7 +770,7 @@
|
|||
</span>
|
||||
<span
|
||||
:for={s <- Enum.take(event.sections_changed, 3)}
|
||||
class="badge badge-xs badge-ghost"
|
||||
class="badge text-xs !px-2 !py-0 text-cool-steel-700 dark:text-cool-steel-300"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -178,15 +178,17 @@
|
|||
<div class="mt-6" id="insights-list">
|
||||
<%= if @insights == [] do %>
|
||||
<div id="empty-state" class="flex items-center justify-center py-8">
|
||||
<div class="card bg-base-100 shadow-md border border-base-200 dark:border-white/10 max-w-md w-full">
|
||||
<div class="card-body items-center text-center">
|
||||
<div class="card shadow-md border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 max-w-md w-full">
|
||||
<div class="items-center text-center">
|
||||
<div class="rounded-full bg-amber-100 dark:bg-amber-900/40 p-4 mb-2">
|
||||
<.icon
|
||||
name="hero-light-bulb-solid"
|
||||
class="h-12 w-12 text-amber-500 dark:text-amber-400"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="card-title text-gray-900 dark:text-white">{t("No Insights Yet")}</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t("No Insights Yet")}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<%= cond do %>
|
||||
<% @filter_status == "dismissed" -> %>
|
||||
|
|
@ -199,8 +201,8 @@
|
|||
{t("Connect Preseem or enable SNMP monitoring to get started.")}
|
||||
<% end %>
|
||||
</p>
|
||||
<div class="card-actions mt-2">
|
||||
<span class="text-sm text-base-content/50">
|
||||
<div class="mt-4 flex gap-2 mt-2">
|
||||
<span class="text-sm text-cool-steel-900/50 dark:text-cool-steel-100/50">
|
||||
{t("Go to your org settings to connect an integration.")}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1096,11 +1096,17 @@
|
|||
|
||||
<div class="mt-6 flex gap-3">
|
||||
<%= if @organization.subscription_plan == "free" do %>
|
||||
<.button phx-click="upgrade_to_paid" class="btn-primary">
|
||||
<.button
|
||||
phx-click="upgrade_to_paid"
|
||||
class="bg-sweet-salmon-500 text-white hover:bg-sweet-salmon-600 dark:bg-sweet-salmon-600 dark:hover:bg-sweet-salmon-700"
|
||||
>
|
||||
{t("Upgrade to Paid Plan")}
|
||||
</.button>
|
||||
<% else %>
|
||||
<.button phx-click="manage_billing" class="btn-secondary">
|
||||
<.button
|
||||
phx-click="manage_billing"
|
||||
class="bg-cerulean-500 text-white hover:bg-cerulean-600 dark:bg-cerulean-600 dark:hover:bg-cerulean-700"
|
||||
>
|
||||
{t("Manage Billing")}
|
||||
</.button>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@
|
|||
{@page_title}
|
||||
<:subtitle>{t("Manage your site locations and hierarchy")}</:subtitle>
|
||||
<:actions>
|
||||
<.link navigate={~p"/sites-map"} class="btn btn-outline btn-sm gap-1.5 mr-2">
|
||||
<.link
|
||||
navigate={~p"/sites-map"}
|
||||
class="btn text-sm !py-1 !px-3 border border-cool-steel-300 bg-transparent dark:border-cool-steel-600 gap-1.5 mr-2"
|
||||
>
|
||||
<.icon name="hero-map" class="h-4 w-4" /> Map View
|
||||
</.link>
|
||||
<.button navigate={~p"/sites/new"} variant="primary">
|
||||
|
|
@ -19,16 +22,16 @@
|
|||
|
||||
<%= if @sites == [] do %>
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div class="card bg-base-100 shadow-md border border-base-200 dark:border-white/10 max-w-md w-full">
|
||||
<div class="card-body items-center text-center">
|
||||
<div class="card shadow-md border dark:border-white/10 max-w-md w-full">
|
||||
<div class="items-center text-center">
|
||||
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
|
||||
<.icon name="hero-map-pin-solid" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
<h3 class="card-title text-gray-900 dark:text-white">{t("No Sites Yet")}</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{t("No Sites Yet")}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t("Create your first site to organize devices by tower location.")}
|
||||
</p>
|
||||
<div class="card-actions mt-2">
|
||||
<div class="mt-4 flex gap-2">
|
||||
<.button navigate={~p"/sites/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> Add Site
|
||||
</.button>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
<.button
|
||||
type="button"
|
||||
phx-click="force_rediscover_all"
|
||||
class="btn-sm"
|
||||
class="btn text-sm !py-1 !px-3"
|
||||
data-confirm={
|
||||
t("This will trigger SNMP discovery for all devices at this site. Continue?")
|
||||
}
|
||||
|
|
@ -54,18 +54,22 @@
|
|||
|
||||
<%!-- Quick Actions --%>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<.button navigate={~p"/devices/new?site_id=#{@site.id}"} variant="primary" class="btn-sm">
|
||||
<.button
|
||||
navigate={~p"/devices/new?site_id=#{@site.id}"}
|
||||
variant="primary"
|
||||
class="btn text-sm !py-1 !px-3"
|
||||
>
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> {t("Add Device")}
|
||||
</.button>
|
||||
<%= if @site.latitude && @site.longitude do %>
|
||||
<.button
|
||||
navigate={~p"/sites-map?lat=#{@site.latitude}&lng=#{@site.longitude}&zoom=15"}
|
||||
class="btn-sm"
|
||||
class="btn text-sm !py-1 !px-3"
|
||||
>
|
||||
<.icon name="hero-map" class="h-4 w-4" /> {t("View on Map")}
|
||||
</.button>
|
||||
<% end %>
|
||||
<.button navigate={~p"/sites/#{@site.id}/edit"} class="btn-sm">
|
||||
<.button navigate={~p"/sites/#{@site.id}/edit"} class="btn text-sm !py-1 !px-3">
|
||||
<.icon name="hero-pencil" class="h-4 w-4" /> {t("Edit Site")}
|
||||
</.button>
|
||||
</div>
|
||||
|
|
@ -286,7 +290,7 @@
|
|||
<dd class="mt-0.5">
|
||||
<.link
|
||||
navigate={~p"/sites/#{@site.parent_site.id}"}
|
||||
class="link link-primary text-sm"
|
||||
class="text-sm text-cerulean-600 hover:text-cerulean-700 hover:underline dark:text-cerulean-400 dark:hover:text-cerulean-300"
|
||||
>
|
||||
{@site.parent_site.name}
|
||||
</.link>
|
||||
|
|
@ -307,13 +311,16 @@
|
|||
<% end %>
|
||||
</dl>
|
||||
<%= if @site.child_sites != [] do %>
|
||||
<div class="mt-4 border-t border-base-300 pt-3">
|
||||
<div class="mt-4 border-t border-cool-steel-300 dark:border-cool-steel-600 pt-3">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{t("Child Sites")}
|
||||
</h4>
|
||||
<ul class="mt-2 space-y-1">
|
||||
<li :for={child <- @site.child_sites}>
|
||||
<.link navigate={~p"/sites/#{child.id}"} class="link link-primary text-sm">
|
||||
<.link
|
||||
navigate={~p"/sites/#{child.id}"}
|
||||
class="text-sm text-cerulean-600 hover:text-cerulean-700 hover:underline dark:text-cerulean-400 dark:hover:text-cerulean-300"
|
||||
>
|
||||
{child.name}
|
||||
</.link>
|
||||
</li>
|
||||
|
|
@ -388,7 +395,10 @@
|
|||
{time_ago(change.detected_at)}
|
||||
</p>
|
||||
</div>
|
||||
<span class={["badge badge-sm", change_size_color(change.diff_size || 0)]}>
|
||||
<span class={[
|
||||
"badge bg-cool-steel-200 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300",
|
||||
change_size_color(change.diff_size || 0)
|
||||
]}>
|
||||
{change_size_label(change.diff_size || 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -410,7 +420,7 @@
|
|||
<.button
|
||||
navigate={~p"/devices/new?site_id=#{@site.id}"}
|
||||
variant="primary"
|
||||
class="btn-sm"
|
||||
class="btn text-sm !py-1 !px-3"
|
||||
>
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> Add
|
||||
</.button>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ defmodule ToweropsWeb.TraceLive.Index do
|
|||
assigns = assign(assigns, :issues, Enum.reverse(issues))
|
||||
|
||||
~H"""
|
||||
<div :if={@issues != []} class="alert alert-warning shadow-sm">
|
||||
<div
|
||||
:if={@issues != []}
|
||||
class="border-l-4 border-wheat-500 bg-wheat-50 p-4 text-wheat-900 shadow-sm dark:bg-wheat-900/20 dark:text-wheat-300"
|
||||
>
|
||||
<.icon name="hero-exclamation-triangle" class="h-5 w-5" />
|
||||
<div>
|
||||
<h3 class="font-bold">Issues Detected</h3>
|
||||
|
|
@ -53,15 +56,15 @@ defmodule ToweropsWeb.TraceLive.Index do
|
|||
:good -> "border-green-200 dark:border-green-800"
|
||||
:warning -> "border-yellow-200 dark:border-yellow-800"
|
||||
:bad -> "border-red-200 dark:border-red-800"
|
||||
_ -> "border-base-200"
|
||||
_ -> "border-cool-steel-200 dark:border-cool-steel-700"
|
||||
end
|
||||
|
||||
assigns = assign(assigns, :color, color)
|
||||
|
||||
~H"""
|
||||
<div class={"stat bg-base-100 rounded-box border #{@color} p-4"}>
|
||||
<div class="stat-title text-xs">{@label}</div>
|
||||
<div class="stat-value text-2xl">{@value}</div>
|
||||
<div class={"flex flex-col bg-cool-steel-50 dark:bg-cool-steel-900 rounded-xl border #{@color} p-4"}>
|
||||
<div class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400">{@label}</div>
|
||||
<div class="text-2xl font-bold">{@value}</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
@ -133,12 +136,14 @@ defmodule ToweropsWeb.TraceLive.Index do
|
|||
|> push_patch(to: ~p"/trace")}
|
||||
end
|
||||
|
||||
def type_badge_class(:account), do: "badge-primary"
|
||||
def type_badge_class(:inventory_item), do: "badge-secondary"
|
||||
def type_badge_class(:site), do: "badge-warning"
|
||||
def type_badge_class(:device), do: "badge-accent"
|
||||
def type_badge_class(:access_point), do: "badge-info"
|
||||
def type_badge_class(_), do: "badge-ghost"
|
||||
def type_badge_class(:account),
|
||||
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
|
||||
|
||||
def type_badge_class(:inventory_item), do: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
|
||||
def type_badge_class(:site), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
def type_badge_class(:device), do: "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400"
|
||||
def type_badge_class(:access_point), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
def type_badge_class(_), do: "text-cool-steel-700 dark:text-cool-steel-300"
|
||||
|
||||
def type_badge_label(:account), do: "Account"
|
||||
def type_badge_label(:inventory_item), do: "Inventory"
|
||||
|
|
@ -148,10 +153,13 @@ defmodule ToweropsWeb.TraceLive.Index do
|
|||
def type_badge_label(_), do: "Unknown"
|
||||
|
||||
@doc false
|
||||
def status_badge_class("active"), do: "badge-success"
|
||||
def status_badge_class("suspended"), do: "badge-warning"
|
||||
def status_badge_class("cancelled"), do: "badge-error"
|
||||
def status_badge_class(_), do: "badge-ghost"
|
||||
def status_badge_class("active"), do: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
|
||||
def status_badge_class("suspended"), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
|
||||
def status_badge_class("cancelled"),
|
||||
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
|
||||
|
||||
def status_badge_class(_), do: "text-cool-steel-700 dark:text-cool-steel-300"
|
||||
|
||||
@doc false
|
||||
def device_status_color(:up), do: "text-green-600 dark:text-green-400"
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
|
||||
<div class="space-y-6">
|
||||
<%!-- Search Bar --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="p-4 sm:p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<.icon name="hero-magnifying-glass" class="h-6 w-6 text-gray-400 flex-shrink-0" />
|
||||
<form phx-change="search" phx-submit="search" class="flex-1 flex gap-2">
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
"Search by customer name, IP address, account ID, serial number, site name, or device name..."
|
||||
)
|
||||
}
|
||||
class="input input-bordered w-full dark:bg-gray-800 dark:border-white/10 dark:text-white"
|
||||
class="w-full rounded-field border border-cool-steel-300 bg-cool-steel-50 px-3 py-2 text-sm text-cool-steel-900 placeholder:text-cool-steel-400 focus:border-cerulean-500 focus:ring-1 focus:ring-cerulean-500 dark:border-cool-steel-600 dark:bg-cool-steel-900 dark:text-cool-steel-100 dark:placeholder:text-cool-steel-500 w-full dark:bg-gray-800 dark:border-white/10 dark:text-white"
|
||||
phx-debounce="300"
|
||||
autofocus
|
||||
/>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<button
|
||||
:if={@query != "" || @trace}
|
||||
phx-click="clear"
|
||||
class="btn btn-ghost btn-sm"
|
||||
class="btn text-sm !py-1 !px-3 bg-transparent hover:bg-cool-steel-200 dark:hover:bg-cool-steel-700"
|
||||
>
|
||||
<.icon name="hero-x-mark" class="h-5 w-5" /> Clear
|
||||
</button>
|
||||
|
|
@ -54,18 +54,21 @@
|
|||
<%!-- Search Results --%>
|
||||
<div
|
||||
:if={@results != [] && !@trace}
|
||||
class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900"
|
||||
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900"
|
||||
>
|
||||
<div class="card-body p-0">
|
||||
<div class="divide-y divide-base-200 dark:divide-white/10">
|
||||
<div class="p-0">
|
||||
<div class="divide-y divide-cool-steel-200 dark:divide-cool-steel-700 dark:divide-white/10">
|
||||
<button
|
||||
:for={result <- @results}
|
||||
phx-click="select_result"
|
||||
phx-value-type={result.type}
|
||||
phx-value-id={result.id}
|
||||
class="w-full flex items-center gap-3 px-4 sm:px-6 py-3 hover:bg-base-200/50 dark:hover:bg-white/5 text-left transition-colors"
|
||||
class="w-full flex items-center gap-3 px-4 sm:px-6 py-3 hover:bg-cool-steel-100/50 dark:hover:bg-cool-steel-800/50 dark:hover:bg-white/5 text-left transition-colors"
|
||||
>
|
||||
<span class={["badge badge-sm font-mono", type_badge_class(result.type)]}>
|
||||
<span class={[
|
||||
"badge text-xs !px-2 !py-0 font-mono bg-cool-steel-200 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300",
|
||||
type_badge_class(result.type)
|
||||
]}>
|
||||
{type_badge_label(result.type)}
|
||||
</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
|
|
@ -95,8 +98,8 @@
|
|||
<%!-- Site Trace View --%>
|
||||
<div :if={@trace && @trace_type == :site} class="space-y-4">
|
||||
<%!-- Site Info --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<.icon name="hero-building-office-2" class="h-5 w-5 text-gray-400" />
|
||||
{@trace.site.name}
|
||||
|
|
@ -120,14 +123,14 @@
|
|||
<%!-- Devices at Site --%>
|
||||
<div
|
||||
:if={length(@trace.devices) > 0}
|
||||
class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900"
|
||||
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900"
|
||||
>
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||
{t("Devices")}
|
||||
</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs text-gray-500 dark:text-gray-400 uppercase">
|
||||
<th>{t("Name")}</th>
|
||||
|
|
@ -138,7 +141,7 @@
|
|||
<tbody>
|
||||
<tr
|
||||
:for={device <- @trace.devices}
|
||||
class="hover:bg-base-200/50 dark:hover:bg-white/5"
|
||||
class="hover:bg-cool-steel-100/50 dark:hover:bg-cool-steel-800/50 dark:hover:bg-white/5"
|
||||
>
|
||||
<td>
|
||||
<.link
|
||||
|
|
@ -167,16 +170,16 @@
|
|||
<%!-- Recent Alerts --%>
|
||||
<div
|
||||
:if={@trace.alerts != []}
|
||||
class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900"
|
||||
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900"
|
||||
>
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||
{t("Recent Alerts")}
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
:for={alert <- @trace.alerts}
|
||||
class="flex items-start gap-3 p-2 rounded-lg hover:bg-base-200/50 dark:hover:bg-white/5"
|
||||
class="flex items-start gap-3 p-2 rounded-lg hover:bg-cool-steel-100/50 dark:hover:bg-cool-steel-800/50 dark:hover:bg-white/5"
|
||||
>
|
||||
<span class={"mt-1.5 h-2 w-2 rounded-full flex-shrink-0 #{alert_dot(alert)}"}>
|
||||
</span>
|
||||
|
|
@ -190,7 +193,7 @@
|
|||
</span>
|
||||
<span
|
||||
:if={alert.resolved_at}
|
||||
class="badge badge-xs badge-success"
|
||||
class="badge text-xs !px-2 !py-0 bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
|
||||
>
|
||||
{t("Resolved")}
|
||||
</span>
|
||||
|
|
@ -216,8 +219,8 @@
|
|||
<%!-- Subscriber + Connection --%>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<%!-- Subscriber Info --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
|
||||
<.icon name="hero-user" class="h-4 w-4 inline" /> Subscriber
|
||||
</h3>
|
||||
|
|
@ -262,7 +265,10 @@
|
|||
Customer since: {Calendar.strftime(@trace.subscriber.customer_since, "%b %Y")}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class={["badge badge-sm", status_badge_class(@trace.subscriber.status)]}>
|
||||
<span class={[
|
||||
"badge bg-cool-steel-200 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300",
|
||||
status_badge_class(@trace.subscriber.status)
|
||||
]}>
|
||||
{@trace.subscriber.status || "unknown"}
|
||||
</span>
|
||||
<%!-- Financial data: role-gated --%>
|
||||
|
|
@ -288,8 +294,8 @@
|
|||
</div>
|
||||
|
||||
<%!-- Connection / Device Info --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
|
||||
<.icon name="hero-signal" class="h-4 w-4 inline" /> Connection
|
||||
</h3>
|
||||
|
|
@ -336,7 +342,7 @@
|
|||
</div>
|
||||
<div
|
||||
:if={@trace.access_point}
|
||||
class="mt-2 pt-2 border-t border-base-200 dark:border-white/10"
|
||||
class="mt-2 pt-2 border-t border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10"
|
||||
>
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-1">
|
||||
{t("Preseem AP")}
|
||||
|
|
@ -362,8 +368,8 @@
|
|||
<%!-- QoE Metrics + Peer Impact --%>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<%!-- QoE Metrics --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
|
||||
<.icon name="hero-chart-bar" class="h-4 w-4 inline" /> QoE Metrics
|
||||
<span class="text-xs font-normal normal-case">(from Preseem)</span>
|
||||
|
|
@ -392,7 +398,7 @@
|
|||
/>
|
||||
</div>
|
||||
<%= if @trace.qoe_metrics.latest_metric do %>
|
||||
<div class="mt-4 pt-3 border-t border-base-200 dark:border-white/10">
|
||||
<div class="mt-4 pt-3 border-t border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10">
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">
|
||||
{t("Latest Metrics")}
|
||||
</div>
|
||||
|
|
@ -442,8 +448,8 @@
|
|||
</div>
|
||||
|
||||
<%!-- Peer Impact --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
|
||||
<.icon name="hero-users" class="h-4 w-4 inline" /> Peer Impact
|
||||
</h3>
|
||||
|
|
@ -481,7 +487,7 @@
|
|||
</div>
|
||||
<div
|
||||
:if={@trace.peer_impact.peer_aps != []}
|
||||
class="mt-3 pt-3 border-t border-base-200 dark:border-white/10"
|
||||
class="mt-3 pt-3 border-t border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10"
|
||||
>
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">
|
||||
{t("Peer APs")}
|
||||
|
|
@ -517,8 +523,8 @@
|
|||
</div>
|
||||
|
||||
<%!-- Active Alerts --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
|
||||
<.icon name="hero-bell-alert" class="h-4 w-4 inline" /> Recent Alerts
|
||||
</h3>
|
||||
|
|
@ -528,7 +534,7 @@
|
|||
<span class="text-sm font-medium">No alerts — all clear</span>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="divide-y divide-base-200 dark:divide-white/10">
|
||||
<div class="divide-y divide-cool-steel-200 dark:divide-cool-steel-700 dark:divide-white/10">
|
||||
<div
|
||||
:for={alert <- @trace.alerts}
|
||||
class="py-2 first:pt-0 last:pb-0 flex items-center gap-3"
|
||||
|
|
@ -549,8 +555,16 @@
|
|||
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">
|
||||
{format_relative_time(alert.triggered_at)}
|
||||
</div>
|
||||
<span :if={alert.resolved_at} class="badge badge-xs badge-success">resolved</span>
|
||||
<span :if={is_nil(alert.resolved_at)} class="badge badge-xs badge-error">
|
||||
<span
|
||||
:if={alert.resolved_at}
|
||||
class="badge text-xs !px-2 !py-0 bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
|
||||
>
|
||||
resolved
|
||||
</span>
|
||||
<span
|
||||
:if={is_nil(alert.resolved_at)}
|
||||
class="badge text-xs !px-2 !py-0 bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
|
||||
>
|
||||
active
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -562,8 +576,8 @@
|
|||
<%!-- Recent Activity: Config Changes + Insights --%>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<%!-- Config Changes — hidden, feature temporarily disabled --%>
|
||||
<div class="hidden card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="hidden card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
|
||||
<.icon name="hero-document-text" class="h-4 w-4 inline" /> Config Changes (24h)
|
||||
</h3>
|
||||
|
|
@ -596,8 +610,8 @@
|
|||
</div>
|
||||
|
||||
<%!-- Active Insights --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
|
||||
<.icon name="hero-light-bulb" class="h-4 w-4 inline" /> Active Insights
|
||||
</h3>
|
||||
|
|
@ -635,14 +649,14 @@
|
|||
<%!-- Inventory Items --%>
|
||||
<div
|
||||
:if={@trace.inventory_items != []}
|
||||
class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900"
|
||||
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900"
|
||||
>
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="p-4 sm:p-6">
|
||||
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
|
||||
<.icon name="hero-cube" class="h-4 w-4 inline" /> Equipment (from Gaiia)
|
||||
</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs uppercase text-gray-500 dark:text-gray-400">
|
||||
<th>Name</th>
|
||||
|
|
@ -653,13 +667,19 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :for={item <- @trace.inventory_items} class="hover">
|
||||
<tr
|
||||
:for={item <- @trace.inventory_items}
|
||||
class="hover:bg-cool-steel-100/50 dark:hover:bg-cool-steel-800/50"
|
||||
>
|
||||
<td class="font-medium text-gray-900 dark:text-white">{item.name}</td>
|
||||
<td class="font-mono text-sm">{item.ip_address}</td>
|
||||
<td>{item.model_name}</td>
|
||||
<td class="font-mono text-xs">{item.serial_number}</td>
|
||||
<td>
|
||||
<span class={["badge badge-xs", status_badge_class(item.status)]}>
|
||||
<span class={[
|
||||
"badge text-xs !px-2 !py-0 bg-cool-steel-200 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300",
|
||||
status_badge_class(item.status)
|
||||
]}>
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -4,24 +4,35 @@ defmodule ToweropsWeb.ConfigTimelineLiveHelpersTest do
|
|||
alias ToweropsWeb.ConfigTimelineLive
|
||||
|
||||
describe "change_size_class/1" do
|
||||
test "> 50 returns error" do
|
||||
assert "badge-error" == ConfigTimelineLive.change_size_class(51)
|
||||
assert "badge-error" == ConfigTimelineLive.change_size_class(1_000)
|
||||
test "> 50 returns error styling" do
|
||||
assert "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400" ==
|
||||
ConfigTimelineLive.change_size_class(51)
|
||||
|
||||
assert "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400" ==
|
||||
ConfigTimelineLive.change_size_class(1_000)
|
||||
end
|
||||
|
||||
test "21..50 returns warning" do
|
||||
assert "badge-warning" == ConfigTimelineLive.change_size_class(21)
|
||||
assert "badge-warning" == ConfigTimelineLive.change_size_class(50)
|
||||
test "21..50 returns warning styling" do
|
||||
assert "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400" ==
|
||||
ConfigTimelineLive.change_size_class(21)
|
||||
|
||||
assert "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400" ==
|
||||
ConfigTimelineLive.change_size_class(50)
|
||||
end
|
||||
|
||||
test "<= 20 returns info" do
|
||||
assert "badge-info" == ConfigTimelineLive.change_size_class(0)
|
||||
assert "badge-info" == ConfigTimelineLive.change_size_class(20)
|
||||
test "<= 20 returns info styling" do
|
||||
assert "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400" == ConfigTimelineLive.change_size_class(0)
|
||||
|
||||
assert "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400" ==
|
||||
ConfigTimelineLive.change_size_class(20)
|
||||
end
|
||||
|
||||
test "non-integer returns info" do
|
||||
assert "badge-info" == ConfigTimelineLive.change_size_class(nil)
|
||||
assert "badge-info" == ConfigTimelineLive.change_size_class("foo")
|
||||
test "non-integer returns info styling" do
|
||||
assert "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400" ==
|
||||
ConfigTimelineLive.change_size_class(nil)
|
||||
|
||||
assert "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400" ==
|
||||
ConfigTimelineLive.change_size_class("foo")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue