feat(ui): apply glassmorphism and staggered animations to tables
This commit is contained in:
parent
e9ac1429c0
commit
185476750d
19 changed files with 405 additions and 368 deletions
|
|
@ -483,6 +483,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
padding="none"
|
padding="none"
|
||||||
class="overflow-hidden border border-gray-200 dark:border-gray-700"
|
class="overflow-hidden border border-gray-200 dark:border-gray-700"
|
||||||
border={false}
|
border={false}
|
||||||
|
tone="glass"
|
||||||
>
|
>
|
||||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||||
<SectionHeader title={props.title} size="sm" />
|
<SectionHeader title={props.title} size="sm" />
|
||||||
|
|
@ -868,7 +869,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${resource.disabled || props.globalDisableFlag?.() ? 'opacity-40' : ''}`}
|
class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors animate-enter ${resource.disabled || props.globalDisableFlag?.() ? 'opacity-40' : ''}`}
|
||||||
>
|
>
|
||||||
{/* Alert toggle column */}
|
{/* Alert toggle column */}
|
||||||
<td class="p-1 px-2 text-center align-middle">
|
<td class="p-1 px-2 text-center align-middle">
|
||||||
|
|
@ -1379,7 +1380,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${resource.disabled || props.globalDisableFlag?.() ? 'opacity-40' : ''}`}
|
class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors animate-enter ${resource.disabled || props.globalDisableFlag?.() ? 'opacity-40' : ''}`}
|
||||||
>
|
>
|
||||||
{/* Alert toggle column */}
|
{/* Alert toggle column */}
|
||||||
<td class="p-1 px-2 text-center align-middle">
|
<td class="p-1 px-2 text-center align-middle">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { createSignal, createMemo, Show, For, onMount, onCleanup, createEffect } from 'solid-js';
|
import { createSignal, createMemo, Show, For, onMount, onCleanup, createEffect } from 'solid-js';
|
||||||
import { useNavigate, useLocation } from '@solidjs/router';
|
import { useNavigate, useLocation } from '@solidjs/router';
|
||||||
import Toggle from '@/components/shared/Toggle';
|
import Toggle from '@/components/shared/Toggle';
|
||||||
|
import { Card } from '@/components/shared/Card';
|
||||||
|
|
||||||
// Workaround for eslint false-positive when `For` is used only in JSX
|
// Workaround for eslint false-positive when `For` is used only in JSX
|
||||||
const __ensureForUsage = For;
|
const __ensureForUsage = For;
|
||||||
|
|
@ -2217,8 +2218,7 @@ const cancelEdit = () => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleTabClick('proxmox')}
|
onClick={() => handleTabClick('proxmox')}
|
||||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${
|
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${activeTab() === 'proxmox'
|
||||||
activeTab() === 'proxmox'
|
|
||||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -2228,8 +2228,7 @@ const cancelEdit = () => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleTabClick('pmg')}
|
onClick={() => handleTabClick('pmg')}
|
||||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${
|
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${activeTab() === 'pmg'
|
||||||
activeTab() === 'pmg'
|
|
||||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -2239,8 +2238,7 @@ const cancelEdit = () => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleTabClick('hosts')}
|
onClick={() => handleTabClick('hosts')}
|
||||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${
|
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${activeTab() === 'hosts'
|
||||||
activeTab() === 'hosts'
|
|
||||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -2250,8 +2248,7 @@ const cancelEdit = () => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleTabClick('docker')}
|
onClick={() => handleTabClick('docker')}
|
||||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${
|
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${activeTab() === 'docker'
|
||||||
activeTab() === 'docker'
|
|
||||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -2745,7 +2742,7 @@ const cancelEdit = () => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={activeTab() === 'docker'}>
|
<Show when={activeTab() === 'docker'}>
|
||||||
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
<Card padding="md" tone="glass" class="mb-6">
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
|
@ -2780,9 +2777,9 @@ const cancelEdit = () => {
|
||||||
rows={4}
|
rows={4}
|
||||||
class="mt-4 w-full rounded-md border border-gray-300 bg-white p-3 text-sm text-gray-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-sky-400 dark:focus:ring-sky-600/40"
|
class="mt-4 w-full rounded-md border border-gray-300 bg-white p-3 text-sm text-gray-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-sky-400 dark:focus:ring-sky-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
<Card padding="md" tone="glass" class="mb-6">
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Swarm service alerts</h3>
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Swarm service alerts</h3>
|
||||||
|
|
@ -2865,7 +2862,7 @@ const cancelEdit = () => {
|
||||||
{serviceGapValidationMessage()}
|
{serviceGapValidationMessage()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
<Show when={hasSection('dockerHosts')}>
|
<Show when={hasSection('dockerHosts')}>
|
||||||
<div ref={registerSection('dockerHosts')} class="scroll-mt-24">
|
<div ref={registerSection('dockerHosts')} class="scroll-mt-24">
|
||||||
|
|
|
||||||
|
|
@ -1864,7 +1864,7 @@ const UnifiedBackups: Component = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<Card padding="none" class="mb-4 overflow-hidden">
|
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
|
||||||
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
|
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
|
||||||
<style>{`
|
<style>{`
|
||||||
.overflow-x-auto::-webkit-scrollbar { display: none; }
|
.overflow-x-auto::-webkit-scrollbar { display: none; }
|
||||||
|
|
@ -2138,7 +2138,7 @@ const UnifiedBackups: Component = () => {
|
||||||
</tr>
|
</tr>
|
||||||
<For each={group.items}>
|
<For each={group.items}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<tr class="border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
<tr class="border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/30 animate-enter">
|
||||||
<td class="p-0.5 pl-5 pr-1.5 text-sm align-middle">{item.vmid}</td>
|
<td class="p-0.5 pl-5 pr-1.5 text-sm align-middle">{item.vmid}</td>
|
||||||
<td class="p-0.5 px-1.5 align-middle">
|
<td class="p-0.5 px-1.5 align-middle">
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -929,7 +929,7 @@ export function Dashboard(props: DashboardProps) {
|
||||||
{/* Table View */}
|
{/* Table View */}
|
||||||
<Show when={connected() && initialDataReceived() && filteredGuests().length > 0}>
|
<Show when={connected() && initialDataReceived() && filteredGuests().length > 0}>
|
||||||
<ComponentErrorBoundary name="Guest Table">
|
<ComponentErrorBoundary name="Guest Table">
|
||||||
<Card padding="none" class="mb-4 bg-white dark:bg-gray-800">
|
<Card padding="none" tone="glass" class="mb-4">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
{/* Desktop Header */}
|
{/* Desktop Header */}
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,7 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowClass = createMemo(() => {
|
const rowClass = createMemo(() => {
|
||||||
const base = 'transition-all duration-200 relative';
|
const base = 'transition-all duration-200 relative animate-enter';
|
||||||
const hover = 'hover:shadow-sm';
|
const hover = 'hover:shadow-sm';
|
||||||
const alertBg = hasUnacknowledgedAlert()
|
const alertBg = hasUnacknowledgedAlert()
|
||||||
? props.alertStyles?.severity === 'critical'
|
? props.alertStyles?.severity === 'critical'
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="none" class="mb-4 overflow-hidden">
|
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
|
||||||
<ScrollableTable minWidth="300px" persistKey="docker-host-summary">
|
<ScrollableTable minWidth="300px" persistKey="docker-host-summary">
|
||||||
<table class="w-full table-fixed border-collapse sm:whitespace-nowrap">
|
<table class="w-full table-fixed border-collapse sm:whitespace-nowrap">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -227,7 +227,7 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowClass = () => {
|
const rowClass = () => {
|
||||||
const baseHover = 'cursor-pointer transition-all duration-200 relative hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-sm';
|
const baseHover = 'cursor-pointer transition-all duration-200 relative hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-sm animate-enter';
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
return 'cursor-pointer transition-all duration-200 relative bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 hover:shadow-sm z-10';
|
return 'cursor-pointer transition-all duration-200 relative bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 hover:shadow-sm z-10';
|
||||||
|
|
|
||||||
|
|
@ -1252,7 +1252,7 @@ const DockerContainerRow: Component<{
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
class={`grid items-center transition-all duration-200 ${hasDrawerContent() ? 'cursor-pointer' : ''} ${expanded() ? 'bg-gray-50 dark:bg-gray-800/40' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'} ${!isRunning() ? 'opacity-60' : ''}`}
|
class={`grid items-center transition-all duration-200 animate-enter ${hasDrawerContent() ? 'cursor-pointer' : ''} ${expanded() ? 'bg-gray-50 dark:bg-gray-800/40' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'} ${!isRunning() ? 'opacity-60' : ''}`}
|
||||||
style={{ 'grid-template-columns': props.gridTemplate() }}
|
style={{ 'grid-template-columns': props.gridTemplate() }}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
aria-expanded={expanded()}
|
aria-expanded={expanded()}
|
||||||
|
|
@ -1949,7 +1949,7 @@ const DockerServiceRow: Component<{
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
class={`grid items-center transition-all duration-200 ${hasTasks() ? 'cursor-pointer' : ''} ${expanded() ? 'bg-gray-50 dark:bg-gray-800/40' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'} ${!isHealthy() ? 'opacity-60' : ''}`}
|
class={`grid items-center transition-all duration-200 animate-enter ${hasTasks() ? 'cursor-pointer' : ''} ${expanded() ? 'bg-gray-50 dark:bg-gray-800/40' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'} ${!isHealthy() ? 'opacity-60' : ''}`}
|
||||||
style={{ 'grid-template-columns': props.gridTemplate() }}
|
style={{ 'grid-template-columns': props.gridTemplate() }}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
aria-expanded={expanded()}
|
aria-expanded={expanded()}
|
||||||
|
|
@ -2407,7 +2407,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card padding="none" class="overflow-hidden">
|
<Card padding="none" tone="glass" class="overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
{/* Header Row */}
|
{/* Header Row */}
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card padding="none" class="overflow-hidden">
|
<Card padding="none" tone="glass" class="overflow-hidden">
|
||||||
<ScrollableTable>
|
<ScrollableTable>
|
||||||
<table class="w-full border-collapse">
|
<table class="w-full border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -243,7 +243,7 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowClass = () => {
|
const rowClass = () => {
|
||||||
const base = 'border-b border-gray-200 dark:border-gray-700 transition-all duration-200';
|
const base = 'border-b border-gray-200 dark:border-gray-700 transition-all duration-200 animate-enter';
|
||||||
const hover = 'hover:bg-gray-50 dark:hover:bg-gray-800/50';
|
const hover = 'hover:bg-gray-50 dark:hover:bg-gray-800/50';
|
||||||
const clickable = hasDrawerContent() ? 'cursor-pointer' : '';
|
const clickable = hasDrawerContent() ? 'cursor-pointer' : '';
|
||||||
const expanded = drawerOpen() ? 'bg-gray-50 dark:bg-gray-800/40' : '';
|
const expanded = drawerOpen() ? 'bg-gray-50 dark:bg-gray-800/40' : '';
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ const MailGateway: Component = () => {
|
||||||
const rblPct = (rbl / Math.max(inbound, 1)) * 100;
|
const rblPct = (rbl / Math.max(inbound, 1)) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
|
<Card padding="none" tone="glass" class="overflow-hidden">
|
||||||
{/* Instance Header Strip */}
|
{/* Instance Header Strip */}
|
||||||
<div class="sticky top-0 z-10 flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-gray-800/40 border-b border-gray-200 dark:border-gray-700">
|
<div class="sticky top-0 z-10 flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-gray-800/40 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|
@ -133,14 +133,14 @@ const MailGateway: Component = () => {
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full text-xs">
|
<table class="w-full text-xs">
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs font-semibold text-gray-900 dark:text-gray-100">{formatNum(total)}</div>
|
<div class="text-xs font-semibold text-gray-900 dark:text-gray-100">{formatNum(total)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">{formatDec(total / 24)}/hr</div>
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">{formatDec(total / 24)}/hr</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Total processed</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Total processed</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(inbound)}</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(inbound)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -149,7 +149,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Inbound</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Inbound</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(outbound)}</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(outbound)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -158,7 +158,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Outbound</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Outbound</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs font-medium text-orange-600 dark:text-orange-400">{formatNum(spam)}</div>
|
<div class="text-xs font-medium text-orange-600 dark:text-orange-400">{formatNum(spam)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -167,7 +167,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Spam caught</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Spam caught</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs font-medium text-red-600 dark:text-red-400">{formatNum(virus)}</div>
|
<div class="text-xs font-medium text-red-600 dark:text-red-400">{formatNum(virus)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -189,7 +189,7 @@ const MailGateway: Component = () => {
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full text-xs">
|
<table class="w-full text-xs">
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatDec(bytesIn / 1024 / 1024)} MB</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatDec(bytesIn / 1024 / 1024)} MB</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -198,7 +198,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Inbound bytes</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Inbound bytes</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatDec(bytesOut / 1024 / 1024)} MB</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatDec(bytesOut / 1024 / 1024)} MB</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -223,7 +223,7 @@ const MailGateway: Component = () => {
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full text-xs">
|
<table class="w-full text-xs">
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs font-medium text-orange-600 dark:text-orange-400">{formatNum(qSpam)}</div>
|
<div class="text-xs font-medium text-orange-600 dark:text-orange-400">{formatNum(qSpam)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -232,7 +232,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Spam quarantined</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Spam quarantined</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs font-medium text-red-600 dark:text-red-400">{formatNum(qVirus)}</div>
|
<div class="text-xs font-medium text-red-600 dark:text-red-400">{formatNum(qVirus)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -241,7 +241,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Virus quarantined</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Virus quarantined</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs font-medium text-yellow-600 dark:text-yellow-400">{formatNum(qAttachment)}</div>
|
<div class="text-xs font-medium text-yellow-600 dark:text-yellow-400">{formatNum(qAttachment)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -250,7 +250,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Attachments blocked</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Attachments blocked</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(qBlacklist)}</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(qBlacklist)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -259,7 +259,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Blacklisted</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Blacklisted</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs font-semibold text-gray-900 dark:text-gray-100">{formatNum(qTotal)}</div>
|
<div class="text-xs font-semibold text-gray-900 dark:text-gray-100">{formatNum(qTotal)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -315,7 +315,7 @@ const MailGateway: Component = () => {
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full text-xs">
|
<table class="w-full text-xs">
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(bouncesIn)}</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(bouncesIn)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -324,7 +324,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Bounces inbound</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Bounces inbound</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(bouncesOut)}</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(bouncesOut)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -333,7 +333,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Bounces outbound</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Bounces outbound</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(rbl)}</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(rbl)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -342,7 +342,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">RBL rejects</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">RBL rejects</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(pregreet)}</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(pregreet)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -351,7 +351,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Pregreet rejects</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Pregreet rejects</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(greylist)}</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(greylist)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -360,7 +360,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Greylisted</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Greylisted</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(junk)}</div>
|
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(junk)}</div>
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
<div class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -369,7 +369,7 @@ const MailGateway: Component = () => {
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Junk mail</td>
|
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Junk mail</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="text-xs text-gray-900 dark:text-gray-100">
|
<div class="text-xs text-gray-900 dark:text-gray-100">
|
||||||
{pmg.mailStats?.averageProcessTimeMs ? formatDec(pmg.mailStats.averageProcessTimeMs / 1000, 2) : '—'} s
|
{pmg.mailStats?.averageProcessTimeMs ? formatDec(pmg.mailStats.averageProcessTimeMs / 1000, 2) : '—'} s
|
||||||
|
|
@ -421,7 +421,7 @@ const MailGateway: Component = () => {
|
||||||
const oldestClass = oldestAge > 1800 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-700 dark:text-gray-300';
|
const oldestClass = oldestAge > 1800 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-700 dark:text-gray-300';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="px-2 py-1.5 text-xs font-medium text-gray-900 dark:text-gray-100 truncate max-w-[150px]">{node.name}</td>
|
<td class="px-2 py-1.5 text-xs font-medium text-gray-900 dark:text-gray-100 truncate max-w-[150px]">{node.name}</td>
|
||||||
<td class="px-2 py-1.5 text-xs text-gray-700 dark:text-gray-300 capitalize">{node.role || '—'}</td>
|
<td class="px-2 py-1.5 text-xs text-gray-700 dark:text-gray-300 capitalize">{node.role || '—'}</td>
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
|
|
@ -463,7 +463,7 @@ const MailGateway: Component = () => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ const Replication: Component = () => {
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card padding="none">
|
<Card padding="none" tone="glass">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-[1000px] w-full divide-y divide-gray-200 dark:divide-gray-700">
|
<table class="min-w-[1000px] w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead class="bg-gray-50 dark:bg-gray-900/40">
|
<thead class="bg-gray-50 dark:bg-gray-900/40">
|
||||||
|
|
@ -121,7 +121,7 @@ const Replication: Component = () => {
|
||||||
{(job) => {
|
{(job) => {
|
||||||
const badge = getStatusBadge(job);
|
const badge = getStatusBadge(job);
|
||||||
return (
|
return (
|
||||||
<tr class="hover:bg-gray-50/80 dark:hover:bg-gray-900/40 transition-colors">
|
<tr class="hover:bg-gray-50/80 dark:hover:bg-gray-900/40 transition-colors animate-enter">
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
|
<div class="font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
|
||||||
{job.guestName || `VM ${job.guestId ?? ''}`}
|
{job.guestName || `VM ${job.guestId ?? ''}`}
|
||||||
|
|
|
||||||
|
|
@ -421,15 +421,13 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class={`rounded-lg border p-4 text-sm shadow-sm ${
|
class={`rounded-lg border p-4 text-sm shadow-sm ${hasWildcardTokens()
|
||||||
hasWildcardTokens()
|
|
||||||
? 'border-amber-300/80 bg-amber-50/80 text-amber-900 dark:border-amber-700/70 dark:bg-amber-900/20 dark:text-amber-100'
|
? 'border-amber-300/80 bg-amber-50/80 text-amber-900 dark:border-amber-700/70 dark:bg-amber-900/20 dark:text-amber-100'
|
||||||
: 'border-gray-200/70 bg-white/70 text-gray-700 dark:border-gray-700/70 dark:bg-gray-900/40 dark:text-gray-300'
|
: 'border-gray-200/70 bg-white/70 text-gray-700 dark:border-gray-700/70 dark:bg-gray-900/40 dark:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={`text-[0.7rem] font-semibold uppercase tracking-wide ${
|
class={`text-[0.7rem] font-semibold uppercase tracking-wide ${hasWildcardTokens()
|
||||||
hasWildcardTokens()
|
|
||||||
? 'text-amber-700 dark:text-amber-300'
|
? 'text-amber-700 dark:text-amber-300'
|
||||||
: 'text-gray-500 dark:text-gray-400'
|
: 'text-gray-500 dark:text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -437,8 +435,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
Full access tokens
|
Full access tokens
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class={`mt-1 text-2xl font-semibold ${
|
class={`mt-1 text-2xl font-semibold ${hasWildcardTokens()
|
||||||
hasWildcardTokens()
|
|
||||||
? 'text-amber-800 dark:text-amber-100'
|
? 'text-amber-800 dark:text-amber-100'
|
||||||
: 'text-gray-900 dark:text-gray-100'
|
: 'text-gray-900 dark:text-gray-100'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -446,8 +443,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
{wildcardCount()}
|
{wildcardCount()}
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
class={`mt-1 text-xs ${
|
class={`mt-1 text-xs ${hasWildcardTokens()
|
||||||
hasWildcardTokens()
|
|
||||||
? 'text-amber-700 dark:text-amber-200'
|
? 'text-amber-700 dark:text-amber-200'
|
||||||
: 'text-gray-500 dark:text-gray-400'
|
: 'text-gray-500 dark:text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -517,7 +513,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card padding="none" class="overflow-hidden border border-gray-200 dark:border-gray-700">
|
<Card padding="none" tone="glass" class="overflow-hidden">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 bg-gray-50/60 px-5 py-4 dark:border-gray-700 dark:bg-gray-900/40">
|
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 bg-gray-50/60 px-5 py-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-100">Token inventory</h4>
|
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-100">Token inventory</h4>
|
||||||
|
|
@ -586,8 +582,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
class={`transition-colors ${
|
class={`transition-colors animate-enter ${rowIsWildcard
|
||||||
rowIsWildcard
|
|
||||||
? 'bg-amber-50/50 dark:bg-amber-900/10'
|
? 'bg-amber-50/50 dark:bg-amber-900/10'
|
||||||
: 'bg-white dark:bg-gray-900/10'
|
: 'bg-white dark:bg-gray-900/10'
|
||||||
} hover:bg-blue-50/40 dark:hover:bg-gray-800/50`}
|
} hover:bg-blue-50/40 dark:hover:bg-gray-800/50`}
|
||||||
|
|
@ -605,8 +600,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
const isWildcard = scope.value === '*';
|
const isWildcard = scope.value === '*';
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${
|
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${isWildcard
|
||||||
isWildcard
|
|
||||||
? 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200'
|
? 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200'
|
||||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
|
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -667,8 +661,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
padding="lg"
|
padding="lg"
|
||||||
class={`border border-gray-200 dark:border-gray-700 transition-shadow ${
|
class={`border border-gray-200 dark:border-gray-700 transition-shadow ${createHighlight() ? 'ring-2 ring-blue-500/60 shadow-lg' : ''
|
||||||
createHighlight() ? 'ring-2 ring-blue-500/60 shadow-lg' : ''
|
|
||||||
}`}
|
}`}
|
||||||
ref={(el: HTMLDivElement) => {
|
ref={(el: HTMLDivElement) => {
|
||||||
createSectionRef = el;
|
createSectionRef = el;
|
||||||
|
|
@ -723,8 +716,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold transition ${
|
class={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold transition ${isFullAccessSelected()
|
||||||
isFullAccessSelected()
|
|
||||||
? 'border-blue-500 bg-blue-600 text-white shadow-sm'
|
? 'border-blue-500 bg-blue-600 text-white shadow-sm'
|
||||||
: 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200'
|
: 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -738,8 +730,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
{(preset) => (
|
{(preset) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold transition ${
|
class={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold transition ${presetMatchesSelection(preset.scopes)
|
||||||
presetMatchesSelection(preset.scopes)
|
|
||||||
? 'border-blue-500 bg-blue-600 text-white shadow-sm'
|
? 'border-blue-500 bg-blue-600 text-white shadow-sm'
|
||||||
: 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200'
|
: 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -771,8 +762,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`rounded-full border px-3 py-1 text-xs font-semibold transition ${
|
class={`rounded-full border px-3 py-1 text-xs font-semibold transition ${isActive()
|
||||||
isActive()
|
|
||||||
? 'border-blue-500 bg-blue-600 text-white shadow-sm'
|
? 'border-blue-500 bg-blue-600 text-white shadow-sm'
|
||||||
: 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200'
|
: 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200'
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Component, For, Show, createMemo } from 'solid-js';
|
import { Component, For, Show, createMemo } from 'solid-js';
|
||||||
import type { NodeConfig } from '@/types/nodes';
|
import type { NodeConfig } from '@/types/nodes';
|
||||||
|
import { Card } from '@/components/shared/Card';
|
||||||
|
|
||||||
type NodeConfigWithStatus = NodeConfig & {
|
type NodeConfigWithStatus = NodeConfig & {
|
||||||
hasPassword?: boolean;
|
hasPassword?: boolean;
|
||||||
|
|
@ -271,7 +272,7 @@ const resolvePveStatusMeta = (
|
||||||
|
|
||||||
export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
|
export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
<Card padding="none" tone="glass" class="overflow-x-auto rounded-lg">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||||
<thead class="bg-gray-50 dark:bg-gray-800/70">
|
<thead class="bg-gray-50 dark:bg-gray-800/70">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -310,7 +311,7 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors">
|
<tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors animate-enter">
|
||||||
<td class="align-top py-3 pl-4 pr-3">
|
<td class="align-top py-3 pl-4 pr-3">
|
||||||
<div class="min-w-0 space-y-1">
|
<div class="min-w-0 space-y-1">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
|
|
@ -492,7 +493,7 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -541,7 +542,7 @@ const resolvePbsStatusMeta = (
|
||||||
|
|
||||||
export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
|
export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
<Card padding="none" tone="glass" class="overflow-x-auto rounded-lg">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||||
<thead class="bg-gray-50 dark:bg-gray-800/70">
|
<thead class="bg-gray-50 dark:bg-gray-800/70">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -567,7 +568,7 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
|
||||||
{(node) => {
|
{(node) => {
|
||||||
const statusMeta = createMemo(() => resolvePbsStatusMeta(node, props.statePbs));
|
const statusMeta = createMemo(() => resolvePbsStatusMeta(node, props.statePbs));
|
||||||
return (
|
return (
|
||||||
<tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors">
|
<tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors animate-enter">
|
||||||
<td class="align-top py-3 pl-4 pr-3">
|
<td class="align-top py-3 pl-4 pr-3">
|
||||||
<div class="min-w-0 space-y-1">
|
<div class="min-w-0 space-y-1">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
|
|
@ -675,7 +676,7 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -724,7 +725,7 @@ const resolvePmgStatusMeta = (
|
||||||
|
|
||||||
export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
|
export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
<Card padding="none" tone="glass" class="overflow-x-auto rounded-lg">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||||
<thead class="bg-gray-50 dark:bg-gray-800/70">
|
<thead class="bg-gray-50 dark:bg-gray-800/70">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -750,7 +751,7 @@ export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
|
||||||
{(node) => {
|
{(node) => {
|
||||||
const statusMeta = createMemo(() => resolvePmgStatusMeta(node, props.statePmg));
|
const statusMeta = createMemo(() => resolvePmgStatusMeta(node, props.statePmg));
|
||||||
return (
|
return (
|
||||||
<tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors">
|
<tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors animate-enter">
|
||||||
<td class="align-top py-3 pl-4 pr-3">
|
<td class="align-top py-3 pl-4 pr-3">
|
||||||
<div class="min-w-0 space-y-1">
|
<div class="min-w-0 space-y-1">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
|
|
@ -847,6 +848,6 @@ export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -635,7 +635,7 @@ export const UnifiedAgents: Component = () => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
<Card padding="none" tone="glass" class="overflow-hidden rounded-lg">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -656,7 +656,7 @@ export const UnifiedAgents: Component = () => {
|
||||||
</tr>
|
</tr>
|
||||||
}>
|
}>
|
||||||
{(agent) => (
|
{(agent) => (
|
||||||
<tr>
|
<tr class="animate-enter">
|
||||||
<td class="whitespace-nowrap px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100">
|
<td class="whitespace-nowrap px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{agent.displayName || agent.hostname}
|
{agent.displayName || agent.hostname}
|
||||||
<Show when={agent.displayName && agent.displayName !== agent.hostname}>
|
<Show when={agent.displayName && agent.displayName !== agent.hostname}>
|
||||||
|
|
@ -709,7 +709,7 @@ export const UnifiedAgents: Component = () => {
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
</Card>
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ export const DiskList: Component<DiskListProps> = (props) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={filteredDisks().length > 0}>
|
<Show when={filteredDisks().length > 0}>
|
||||||
<Card padding="none" class="overflow-hidden">
|
<Card padding="none" tone="glass" class="overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -191,7 +191,7 @@ export const DiskList: Component<DiskListProps> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors">
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors animate-enter">
|
||||||
<td class="px-2 py-1.5 text-xs">
|
<td class="px-2 py-1.5 text-xs">
|
||||||
<span class="font-medium text-gray-900 dark:text-gray-100">
|
<span class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
{disk.node}
|
{disk.node}
|
||||||
|
|
|
||||||
|
|
@ -742,7 +742,7 @@ const Storage: Component = () => {
|
||||||
{/* Storage Table - shows for both PVE and PBS storage */}
|
{/* Storage Table - shows for both PVE and PBS storage */}
|
||||||
<Show when={connected() && initialDataReceived() && sortedStorage().length > 0}>
|
<Show when={connected() && initialDataReceived() && sortedStorage().length > 0}>
|
||||||
<ComponentErrorBoundary name="Storage Table">
|
<ComponentErrorBoundary name="Storage Table">
|
||||||
<Card padding="none" class="mb-4 overflow-hidden">
|
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
|
||||||
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
|
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
|
||||||
<style>{`
|
<style>{`
|
||||||
.overflow-x-auto::-webkit-scrollbar { display: none; }
|
.overflow-x-auto::-webkit-scrollbar { display: none; }
|
||||||
|
|
@ -1043,7 +1043,7 @@ const Storage: Component = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr
|
<tr
|
||||||
class={`${rowClass()} transition-colors`}
|
class={`${rowClass()} transition-colors animate-enter`}
|
||||||
style={rowStyle()}
|
style={rowStyle()}
|
||||||
onClick={toggleDrawer}
|
onClick={toggleDrawer}
|
||||||
aria-expanded={canExpand() && isExpanded() ? 'true' : 'false'}
|
aria-expanded={canExpand() && isExpanded() ? 'true' : 'false'}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { JSX, splitProps, mergeProps } from 'solid-js';
|
import { JSX, splitProps, mergeProps } from 'solid-js';
|
||||||
|
|
||||||
type Tone = 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger';
|
type Tone = 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger' | 'glass';
|
||||||
type Padding = 'none' | 'sm' | 'md' | 'lg';
|
type Padding = 'none' | 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
type CardProps = {
|
type CardProps = {
|
||||||
|
|
@ -17,6 +17,7 @@ const toneClassMap: Record<Tone, string> = {
|
||||||
success: 'bg-green-50/70 dark:bg-green-900/20',
|
success: 'bg-green-50/70 dark:bg-green-900/20',
|
||||||
warning: 'bg-amber-50/80 dark:bg-amber-900/20',
|
warning: 'bg-amber-50/80 dark:bg-amber-900/20',
|
||||||
danger: 'bg-red-50/80 dark:bg-red-900/20',
|
danger: 'bg-red-50/80 dark:bg-red-900/20',
|
||||||
|
glass: 'glass',
|
||||||
};
|
};
|
||||||
|
|
||||||
const paddingClassMap: Record<Padding, string> = {
|
const paddingClassMap: Record<Padding, string> = {
|
||||||
|
|
|
||||||
|
|
@ -498,7 +498,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="none" class="mb-4 overflow-hidden">
|
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
|
|
@ -569,7 +569,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowClass = createMemo(() => {
|
const rowClass = createMemo(() => {
|
||||||
const baseHover = 'cursor-pointer transition-all duration-200 relative hover:shadow-sm group';
|
const baseHover = 'cursor-pointer transition-all duration-200 relative hover:shadow-sm group animate-enter';
|
||||||
|
|
||||||
if (isSelected()) {
|
if (isSelected()) {
|
||||||
return `cursor-pointer transition-all duration-200 relative hover:shadow-sm z-10 group`;
|
return `cursor-pointer transition-all duration-200 relative hover:shadow-sm z-10 group`;
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ interface StatusDotProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const VARIANT_CLASSES: Record<StatusIndicatorVariant, string> = {
|
const VARIANT_CLASSES: Record<StatusIndicatorVariant, string> = {
|
||||||
success: 'bg-emerald-500 dark:bg-emerald-400',
|
success: 'bg-emerald-500 dark:bg-emerald-400 glow-success',
|
||||||
warning: 'bg-amber-500 dark:bg-amber-400',
|
warning: 'bg-amber-500 dark:bg-amber-400 glow-warning',
|
||||||
danger: 'bg-red-500 dark:bg-red-400',
|
danger: 'bg-red-500 dark:bg-red-400 glow-danger',
|
||||||
muted: 'bg-gray-400 dark:bg-gray-500',
|
muted: 'bg-gray-400 dark:bg-gray-500',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -479,3 +479,50 @@ body,
|
||||||
.delay-300 {
|
.delay-300 {
|
||||||
animation-delay: 300ms;
|
animation-delay: 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism Utilities */
|
||||||
|
.glass {
|
||||||
|
@apply bg-white/80 dark:bg-gray-800/80 backdrop-blur-md border border-white/20 dark:border-gray-700/30 shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-panel {
|
||||||
|
@apply bg-white/60 dark:bg-gray-900/60 backdrop-blur-xl border border-gray-200/50 dark:border-gray-700/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-hover {
|
||||||
|
@apply hover:bg-white/90 dark:hover:bg-gray-800/90 transition-colors duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Staggered Entry Animation */
|
||||||
|
@keyframes enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-enter {
|
||||||
|
animation: enter 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glow effects for status dots */
|
||||||
|
.glow-success {
|
||||||
|
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-warning {
|
||||||
|
box-shadow: 0 0 8px rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-danger {
|
||||||
|
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-info {
|
||||||
|
box-shadow: 0 0 8px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue