mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
feat(site): enhance monitoring, domain, and system tracking
Build Docker images / Hub (push) Failing after 5m57s
Build Docker images / Hub (push) Failing after 5m57s
- Improve domain lookup by adding CNAME and SRV record support
- Enhance domain status logic to include expiry and DNS resolution verification
- Update monitoring API to perform synchronous initial checks for immediate status updates
- Refactor site UI:
- Add tag filtering to domains and monitors tables
- Improve calendar view with better visual indicators for today and events
- Update monitor detail view with improved status badges and pending states
- Simplify home page layout by removing redundant card wrappers
- Update localization files for numerous languages to support new UI elements
- Add `cleanEndpointsConfig` to hub to safely reuse Docker network settings during container updates
This commit is contained in:
@@ -60,17 +60,17 @@ import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Settings2Icon,
|
||||
FilterIcon,
|
||||
LayoutGridIcon,
|
||||
LayoutListIcon,
|
||||
Tag,
|
||||
} from "lucide-react"
|
||||
import { DomainDialog } from "./domain-dialog"
|
||||
import { Link } from "@/components/router"
|
||||
import { useBrowserStorage } from "@/lib/utils"
|
||||
|
||||
type ViewMode = "table" | "grid"
|
||||
type StatusFilter = "all" | "active" | "expiring" | "expired" | "unknown" | "watchlist"
|
||||
type StatusFilter = "all" | "active" | "expiring" | "expired" | "unknown" | "paused"
|
||||
|
||||
export default function DomainsTable() {
|
||||
const { t } = useLingui()
|
||||
@@ -81,6 +81,7 @@ export default function DomainsTable() {
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
|
||||
const [filter, setFilter] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
||||
const [tagFilter, setTagFilter] = useState<string>("all")
|
||||
|
||||
const [viewMode, setViewMode] = useBrowserStorage<ViewMode>(
|
||||
"domainsViewMode",
|
||||
@@ -98,16 +99,29 @@ export default function DomainsTable() {
|
||||
return domains.filter((d) => d.status === statusFilter)
|
||||
}, [domains, statusFilter])
|
||||
|
||||
// Then filter by search text
|
||||
// Then filter by search text and tags
|
||||
const filteredDomains = useMemo(() => {
|
||||
if (!filter) return statusFilteredDomains
|
||||
const f = filter.toLowerCase()
|
||||
return statusFilteredDomains.filter(
|
||||
(d) =>
|
||||
d.domain_name.toLowerCase().includes(f) ||
|
||||
(d.registrar_name || "").toLowerCase().includes(f)
|
||||
)
|
||||
}, [statusFilteredDomains, filter])
|
||||
let result = statusFilteredDomains
|
||||
if (filter) {
|
||||
const f = filter.toLowerCase()
|
||||
result = result.filter(
|
||||
(d) =>
|
||||
d.domain_name.toLowerCase().includes(f) ||
|
||||
(d.registrar_name || "").toLowerCase().includes(f)
|
||||
)
|
||||
}
|
||||
if (tagFilter !== "all") {
|
||||
result = result.filter((d) => d.tags?.includes(tagFilter))
|
||||
}
|
||||
return result
|
||||
}, [statusFilteredDomains, filter, tagFilter])
|
||||
|
||||
// Extract all unique tags
|
||||
const allTags = useMemo(() => {
|
||||
const tagSet = new Set<string>()
|
||||
domains.forEach((d) => d.tags?.forEach((tag) => tagSet.add(tag)))
|
||||
return Array.from(tagSet).sort()
|
||||
}, [domains])
|
||||
|
||||
const statusCounts = useMemo(() => {
|
||||
const total = domains.length
|
||||
@@ -115,7 +129,8 @@ export default function DomainsTable() {
|
||||
const expiring = domains.filter((d) => d.status === "expiring").length
|
||||
const expired = domains.filter((d) => d.status === "expired").length
|
||||
const unknown = domains.filter((d) => d.status === "unknown").length
|
||||
return { total, active, expiring, expired, unknown }
|
||||
const paused = domains.filter((d) => d.status === "paused").length
|
||||
return { total, active, expiring, expired, unknown, paused }
|
||||
}, [domains])
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
@@ -205,6 +220,13 @@ export default function DomainsTable() {
|
||||
<AlertTriangle className="inline h-3 w-3 text-red-500" />
|
||||
</>
|
||||
)}
|
||||
{statusCounts.paused > 0 && (
|
||||
<>
|
||||
{" "}
|
||||
{statusCounts.paused}{" "}
|
||||
<Clock className="inline h-3 w-3 text-gray-400" />
|
||||
</>
|
||||
)}
|
||||
/ {statusCounts.total})
|
||||
</span>
|
||||
</CardDescription>
|
||||
@@ -215,6 +237,31 @@ export default function DomainsTable() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Quick status filters */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{[
|
||||
{ key: "all", label: `All ${statusCounts.total}`, color: "bg-primary" },
|
||||
{ key: "active", label: `Active ${statusCounts.active}`, color: "bg-green-500" },
|
||||
{ key: "expiring", label: `Expiring ${statusCounts.expiring}`, color: "bg-yellow-500" },
|
||||
{ key: "expired", label: `Expired ${statusCounts.expired}`, color: "bg-red-500" },
|
||||
{ key: "unknown", label: `Unknown ${statusCounts.unknown}`, color: "bg-gray-400" },
|
||||
{ key: "paused", label: `Paused ${statusCounts.paused}`, color: "bg-gray-400" },
|
||||
].map((s) => (
|
||||
<Button
|
||||
key={s.key}
|
||||
variant={statusFilter === s.key ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-7 text-xs gap-1.5"
|
||||
onClick={() => setStatusFilter(s.key as StatusFilter)}
|
||||
disabled={s.key !== "all" && parseInt(s.label.split(" ")[1]) === 0}
|
||||
>
|
||||
<span className={`h-2 w-2 rounded-full ${s.color}`} />
|
||||
{s.label.split(" ")[0]}
|
||||
<span className="text-[10px] opacity-70">{s.label.split(" ")[1]}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filter row */}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<div className="relative flex-1">
|
||||
@@ -225,11 +272,33 @@ export default function DomainsTable() {
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
{allTags.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Tag className="me-1.5 size-4 opacity-80" />
|
||||
{tagFilter === "all" ? t`Tags` : tagFilter}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuRadioGroup value={tagFilter} onValueChange={setTagFilter}>
|
||||
<DropdownMenuRadioItem value="all">
|
||||
<Trans>All Tags</Trans>
|
||||
</DropdownMenuRadioItem>
|
||||
{allTags.map((tag) => (
|
||||
<DropdownMenuRadioItem key={tag} value={tag}>
|
||||
{tag}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Settings2Icon className="me-1.5 size-4 opacity-80" />
|
||||
<Trans>View</Trans>
|
||||
<Button variant="outline" size="sm">
|
||||
<FilterIcon className="me-1.5 size-4 opacity-80" />
|
||||
<Trans>Options</Trans>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-48">
|
||||
@@ -271,6 +340,11 @@ export default function DomainsTable() {
|
||||
<DropdownMenuRadioItem value="unknown">
|
||||
<Trans>Unknown ({statusCounts.unknown})</Trans>
|
||||
</DropdownMenuRadioItem>
|
||||
{statusCounts.paused > 0 && (
|
||||
<DropdownMenuRadioItem value="paused">
|
||||
<Trans>Paused ({statusCounts.paused})</Trans>
|
||||
</DropdownMenuRadioItem>
|
||||
)}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -304,6 +378,7 @@ export default function DomainsTable() {
|
||||
<TableHead>Days Left</TableHead>
|
||||
<TableHead>Registrar</TableHead>
|
||||
<TableHead>SSL Expiry</TableHead>
|
||||
<TableHead>Tags</TableHead>
|
||||
<TableHead className="w-[100px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -361,6 +436,19 @@ export default function DomainsTable() {
|
||||
"N/A"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<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>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -447,6 +535,20 @@ export default function DomainsTable() {
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{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 grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Days Left</div>
|
||||
|
||||
Reference in New Issue
Block a user