Compare commits

..

4 Commits

Author SHA1 Message Date
Pavel Pikta
4d05bfdff0 feat: add crossorigin attribute to manifest link (#1457)
Signed-off-by: Pavel Pikta <pavel_pikta@epam.com>
2025-11-26 19:41:54 -05:00
henrygd
0388401a9e change layout of extra disks in all systems table (#1409) 2025-11-25 16:23:48 -05:00
henrygd
162c548010 quiet hours refactoring: change 'future' to 'inactive' 2025-11-24 19:12:35 -05:00
henrygd
888b4a57e5 add quiet hours to silence alerts during specific time periods (#265) 2025-11-24 17:35:28 -05:00
4 changed files with 511 additions and 490 deletions

View File

@@ -48,7 +48,8 @@
}, },
"suspicious": { "suspicious": {
"useAwait": "error", "useAwait": "error",
"noEvolvingTypes": "error" "noEvolvingTypes": "error",
"noArrayIndexKey": "off"
} }
} }
}, },

View File

@@ -2,7 +2,7 @@
<html lang="en" dir="ltr"> <html lang="en" dir="ltr">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="manifest" href="./static/manifest.json" /> <link rel="manifest" href="./static/manifest.json" crossorigin="use-credentials" />
<link rel="icon" type="image/svg+xml" href="./static/icon.svg" /> <link rel="icon" type="image/svg+xml" href="./static/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />

View File

