mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-04 21:32:57 +00:00
ui(site): enhance table components and visibility
Refactor domain, monitor, and system tables to improve visual clarity and information density. - Update badge and pill styles in `domains-table` and `monitors-table` to use solid background colors for better contrast. - Integrate `ContainersTable` into `MonitorCard` and `SystemCard` to provide nested container visibility. - Simplify `MonitorsTable` by removing the unused network view mode. - Improve layout and spacing in various table components for better readability.
This commit is contained in:
@@ -88,17 +88,17 @@ function DaysLeftBadge({ days, label = "days" }: { days: number | undefined; lab
|
|||||||
const isExpired = days < 0
|
const isExpired = days < 0
|
||||||
|
|
||||||
const colorClass = isExpired
|
const colorClass = isExpired
|
||||||
? "bg-red-500/15 text-red-600 border-red-500/30"
|
? "bg-red-500 text-white border-red-600"
|
||||||
: isCritical
|
: isCritical
|
||||||
? "bg-red-500/15 text-red-600 border-red-500/30"
|
? "bg-red-500 text-white border-red-600"
|
||||||
: isWarning
|
: isWarning
|
||||||
? "bg-yellow-500/15 text-yellow-600 border-yellow-500/30"
|
? "bg-yellow-500 text-white border-yellow-600"
|
||||||
: "bg-green-500/15 text-green-600 border-green-500/30"
|
: "bg-green-500 text-white border-green-600"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`inline-flex flex-col items-center justify-center px-3 py-1.5 rounded-lg border-2 ${colorClass} min-w-[70px]`}>
|
<div className={`inline-flex flex-col items-center justify-center px-3 py-1.5 rounded-lg border-2 ${colorClass} min-w-[70px]`}>
|
||||||
<span className="text-lg font-bold leading-none">{isExpired ? Math.abs(days) : days}</span>
|
<span className="text-lg font-bold leading-none">{isExpired ? Math.abs(days) : days}</span>
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wide opacity-80">{isExpired ? "EXPIRED" : days === 1 ? "DAY" : label.toUpperCase()}</span>
|
<span className="text-[10px] font-medium uppercase tracking-wide opacity-90">{isExpired ? "EXPIRED" : days === 1 ? "DAY" : label.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -552,80 +552,104 @@ export default function DomainsTable() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{filteredDomains.map((domain) => (
|
{filteredDomains.map((domain) => {
|
||||||
<div key={domain.id} className="rounded-lg border bg-card p-4 space-y-3 hover:shadow-md transition-shadow">
|
const isExpired = domain.status === "expired"
|
||||||
<div className="flex items-start justify-between">
|
const isExpiring = domain.status === "expiring"
|
||||||
<Link href={`/domain/${domain.id}`} className="flex items-center gap-3 cursor-pointer min-w-0">
|
const isActive = domain.status === "active"
|
||||||
{domain.favicon_url && (
|
|
||||||
<img
|
const cardGradient = isExpired
|
||||||
src={domain.favicon_url}
|
? "from-red-500/5 to-red-500/10 border-red-500/20"
|
||||||
alt=""
|
: isExpiring
|
||||||
className="h-5 w-5 shrink-0"
|
? "from-yellow-500/5 to-yellow-500/10 border-yellow-500/20"
|
||||||
onError={(e) => (e.currentTarget.style.display = "none")}
|
: isActive
|
||||||
/>
|
? "from-green-500/5 to-green-500/10 border-green-500/20"
|
||||||
)}
|
: "from-gray-500/5 to-gray-500/10 border-gray-500/20"
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="font-medium truncate hover:underline">{domain.domain_name}</div>
|
return (
|
||||||
|
<div key={domain.id} className={`rounded-lg border bg-gradient-to-br ${cardGradient} p-4 space-y-3 hover:shadow-lg hover:scale-[1.02] transition-all duration-200`}>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<Link href={`/domain/${domain.id}`} className="flex items-center gap-3 cursor-pointer min-w-0 flex-1">
|
||||||
|
{domain.favicon_url && (
|
||||||
|
<img
|
||||||
|
src={domain.favicon_url}
|
||||||
|
alt=""
|
||||||
|
className="h-8 w-8 shrink-0 rounded-lg bg-background p-1"
|
||||||
|
onError={(e) => (e.currentTarget.style.display = "none")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="font-semibold text-base truncate hover:underline">{domain.domain_name}</div>
|
||||||
|
{displayOptions.showRegistrar && (
|
||||||
|
<div className="text-xs text-muted-foreground truncate mt-0.5">{domain.registrar_name || "Unknown"}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 shrink-0">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => handleEdit(domain)}>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleRefresh(domain.id)} disabled={refreshMutation.isPending}>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
Refresh
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleDelete(domain.id)} className="text-destructive">
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(domain.status)}
|
||||||
|
<Badge className={getStatusBadgeColor(domain.status)}>
|
||||||
|
{getStatusLabel(domain.status)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{displayOptions.showTags && domain.tags && domain.tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{domain.tags.map((tag: string) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="inline-flex items-center gap-1 rounded-md bg-background/50 border px-2 py-0.5 text-[10px] font-medium"
|
||||||
|
>
|
||||||
|
<Tag className="h-3 w-3" />
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
)}
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 shrink-0">
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => handleEdit(domain)}>Edit</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleRefresh(domain.id)} disabled={refreshMutation.isPending}>
|
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
|
||||||
Refresh
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleDelete(domain.id)} className="text-destructive">
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="space-y-3">
|
||||||
{getStatusIcon(domain.status)}
|
|
||||||
<Badge className={getStatusBadgeColor(domain.status)}>
|
|
||||||
{getStatusLabel(domain.status)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{displayOptions.showTags && domain.tags && domain.tags.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{domain.tags.map((tag: string) => (
|
|
||||||
<span
|
|
||||||
key={tag}
|
|
||||||
className="inline-flex items-center gap-1 rounded-md bg-muted px-1.5 py-0.5 text-[10px] font-medium"
|
|
||||||
>
|
|
||||||
<Tag className="h-3 w-3" />
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-2 text-sm">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{displayOptions.showExpiryDate && (
|
{displayOptions.showExpiryDate && (
|
||||||
<span className="text-xs text-muted-foreground">{domain.expiry_date ? formatDate(domain.expiry_date) : "Unknown"}</span>
|
<div className="flex items-center justify-between text-sm">
|
||||||
)}
|
<span className="text-xs text-muted-foreground">Expiry</span>
|
||||||
{displayOptions.showRegistrar && (
|
<span className="font-medium">{domain.expiry_date ? formatDate(domain.expiry_date) : "Unknown"}</span>
|
||||||
<span className="text-xs text-muted-foreground truncate max-w-[120px]">{domain.registrar_name || "Unknown"}</span>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<DaysLeftBadge days={domain.days_until_expiry} />
|
|
||||||
{displayOptions.showSSL && domain.ssl_valid_to && (
|
|
||||||
<DaysLeftBadge days={domain.ssl_days_until} label="ssl" />
|
|
||||||
)}
|
)}
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<DaysLeftBadge days={domain.days_until_expiry} />
|
||||||
|
{displayOptions.showSSL && domain.ssl_valid_to && (
|
||||||
|
<DaysLeftBadge days={domain.ssl_days_until} label="ssl" />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => handleRefresh(domain.id)}
|
||||||
|
disabled={refreshMutation.isPending}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 ${refreshMutation.isPending ? 'animate-spin' : ''}`} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -70,9 +70,8 @@ import {
|
|||||||
} from "@/lib/monitors"
|
} from "@/lib/monitors"
|
||||||
import { cn, useBrowserStorage } from "@/lib/utils"
|
import { cn, useBrowserStorage } from "@/lib/utils"
|
||||||
import { AddMonitorDialog } from "./add-monitor-dialog"
|
import { AddMonitorDialog } from "./add-monitor-dialog"
|
||||||
import { GroupedMonitorsTable } from "./grouped-monitors-table"
|
|
||||||
import { Link } from "@/components/router"
|
import { Link } from "@/components/router"
|
||||||
import { Network } from "lucide-react"
|
import ContainersTable from "../containers-table/containers-table"
|
||||||
|
|
||||||
// Status indicator component
|
// Status indicator component
|
||||||
function StatusIndicator({ status }: { status: MonitorStatus }) {
|
function StatusIndicator({ status }: { status: MonitorStatus }) {
|
||||||
@@ -225,6 +224,11 @@ function MonitorCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Containers Section */}
|
||||||
|
<div className="pt-3 border-t">
|
||||||
|
<ContainersTable />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 pt-2 border-t">
|
<div className="flex items-center gap-2 pt-2 border-t">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -279,18 +283,18 @@ function MonitorCard({
|
|||||||
|
|
||||||
// Uptime pill badge component - big and visible
|
// Uptime pill badge component - big and visible
|
||||||
function UptimePill({ uptime, label = "24h" }: { uptime: number; label?: string }) {
|
function UptimePill({ uptime, label = "24h" }: { uptime: number; label?: string }) {
|
||||||
let colorClass = "bg-green-500/15 text-green-600 border-green-500/30"
|
let colorClass = "bg-green-500 text-white border-green-500"
|
||||||
let icon = <CheckCircleIcon className="h-3.5 w-3.5" />
|
let icon = <CheckCircleIcon className="h-3.5 w-3.5" />
|
||||||
|
|
||||||
if (uptime < 99.9) {
|
if (uptime < 99.9) {
|
||||||
colorClass = "bg-green-500/15 text-green-600 border-green-500/30"
|
colorClass = "bg-green-500 text-white border-green-500"
|
||||||
}
|
}
|
||||||
if (uptime < 95) {
|
if (uptime < 95) {
|
||||||
colorClass = "bg-yellow-500/15 text-yellow-600 border-yellow-500/30"
|
colorClass = "bg-yellow-500 text-white border-yellow-500"
|
||||||
icon = <AlertTriangle className="h-3.5 w-3.5" />
|
icon = <AlertTriangle className="h-3.5 w-3.5" />
|
||||||
}
|
}
|
||||||
if (uptime < 90) {
|
if (uptime < 90) {
|
||||||
colorClass = "bg-red-500/15 text-red-600 border-red-500/30"
|
colorClass = "bg-red-500 text-white border-red-500"
|
||||||
icon = <XCircle className="h-3.5 w-3.5" />
|
icon = <XCircle className="h-3.5 w-3.5" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +302,7 @@ function UptimePill({ uptime, label = "24h" }: { uptime: number; label?: string
|
|||||||
<div className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full border-2 ${colorClass}`}>
|
<div className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full border-2 ${colorClass}`}>
|
||||||
{icon}
|
{icon}
|
||||||
<span className="text-sm font-bold">{formatUptime(uptime)}</span>
|
<span className="text-sm font-bold">{formatUptime(uptime)}</span>
|
||||||
<span className="text-[10px] font-medium uppercase opacity-70">{label}</span>
|
<span className="text-[10px] font-medium uppercase opacity-90">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -309,10 +313,6 @@ function UptimeBar({ stats }: { stats?: Record<string, number> }) {
|
|||||||
const uptime7d = stats?.uptime_7d ?? 100
|
const uptime7d = stats?.uptime_7d ?? 100
|
||||||
const uptime30d = stats?.uptime_30d ?? 100
|
const uptime30d = stats?.uptime_30d ?? 100
|
||||||
|
|
||||||
let color = "bg-green-500"
|
|
||||||
if (uptime24h < 95) color = "bg-yellow-500"
|
|
||||||
if (uptime24h < 90) color = "bg-red-500"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -532,7 +532,7 @@ function MonitorRow({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewMode = "table" | "grid" | "network"
|
type ViewMode = "table" | "grid"
|
||||||
type StatusFilter = "all" | MonitorStatus
|
type StatusFilter = "all" | MonitorStatus
|
||||||
type TypeFilter = "all" | MonitorType
|
type TypeFilter = "all" | MonitorType
|
||||||
|
|
||||||
@@ -745,10 +745,6 @@ export default memo(function MonitorsTable() {
|
|||||||
<LayoutGridIcon className="size-4" />
|
<LayoutGridIcon className="size-4" />
|
||||||
<Trans>Grid</Trans>
|
<Trans>Grid</Trans>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
<DropdownMenuRadioItem value="network" className="gap-2">
|
|
||||||
<Network className="size-4" />
|
|
||||||
<Trans>Network (Grouped)</Trans>
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
</DropdownMenuRadioGroup>
|
</DropdownMenuRadioGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
@@ -807,8 +803,6 @@ export default memo(function MonitorsTable() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === "network" ? (
|
|
||||||
<GroupedMonitorsTable />
|
|
||||||
) : viewMode === "table" ? (
|
) : viewMode === "table" ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { $router, Link } from "../router"
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||||
import { SystemsTableColumns, ActionsButton, IndicatorDot } from "./systems-table-columns"
|
import { SystemsTableColumns, ActionsButton, IndicatorDot } from "./systems-table-columns"
|
||||||
import { AddSystemDialog } from "../add-system"
|
import { AddSystemDialog } from "../add-system"
|
||||||
|
import ContainersTable from "../containers-table/containers-table"
|
||||||
|
|
||||||
type ViewMode = "table" | "grid"
|
type ViewMode = "table" | "grid"
|
||||||
type StatusFilter = "all" | SystemRecord["status"]
|
type StatusFilter = "all" | SystemRecord["status"]
|
||||||
@@ -512,6 +513,9 @@ const SystemCard = memo(
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4 pt-4 border-t border-border/60">
|
||||||
|
<ContainersTable systemId={system.id} />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<Link
|
<Link
|
||||||
href={getPagePath($router, "system", { id: row.original.id })}
|
href={getPagePath($router, "system", { id: row.original.id })}
|
||||||
|
|||||||
Reference in New Issue
Block a user