@@ -1,7 +1,16 @@
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { MoreHorizontalIcon, PlusIcon, Trash2Icon, ServerIcon, ClockIcon, CalendarIcon, ActivityIcon, PenSquareIcon } from "lucide-react" import {
MoreHorizontalIcon,
PlusIcon,
Trash2Icon,
ServerIcon,
ClockIcon,
CalendarIcon,
ActivityIcon,
PenSquareIcon,
} from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
@@ -15,12 +24,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
@@ -54,7 +58,7 @@ export function QuietHours() {
.then(({ items }) => setData(items)) .then(({ items }) => setData(items))
// Subscribe to changes // Subscribe to changes
; (async () => { ;(async () => {
unsubscribe = await pb.collection("quiet_hours").subscribe( unsubscribe = await pb.collection("quiet_hours").subscribe(
"*", "*",
(e) => { (e) => {
@@ -62,9 +66,7 @@ export function QuietHours() {
setData((current) => [e.record as QuietHoursRecord, ...current]) setData((current) => [e.record as QuietHoursRecord, ...current])
} }
if (e.action === "update") { if (e.action === "update") {
setData((current) => setData((current) => current.map((r) => (r.id === e.record.id ? (e.record as QuietHoursRecord) : r)))
current.map((r) => (r.id === e.record.id ? (e.record as QuietHoursRecord) : r))
)
} }
if (e.action === "delete") { if (e.action === "delete") {
setData((current) => current.filter((r) => r.id !== e.record.id)) setData((current) => current.filter((r) => r.id !== e.record.id))
@@ -102,8 +104,8 @@ export function QuietHours() {
const formatDateTime = (record: QuietHoursRecord) => { const formatDateTime = (record: QuietHoursRecord) => {
if (record.type === "daily") { if (record.type === "daily") {
// For daily windows, show only time // For daily windows, show only time
const startTime = new Date(record.start).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) const startTime = new Date(record.start).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
const endTime = new Date(record.end).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) const endTime = new Date(record.end).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
return `${startTime} - ${endTime}` return `${startTime} - ${endTime}`
} }
// For one-time windows, show full date and time // For one-time windows, show full date and time
@@ -112,7 +114,7 @@ export function QuietHours() {
return `${start} - ${end}` return `${start} - ${end}`
} }
const getWindowState = (record: QuietHoursRecord): "active" | "past" | "future" => { const getWindowState = (record: QuietHoursRecord): "active" | "past" | "inactive" => {
const now = new Date() const now = new Date()
if (record.type === "daily") { if (record.type === "daily") {
@@ -132,9 +134,9 @@ export function QuietHours() {
// Handle cases where window spans midnight // Handle cases where window spans midnight
if (localStartMinutes <= localEndMinutes) { if (localStartMinutes <= localEndMinutes) {
return currentMinutes >= localStartMinutes && currentMinutes < localEndMinutes ? "active" : "future" return currentMinutes >= localStartMinutes && currentMinutes < localEndMinutes ? "active" : "inactive"
} else { } else {
return currentMinutes >= localStartMinutes || currentMinutes < localEndMinutes ? "active" : "future" return currentMinutes >= localStartMinutes || currentMinutes < localEndMinutes ? "active" : "inactive"
} }
} else { } else {
// For one-time windows // For one-time windows
@@ -146,7 +148,7 @@ export function QuietHours() {
} else if (now >= endDate) { } else if (now >= endDate) {
return "past" return "past"
} else { } else {
return "future" return "inactive"
} }
} }
} }
@@ -159,7 +161,9 @@ export function QuietHours() {
<Trans>Quiet hours</Trans> <Trans>Quiet hours</Trans>
</h3> </h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
<Trans>Schedule quiet hours where notifications will not be sent, such as during maintenance periods.</Trans> <Trans>
Schedule quiet hours where notifications will not be sent, such as during maintenance periods.
</Trans>
</p> </p>
</div> </div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@@ -171,12 +175,7 @@ export function QuietHours() {
</span> </span>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<QuietHoursDialog <QuietHoursDialog editingRecord={editingRecord} systems={systems} onClose={closeDialog} toast={toast} />
editingRecord={editingRecord}
systems={systems}
onClose={closeDialog}
toast={toast}
/>
</Dialog> </Dialog>
</div> </div>
{data.length > 0 && ( {data.length > 0 && (
@@ -198,14 +197,14 @@ export function QuietHours() {
</TableHead> </TableHead>
<TableHead className="px-4"> <TableHead className="px-4">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<ActivityIcon className="size-4" /> <CalendarIcon className="size-4" />
<Trans>State</Trans> <Trans>Schedule</Trans>
</span> </span>
</TableHead> </TableHead>
<TableHead className="px-4"> <TableHead className="px-4">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CalendarIcon className="size-4" /> <ActivityIcon className="size-4" />
<Trans>Schedule</Trans> <Trans>State</Trans>
</span> </span>
</TableHead> </TableHead>
<TableHead className="px-4 text-right sr-only"> <TableHead className="px-4 text-right sr-only">
@@ -217,33 +216,31 @@ export function QuietHours() {
{data.map((record) => ( {data.map((record) => (
<TableRow key={record.id}> <TableRow key={record.id}>
<TableCell className="px-4 py-3"> <TableCell className="px-4 py-3">
{record.system ? (record.expand?.system?.name || record.system) : <Trans>All Systems</Trans>} {record.system ? record.expand?.system?.name || record.system : <Trans>All Systems</Trans>}
</TableCell> </TableCell>
<TableCell className="px-4 py-3"> <TableCell className="px-4 py-3">
{record.type === "daily" ? <Trans>Daily</Trans> : <Trans>One-time</Trans>} {record.type === "daily" ? <Trans>Daily</Trans> : <Trans>One-time</Trans>}
</TableCell> </TableCell>
<TableCell className="px-4 py-3">{formatDateTime(record)}</TableCell>
<TableCell className="px-4 py-3"> <TableCell className="px-4 py-3">
{(() => { {(() => {
const state = getWindowState(record) const state = getWindowState(record)
const stateConfig = { const stateConfig = {
active: { label: <Trans>Active</Trans>, variant: "success" as const }, active: { label: <Trans>Active</Trans>, variant: "success" as const },
past: { label: <Trans>Past</Trans>, variant: "danger" as const }, past: { label: <Trans>Past</Trans>, variant: "danger" as const },
future: { label: <Trans>Future</Trans>, variant: "default" as const }, inactive: { label: <Trans>Inactive</Trans>, variant: "default" as const },
} }
const config = stateConfig[state] const config = stateConfig[state]
return ( return <Badge variant={config.variant}>{config.label}</Badge>
<Badge variant={config.variant}>
{config.label}
</Badge>
)
})()} })()}
</TableCell> </TableCell>
<TableCell className="px-4 py-3">{formatDateTime(record)}</TableCell>
<TableCell className="px-4 py-3 text-right"> <TableCell className="px-4 py-3 text-right">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8"> <Button variant="ghost" size="icon" className="size-8">
<span className="sr-only"><Trans>Open menu</Trans></span> <span className="sr-only">
<Trans>Open menu</Trans>
</span>
<MoreHorizontalIcon className="size-4" /> <MoreHorizontalIcon className="size-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -265,8 +262,6 @@ export function QuietHours() {
</Table> </Table>
</div> </div>
)} )}
</> </>
) )
} }
@@ -274,10 +269,10 @@ export function QuietHours() {
// Helper function to format Date as datetime-local string (YYYY-MM-DDTHH:mm) in local time // Helper function to format Date as datetime-local string (YYYY-MM-DDTHH:mm) in local time
function formatDateTimeLocal(date: Date): string { function formatDateTimeLocal(date: Date): string {
const year = date.getFullYear() const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, "0")
const day = String(date.getDate()).padStart(2, '0') const day = String(date.getDate()).padStart(2, "0")
const hours = String(date.getHours()).padStart(2, '0') const hours = String(date.getHours()).padStart(2, "0")
const minutes = String(date.getMinutes()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, "0")
return `${year}-${month}-${day}T${hours}:${minutes}` return `${year}-${month}-${day}T${hours}:${minutes}`
} }
@@ -290,7 +285,7 @@ function QuietHoursDialog({
editingRecord: QuietHoursRecord | null editingRecord: QuietHoursRecord | null
systems: SystemRecord[] systems: SystemRecord[]
onClose: () => void onClose: () => void
toast: any toast: ReturnType<typeof useToast>["toast"]
}) { }) {
const [selectedSystem, setSelectedSystem] = useState(editingRecord?.system || "") const [selectedSystem, setSelectedSystem] = useState(editingRecord?.system || "")
const [isGlobal, setIsGlobal] = useState(!editingRecord?.system) const [isGlobal, setIsGlobal] = useState(!editingRecord?.system)
@@ -395,9 +390,7 @@ function QuietHoursDialog({
return ( return (
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>{editingRecord ? <Trans>Edit Quiet Hours</Trans> : <Trans>Add Quiet Hours</Trans>}</DialogTitle>
{editingRecord ? <Trans>Edit Quiet Hours</Trans> : <Trans>Add Quiet Hours</Trans>}
</DialogTitle>
<DialogDescription> <DialogDescription>
<Trans>Configure quiet hours where notifications will not be sent.</Trans> <Trans>Configure quiet hours where notifications will not be sent.</Trans>
</DialogDescription> </DialogDescription>
@@ -437,7 +430,7 @@ function QuietHoursDialog({
tabIndex={-1} tabIndex={-1}
autoComplete="off" autoComplete="off"
value={selectedSystem} value={selectedSystem}
onChange={() => { }} onChange={() => {}}
required={!isGlobal} required={!isGlobal}
/> />
</div> </div>
@@ -467,7 +460,7 @@ function QuietHoursDialog({
<> <>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="start-datetime"> <Label htmlFor="start-datetime">
<Trans>Start Date & Time</Trans> <Trans>Start Time</Trans>
</Label> </Label>
<Input <Input
id="start-datetime" id="start-datetime"
@@ -481,7 +474,7 @@ function QuietHoursDialog({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="end-datetime"> <Label htmlFor="end-datetime">
<Trans>End Date & Time</Trans> <Trans>End Time</Trans>
</Label> </Label>
<Input <Input
id="end-datetime" id="end-datetime"
@@ -495,12 +488,13 @@ function QuietHoursDialog({
</div> </div>
</> </>
) : ( ) : (
<> <div className="grid gap-2 grid-cols-2">
<div className="grid gap-2"> <div>
<Label htmlFor="start-time"> <Label htmlFor="start-time">
<Trans>Start Time</Trans> <Trans>Start Time</Trans>
</Label> </Label>
<Input <Input
className="tabular-nums tracking-tighter"
id="start-time" id="start-time"
type="time" type="time"
value={startTime} value={startTime}
@@ -508,13 +502,20 @@ function QuietHoursDialog({
required required
/> />
</div> </div>
<div className="grid gap-2"> <div>
<Label htmlFor="end-time"> <Label htmlFor="end-time">
<Trans>End Time</Trans> <Trans>End Time</Trans>
</Label> </Label>
<Input id="end-time" type="time" value={endTime} onChange={(e) => setEndTime(e.target.value)} required /> <Input
className="tabular-nums tracking-tighter"
id="end-time"
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
required
/>
</div>
</div> </div>
</>
)} )}
<DialogFooter> <DialogFooter>

View File

@@ -1,3 +1,4 @@
/** biome-ignore-all lint/correctness/useHookAtTopLevel: <explanation> */
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import { Trans, useLingui } from "@lingui/react/macro" import { Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
@@ -218,7 +219,7 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
}, },
}, },
{ {
accessorFn: ({ info }) => (info.bb || (info.b || 0) * 1024 * 1024) || undefined, accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024 || undefined,
id: "net", id: "net",
name: () => t`Net`, name: () => t`Net`,
size: 0, size: 0,
@@ -291,7 +292,10 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
[STATUS_COLORS[SystemStatus.Up]]: numFailed === 0, [STATUS_COLORS[SystemStatus.Up]]: numFailed === 0,
})} })}
/> />
{totalCount} <span className="text-muted-foreground text-sm -ms-0.5">({t`Failed`.toLowerCase()}: {numFailed})</span> {totalCount}{" "}
<span className="text-muted-foreground text-sm -ms-0.5">
({t`Failed`.toLowerCase()}: {numFailed})
</span>
</span> </span>
) )
}, },
@@ -395,7 +399,6 @@ function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
const { info: sysInfo, status, id } = info.row.original const { info: sysInfo, status, id } = info.row.original
const extraFs = Object.entries(sysInfo.efs ?? {}) const extraFs = Object.entries(sysInfo.efs ?? {})
// No extra disks - show basic meter
if (extraFs.length === 0) { if (extraFs.length === 0) {
return TableCellWithMeter(info) return TableCellWithMeter(info)
} }
@@ -405,10 +408,9 @@ function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
// sort extra disks by percentage descending // sort extra disks by percentage descending
extraFs.sort((a, b) => b[1] - a[1]) extraFs.sort((a, b) => b[1] - a[1])
function getMeterClass(pct: number) { function getIndicatorColor(pct: number) {
const threshold = getMeterState(pct) const threshold = getMeterState(pct)
return cn( return (
"h-full",
(status !== SystemStatus.Up && STATUS_COLORS.paused) || (status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) || (threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) || (threshold === MeterState.Warn && STATUS_COLORS.pending) ||
@@ -416,28 +418,43 @@ function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
) )
} }
function getMeterClass(pct: number) {
return cn("h-full", getIndicatorColor(pct))
}
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Link href={getPagePath($router, "system", { id })} tabIndex={-1} className="flex flex-col gap-0.5 w-full relative z-10"> <Link
href={getPagePath($router, "system", { id })}
tabIndex={-1}
className="flex flex-col gap-0.5 w-full relative z-10"
>
<div className="flex gap-2 items-center tabular-nums tracking-tight"> <div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8 shrink-0">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span> <span className="min-w-8 shrink-0">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden"> <span className="flex-1 min-w-8 flex items-center gap-0.5 px-1 justify-end bg-muted h-[1em] rounded-sm overflow-hidden relative">
{/* Root disk */} {/* Root disk */}
<span className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span> <span
{/* Extra disks */} className={cn("absolute inset-0", getMeterClass(rootDiskPct))}
{extraFs.map(([_name, pct], index) => ( style={{ width: `${rootDiskPct}%` }}
<span key={index} className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span> ></span>
{/* Extra disk indicators */}
{extraFs.map(([name, pct]) => (
<span
key={name}
className={cn("size-1.5 rounded-full shrink-0 outline-[0.5px] outline-muted", getIndicatorColor(pct))}
/>
))} ))}
</span> </span>
</div> </div>
</Link> </Link>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right" className="max-w-xs pb-2"> <TooltipContent side="right" className="max-w-xs pb-2">
<div className="grid gap-1.5"> <div className="grid gap-1">
<div className="grid gap-0.5"> <div className="grid gap-0.5">
<div className="text-[0.65rem] text-muted-foreground uppercase tracking-wide tabular-nums"><Trans context="Root disk label">Root</Trans></div> <div className="text-[0.65rem] text-muted-foreground uppercase tracking-wide tabular-nums">
<Trans context="Root disk label">Root</Trans>
</div>
<div className="flex gap-2 items-center tabular-nums text-xs"> <div className="flex gap-2 items-center tabular-nums text-xs">
<span className="min-w-7">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span> <span className="min-w-7">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden"> <span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden">
@@ -448,7 +465,9 @@ function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
{extraFs.map(([name, pct]) => { {extraFs.map(([name, pct]) => {
return ( return (
<div key={name} className="grid gap-0.5"> <div key={name} className="grid gap-0.5">
<div className="text-[0.65rem] max-w-40 text-muted-foreground uppercase tracking-wide truncate">{name}</div> <div className="text-[0.65rem] max-w-40 text-muted-foreground uppercase tracking-wide truncate">
{name}
</div>
<div className="flex gap-2 items-center tabular-nums text-xs"> <div className="flex gap-2 items-center tabular-nums text-xs">
<span className="min-w-7">{decimalString(pct, pct >= 10 ? 1 : 2)}%</span> <span className="min-w-7">{decimalString(pct, pct >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden"> <span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden">