mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-17 19:16:25 +00:00
Updete AppImage
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
import useSWR from "swr"
|
||||
import { useState, useEffect } from "react"
|
||||
import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware"
|
||||
import { usePollingConfig } from "@/lib/polling-config"
|
||||
|
||||
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
||||
if (!sizeStr) return 0
|
||||
@@ -163,14 +164,38 @@ const groupAndSortTemperatures = (temperatures: any[]) => {
|
||||
}
|
||||
|
||||
export default function Hardware() {
|
||||
const { intervals } = usePollingConfig()
|
||||
|
||||
// Static data (loaded once on mount): system info, memory, PCI, network/storage summaries
|
||||
const {
|
||||
data: hardwareData,
|
||||
error,
|
||||
isLoading,
|
||||
data: staticHardwareData,
|
||||
error: staticError,
|
||||
isLoading: staticLoading,
|
||||
} = useSWR<HardwareData>("/api/hardware", fetcher, {
|
||||
refreshInterval: 5000,
|
||||
refreshInterval: 0, // Never refresh automatically - only load once
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
})
|
||||
|
||||
// Dynamic data (temperatures only) - polls at configured interval
|
||||
const { data: temperatureData, error: tempError } = useSWR<{ temperatures: any[] }>(
|
||||
"/api/hardware/temperatures",
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: intervals.hardware,
|
||||
},
|
||||
)
|
||||
|
||||
const hardwareData = staticHardwareData
|
||||
? {
|
||||
...staticHardwareData,
|
||||
temperatures: temperatureData?.temperatures || staticHardwareData.temperatures || [],
|
||||
}
|
||||
: null
|
||||
|
||||
const error = staticError || tempError
|
||||
const isLoading = staticLoading
|
||||
|
||||
useEffect(() => {
|
||||
if (hardwareData?.storage_devices) {
|
||||
console.log("[v0] Storage devices data from backend:", hardwareData.storage_devices)
|
||||
|
||||
@@ -73,6 +73,10 @@ interface NetworkInterface {
|
||||
vm_status?: string
|
||||
}
|
||||
|
||||
interface NetworkMetricsProps {
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
const getInterfaceTypeBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case "physical":
|
||||
@@ -143,15 +147,16 @@ const fetcher = async (url: string): Promise<NetworkData> => {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export function NetworkMetrics() {
|
||||
export function NetworkMetrics({ isActive = true }: NetworkMetricsProps) {
|
||||
const {
|
||||
data: networkData,
|
||||
error,
|
||||
isLoading,
|
||||
} = useSWR<NetworkData>("/api/network", fetcher, {
|
||||
refreshInterval: 60000, // Refresh every 60 seconds
|
||||
refreshInterval: isActive ? 60000 : 0, // Refresh every 60 seconds only if active, otherwise pause
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: true,
|
||||
isPaused: () => !isActive,
|
||||
})
|
||||
|
||||
const [selectedInterface, setSelectedInterface] = useState<NetworkInterface | null>(null)
|
||||
@@ -166,10 +171,15 @@ export function NetworkMetrics() {
|
||||
revalidateOnReconnect: true,
|
||||
})
|
||||
|
||||
const { data: interfaceHistoricalData } = useSWR<any>(`/api/node/metrics?timeframe=${timeframe}`, fetcher, {
|
||||
refreshInterval: 30000,
|
||||
revalidateOnFocus: false,
|
||||
})
|
||||
const { data: interfaceHistoricalData } = useSWR<any>(
|
||||
isActive ? `/api/node/metrics?timeframe=${timeframe}` : null,
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: isActive ? 30000 : 0,
|
||||
revalidateOnFocus: false,
|
||||
isPaused: () => !isActive,
|
||||
},
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
@@ -16,6 +16,7 @@ interface NetworkTrafficChartProps {
|
||||
interfaceName?: string
|
||||
onTotalsCalculated?: (totals: { received: number; sent: number }) => void
|
||||
refreshInterval?: number // En milisegundos, por defecto 60000 (60 segundos)
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
|
||||
@@ -43,6 +44,7 @@ export function NetworkTrafficChart({
|
||||
interfaceName,
|
||||
onTotalsCalculated,
|
||||
refreshInterval = 60000,
|
||||
isActive = true,
|
||||
}: NetworkTrafficChartProps) {
|
||||
const [data, setData] = useState<NetworkMetricsData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -59,16 +61,20 @@ export function NetworkTrafficChart({
|
||||
}, [timeframe, interfaceName])
|
||||
|
||||
useEffect(() => {
|
||||
if (refreshInterval > 0) {
|
||||
if (refreshInterval > 0 && isActive) {
|
||||
const interval = setInterval(() => {
|
||||
fetchMetrics()
|
||||
}, refreshInterval)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [timeframe, interfaceName, refreshInterval])
|
||||
}, [timeframe, interfaceName, refreshInterval, isActive])
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
if (!isActive) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isInitialLoad) {
|
||||
setLoading(true)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,12 @@ export function NodeMetricsCharts() {
|
||||
useEffect(() => {
|
||||
console.log("[v0] NodeMetricsCharts component mounted")
|
||||
fetchMetrics()
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchMetrics()
|
||||
}, 60000) // 60 seconds instead of 30 to reduce server load
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [timeframe])
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
|
||||
@@ -13,6 +13,9 @@ import { SystemLogs } from "./system-logs"
|
||||
import { OnboardingCarousel } from "./onboarding-carousel"
|
||||
import { HealthStatusModal } from "./health-status-modal"
|
||||
import { getApiUrl } from "../lib/api-config"
|
||||
import { usePollingConfig, INTERVAL_OPTIONS } from "@/lib/polling-config"
|
||||
import { Label } from "./ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||
import {
|
||||
RefreshCw,
|
||||
AlertTriangle,
|
||||
@@ -26,6 +29,7 @@ import {
|
||||
Box,
|
||||
Cpu,
|
||||
FileText,
|
||||
Settings,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { ThemeToggle } from "./theme-toggle"
|
||||
@@ -65,6 +69,7 @@ export function ProxmoxDashboard() {
|
||||
const [showNavigation, setShowNavigation] = useState(true)
|
||||
const [lastScrollY, setLastScrollY] = useState(0)
|
||||
const [showHealthModal, setShowHealthModal] = useState(false)
|
||||
const { intervals, updateInterval } = usePollingConfig()
|
||||
|
||||
const fetchSystemData = useCallback(async () => {
|
||||
console.log("[v0] Fetching system data from Flask server...")
|
||||
@@ -127,8 +132,12 @@ export function ProxmoxDashboard() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch if we actually need the data (always for header)
|
||||
fetchSystemData()
|
||||
const interval = setInterval(fetchSystemData, 10000)
|
||||
|
||||
// Poll every 30 seconds for header info (less frequent since it's just for display)
|
||||
const interval = setInterval(fetchSystemData, 30000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchSystemData])
|
||||
|
||||
@@ -216,6 +225,8 @@ export function ProxmoxDashboard() {
|
||||
return "Hardware"
|
||||
case "logs":
|
||||
return "System Logs"
|
||||
case "settings":
|
||||
return "Settings"
|
||||
default:
|
||||
return "Navigation Menu"
|
||||
}
|
||||
@@ -362,7 +373,7 @@ export function ProxmoxDashboard() {
|
||||
>
|
||||
<div className="container mx-auto px-4 md:px-6 pt-4 md:pt-6">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-0">
|
||||
<TabsList className="hidden md:grid w-full grid-cols-6 bg-card border border-border">
|
||||
<TabsList className="hidden md:grid w-full grid-cols-7 bg-card border border-border">
|
||||
<TabsTrigger
|
||||
value="overview"
|
||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||
@@ -399,6 +410,12 @@ export function ProxmoxDashboard() {
|
||||
>
|
||||
System Logs
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="settings"
|
||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||
>
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
||||
@@ -507,6 +524,21 @@ export function ProxmoxDashboard() {
|
||||
<FileText className="h-5 w-5" />
|
||||
<span>System Logs</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab("settings")
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full justify-start gap-3 ${
|
||||
activeTab === "settings"
|
||||
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
<span>Settings</span>
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
@@ -517,7 +549,7 @@ export function ProxmoxDashboard() {
|
||||
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4 md:space-y-6">
|
||||
<TabsContent value="overview" className="space-y-4 md:space-y-6 mt-0">
|
||||
<SystemOverview key={`overview-${componentKey}`} />
|
||||
<SystemOverview key={`overview-${componentKey}`} isActive={activeTab === "overview"} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="storage" className="space-y-4 md:space-y-6 mt-0">
|
||||
@@ -525,7 +557,7 @@ export function ProxmoxDashboard() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="network" className="space-y-4 md:space-y-6 mt-0">
|
||||
<NetworkMetrics key={`network-${componentKey}`} />
|
||||
<NetworkMetrics key={`network-${componentKey}`} isActive={activeTab === "network"} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="vms" className="space-y-4 md:space-y-6 mt-0">
|
||||
@@ -539,6 +571,183 @@ export function ProxmoxDashboard() {
|
||||
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
|
||||
<SystemLogs key={`logs-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings" className="space-y-4 md:space-y-6 mt-0">
|
||||
<div className="grid gap-4 md:gap-6">
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||
<Settings className="h-6 w-6" />
|
||||
Settings
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Configure your ProxMenux Monitor preferences and system settings.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="border-t border-border pt-4">
|
||||
<h3 className="text-lg font-medium mb-2">Appearance</h3>
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<div>
|
||||
<p className="font-medium">Theme</p>
|
||||
<p className="text-sm text-muted-foreground">Choose your preferred color scheme</p>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-4">
|
||||
<h3 className="text-lg font-medium mb-2">System Information</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between py-2">
|
||||
<span className="text-muted-foreground">Version:</span>
|
||||
<span className="font-medium">ProxMenux Monitor v1.0.0</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
<span className="text-muted-foreground">Server:</span>
|
||||
<span className="font-medium">{systemStatus.serverName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
<span className="text-muted-foreground">Node ID:</span>
|
||||
<span className="font-medium">{systemStatus.nodeId}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
<span className="text-muted-foreground">Status:</span>
|
||||
<Badge variant="outline" className={statusColor}>
|
||||
{statusIcon}
|
||||
<span className="ml-1 capitalize">{systemStatus.status}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-4">
|
||||
<h3 className="text-lg font-medium mb-2">Polling Intervals</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Configure how frequently each section updates its data. Lower values provide more real-time data
|
||||
but increase server load.
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<Label htmlFor="storage-interval" className="font-medium">
|
||||
Storage
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">Update frequency for storage metrics</p>
|
||||
</div>
|
||||
<Select
|
||||
value={intervals.storage.toString()}
|
||||
onValueChange={(value) => updateInterval("storage", Number.parseInt(value))}
|
||||
>
|
||||
<SelectTrigger id="storage-interval" className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INTERVAL_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<Label htmlFor="network-interval" className="font-medium">
|
||||
Network
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">Update frequency for network metrics</p>
|
||||
</div>
|
||||
<Select
|
||||
value={intervals.network.toString()}
|
||||
onValueChange={(value) => updateInterval("network", Number.parseInt(value))}
|
||||
>
|
||||
<SelectTrigger id="network-interval" className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INTERVAL_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<Label htmlFor="vms-interval" className="font-medium">
|
||||
VMs & LXCs
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">Update frequency for VM/LXC data</p>
|
||||
</div>
|
||||
<Select
|
||||
value={intervals.vms.toString()}
|
||||
onValueChange={(value) => updateInterval("vms", Number.parseInt(value))}
|
||||
>
|
||||
<SelectTrigger id="vms-interval" className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INTERVAL_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<Label htmlFor="hardware-interval" className="font-medium">
|
||||
Hardware
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">Update frequency for temperature sensors only</p>
|
||||
</div>
|
||||
<Select
|
||||
value={intervals.hardware.toString()}
|
||||
onValueChange={(value) => updateInterval("hardware", Number.parseInt(value))}
|
||||
>
|
||||
<SelectTrigger id="hardware-interval" className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INTERVAL_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-xs text-muted-foreground bg-blue-500/10 border border-blue-500/20 rounded-lg p-3">
|
||||
<strong>Note:</strong> Hardware static information (System Info, Memory Modules, PCI Devices,
|
||||
Network/Storage Summaries) is loaded only once when entering the Hardware page and does not
|
||||
refresh automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-4">
|
||||
<h3 className="text-lg font-medium mb-2">About</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
ProxMenux Monitor is a comprehensive dashboard for monitoring and managing Proxmox VE systems.
|
||||
</p>
|
||||
<a
|
||||
href="https://ko-fi.com/macrimi"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center text-sm text-blue-500 hover:text-blue-600 hover:underline transition-colors"
|
||||
>
|
||||
Support and contribute to the project →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
Menu,
|
||||
Terminal,
|
||||
} from "lucide-react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState, useEffect, useMemo } from "react"
|
||||
|
||||
interface Log {
|
||||
timestamp: string
|
||||
@@ -433,22 +433,35 @@ export function SystemLogs() {
|
||||
return String(value).toLowerCase()
|
||||
}
|
||||
|
||||
const logsOnly: CombinedLogEntry[] = logs
|
||||
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
|
||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
|
||||
const memoizedLogs = useMemo(() => logs, [logs])
|
||||
const memoizedEvents = useMemo(() => events, [events])
|
||||
const memoizedBackups = useMemo(() => backups, [backups])
|
||||
const memoizedNotifications = useMemo(() => notifications, [notifications])
|
||||
|
||||
const eventsOnly: CombinedLogEntry[] = events
|
||||
.map((event) => ({
|
||||
timestamp: event.starttime,
|
||||
level: event.level,
|
||||
service: event.type,
|
||||
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
|
||||
source: `Node: ${event.node} • User: ${event.user}`,
|
||||
isEvent: true,
|
||||
eventData: event,
|
||||
sortTimestamp: new Date(event.starttime).getTime(),
|
||||
}))
|
||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
|
||||
const logsOnly: CombinedLogEntry[] = useMemo(
|
||||
() =>
|
||||
memoizedLogs
|
||||
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
|
||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||
[memoizedLogs],
|
||||
)
|
||||
|
||||
const eventsOnly: CombinedLogEntry[] = useMemo(
|
||||
() =>
|
||||
memoizedEvents
|
||||
.map((event) => ({
|
||||
timestamp: event.starttime,
|
||||
level: event.level,
|
||||
service: event.type,
|
||||
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
|
||||
source: `Node: ${event.node} • User: ${event.user}`,
|
||||
isEvent: true,
|
||||
eventData: event,
|
||||
sortTimestamp: new Date(event.starttime).getTime(),
|
||||
}))
|
||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||
[memoizedEvents],
|
||||
)
|
||||
|
||||
const filteredLogsOnly = logsOnly.filter((log) => {
|
||||
const message = log.message || ""
|
||||
@@ -479,32 +492,40 @@ export function SystemLogs() {
|
||||
const displayedLogsOnly = filteredLogsOnly.slice(0, displayedLogsCount)
|
||||
const displayedEventsOnly = filteredEventsOnly.slice(0, displayedLogsCount)
|
||||
|
||||
const combinedLogs: CombinedLogEntry[] = [
|
||||
...logs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
|
||||
...events.map((event) => ({
|
||||
timestamp: event.starttime,
|
||||
level: event.level,
|
||||
service: event.type,
|
||||
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
|
||||
source: `Node: ${event.node} • User: ${event.user}`,
|
||||
isEvent: true,
|
||||
eventData: event,
|
||||
sortTimestamp: new Date(event.starttime).getTime(),
|
||||
})),
|
||||
].sort((a, b) => b.sortTimestamp - a.sortTimestamp) // Sort by timestamp descending
|
||||
const combinedLogs: CombinedLogEntry[] = useMemo(
|
||||
() =>
|
||||
[
|
||||
...memoizedLogs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
|
||||
...memoizedEvents.map((event) => ({
|
||||
timestamp: event.starttime,
|
||||
level: event.level,
|
||||
service: event.type,
|
||||
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
|
||||
source: `Node: ${event.node} • User: ${event.user}`,
|
||||
isEvent: true,
|
||||
eventData: event,
|
||||
sortTimestamp: new Date(event.starttime).getTime(),
|
||||
})),
|
||||
].sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||
[memoizedLogs, memoizedEvents],
|
||||
)
|
||||
|
||||
const filteredCombinedLogs = combinedLogs.filter((log) => {
|
||||
const message = log.message || ""
|
||||
const service = log.service || ""
|
||||
const searchTermLower = safeToLowerCase(searchTerm)
|
||||
const filteredCombinedLogs = useMemo(
|
||||
() =>
|
||||
combinedLogs.filter((log) => {
|
||||
const message = log.message || ""
|
||||
const service = log.service || ""
|
||||
const searchTermLower = safeToLowerCase(searchTerm)
|
||||
|
||||
const matchesSearch =
|
||||
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
|
||||
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
||||
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
||||
const matchesSearch =
|
||||
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
|
||||
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
||||
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
||||
|
||||
return matchesSearch && matchesLevel && matchesService
|
||||
})
|
||||
return matchesSearch && matchesLevel && matchesService
|
||||
}),
|
||||
[combinedLogs, searchTerm, levelFilter, serviceFilter],
|
||||
)
|
||||
|
||||
// CHANGE: Re-assigning displayedLogs to use the filteredCombinedLogs
|
||||
const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount)
|
||||
@@ -605,7 +626,7 @@ export function SystemLogs() {
|
||||
info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length,
|
||||
}
|
||||
|
||||
const uniqueServices = [...new Set(logs.map((log) => log.service))]
|
||||
const uniqueServices = useMemo(() => [...new Set(memoizedLogs.map((log) => log.service))], [memoizedLogs])
|
||||
|
||||
const getBackupType = (volid: string): "vm" | "lxc" => {
|
||||
if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) {
|
||||
@@ -930,9 +951,11 @@ export function SystemLogs() {
|
||||
<SelectValue placeholder="Filter by service" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Services</SelectItem>
|
||||
{uniqueServices.slice(0, 20).map((service) => (
|
||||
<SelectItem key={service} value={service}>
|
||||
<SelectItem key="service-all" value="all">
|
||||
All Services
|
||||
</SelectItem>
|
||||
{uniqueServices.slice(0, 20).map((service, idx) => (
|
||||
<SelectItem key={`service-${service}-${idx}`} value={service}>
|
||||
{service}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -947,51 +970,59 @@ export function SystemLogs() {
|
||||
|
||||
<ScrollArea className="h-[600px] w-full rounded-md border border-border overflow-x-hidden">
|
||||
<div className="space-y-2 p-4 w-full box-border">
|
||||
{displayedLogs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden box-border"
|
||||
onClick={() => {
|
||||
if (log.eventData) {
|
||||
setSelectedEvent(log.eventData)
|
||||
setIsEventModalOpen(true)
|
||||
} else {
|
||||
setSelectedLog(log as SystemLog)
|
||||
setIsLogModalOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0 flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className={getLevelColor(log.level)}>
|
||||
{getLevelIcon(log.level)}
|
||||
{log.level.toUpperCase()}
|
||||
</Badge>
|
||||
{log.eventData && (
|
||||
<Badge variant="outline" className="bg-purple-500/10 text-purple-500 border-purple-500/20">
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
EVENT
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{displayedLogs.map((log, index) => {
|
||||
// Generate a more stable unique key
|
||||
const timestampMs = new Date(log.timestamp).getTime()
|
||||
const uniqueKey = log.eventData
|
||||
? `event-${log.eventData.upid.replace(/:/g, "-")}-${timestampMs}`
|
||||
: `log-${timestampMs}-${log.service?.substring(0, 10) || "unknown"}-${log.pid || "nopid"}-${index}`
|
||||
|
||||
<div className="flex-1 min-w-0 overflow-hidden box-border">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
|
||||
<div className="text-sm font-medium text-foreground truncate min-w-0">{log.service}</div>
|
||||
<div className="text-xs text-muted-foreground font-mono truncate sm:ml-2 sm:flex-shrink-0">
|
||||
{log.timestamp}
|
||||
return (
|
||||
<div
|
||||
key={uniqueKey}
|
||||
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden box-border"
|
||||
onClick={() => {
|
||||
if (log.eventData) {
|
||||
setSelectedEvent(log.eventData)
|
||||
setIsEventModalOpen(true)
|
||||
} else {
|
||||
setSelectedLog(log as SystemLog)
|
||||
setIsLogModalOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0 flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className={getLevelColor(log.level)}>
|
||||
{getLevelIcon(log.level)}
|
||||
{log.level.toUpperCase()}
|
||||
</Badge>
|
||||
{log.eventData && (
|
||||
<Badge variant="outline" className="bg-purple-500/10 text-purple-500 border-purple-500/20">
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
EVENT
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 overflow-hidden box-border">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
|
||||
<div className="text-sm font-medium text-foreground truncate min-w-0">{log.service}</div>
|
||||
<div className="text-xs text-muted-foreground font-mono truncate sm:ml-2 sm:flex-shrink-0">
|
||||
{log.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
|
||||
{log.message}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate break-all overflow-hidden">
|
||||
{log.source}
|
||||
{log.pid && ` • PID: ${log.pid}`}
|
||||
{log.hostname && ` • Host: ${log.hostname}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
|
||||
{log.message}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate break-all overflow-hidden">
|
||||
{log.source}
|
||||
{log.pid && ` • PID: ${log.pid}`}
|
||||
{log.hostname && ` • Host: ${log.hostname}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
|
||||
{displayedLogs.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
@@ -1052,44 +1083,48 @@ export function SystemLogs() {
|
||||
|
||||
<ScrollArea className="h-[500px] w-full rounded-md border border-border">
|
||||
<div className="space-y-2 p-4">
|
||||
{backups.map((backup, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedBackup(backup)
|
||||
setIsBackupModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<HardDrive className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
{memoizedBackups.map((backup, index) => {
|
||||
const uniqueKey = `backup-${backup.volid.replace(/[/:]/g, "-")}-${backup.timestamp || index}`
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1 gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="outline" className={getBackupTypeColor(backup.volid)}>
|
||||
{getBackupTypeLabel(backup.volid)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className={getBackupStorageColor(backup.volid)}>
|
||||
{getBackupStorageLabel(backup.volid)}
|
||||
return (
|
||||
<div
|
||||
key={uniqueKey}
|
||||
className="flex items-start space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedBackup(backup)
|
||||
setIsBackupModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<HardDrive className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1 gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="outline" className={getBackupTypeColor(backup.volid)}>
|
||||
{getBackupTypeLabel(backup.volid)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className={getBackupStorageColor(backup.volid)}>
|
||||
{getBackupStorageLabel(backup.volid)}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-green-500/10 text-green-500 border-green-500/20 whitespace-nowrap"
|
||||
>
|
||||
{backup.size_human}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-green-500/10 text-green-500 border-green-500/20 whitespace-nowrap"
|
||||
>
|
||||
{backup.size_human}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mb-1 truncate">Storage: {backup.storage}</div>
|
||||
<div className="text-xs text-muted-foreground flex items-center">
|
||||
<Calendar className="h-3 w-3 mr-1 flex-shrink-0" />
|
||||
<span className="truncate">{backup.created}</span>
|
||||
<div className="text-xs text-muted-foreground mb-1 truncate">Storage: {backup.storage}</div>
|
||||
<div className="text-xs text-muted-foreground flex items-center">
|
||||
<Calendar className="h-3 w-3 mr-1 flex-shrink-0" />
|
||||
<span className="truncate">{backup.created}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
|
||||
{backups.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
@@ -1105,42 +1140,47 @@ export function SystemLogs() {
|
||||
<TabsContent value="notifications" className="space-y-4">
|
||||
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
|
||||
<div className="space-y-2 p-4">
|
||||
{notifications.map((notification, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden w-full"
|
||||
onClick={() => {
|
||||
setSelectedNotification(notification)
|
||||
setIsNotificationModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0 flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className={getNotificationTypeColor(notification.type)}>
|
||||
{notification.type.toUpperCase()}
|
||||
</Badge>
|
||||
<Badge variant="outline" className={getNotificationSourceColor(notification.source)}>
|
||||
{notification.source === "task-log" && <Activity className="h-3 w-3 mr-1" />}
|
||||
{notification.source === "journal" && <FileText className="h-3 w-3 mr-1" />}
|
||||
{notification.source.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
{memoizedNotifications.map((notification, index) => {
|
||||
const timestampMs = new Date(notification.timestamp).getTime()
|
||||
const uniqueKey = `notification-${timestampMs}-${notification.service?.substring(0, 10) || "unknown"}-${notification.source?.substring(0, 10) || "unknown"}-${index}`
|
||||
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
|
||||
<div className="text-sm font-medium text-foreground truncate">{notification.service}</div>
|
||||
<div className="text-xs text-muted-foreground font-mono truncate">
|
||||
{notification.timestamp}
|
||||
return (
|
||||
<div
|
||||
key={uniqueKey}
|
||||
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden w-full"
|
||||
onClick={() => {
|
||||
setSelectedNotification(notification)
|
||||
setIsNotificationModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0 flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className={getNotificationTypeColor(notification.type)}>
|
||||
{notification.type.toUpperCase()}
|
||||
</Badge>
|
||||
<Badge variant="outline" className={getNotificationSourceColor(notification.source)}>
|
||||
{notification.source === "task-log" && <Activity className="h-3 w-3 mr-1" />}
|
||||
{notification.source === "journal" && <FileText className="h-3 w-3 mr-1" />}
|
||||
{notification.source.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
|
||||
<div className="text-sm font-medium text-foreground truncate">{notification.service}</div>
|
||||
<div className="text-xs text-muted-foreground font-mono truncate">
|
||||
{notification.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
|
||||
{notification.message}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground break-words overflow-hidden">
|
||||
Service: {notification.service} • Source: {notification.source}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
|
||||
{notification.message}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground break-words overflow-hidden">
|
||||
Service: {notification.service} • Source: {notification.source}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
|
||||
{notifications.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
|
||||
@@ -96,6 +96,10 @@ interface ProxmoxStorageData {
|
||||
}>
|
||||
}
|
||||
|
||||
interface SystemOverviewProps {
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
const fetchSystemData = async (): Promise<SystemData | null> => {
|
||||
try {
|
||||
const apiUrl = getApiUrl("/api/system")
|
||||
@@ -219,7 +223,7 @@ const fetchProxmoxStorageData = async (): Promise<ProxmoxStorageData | null> =>
|
||||
}
|
||||
}
|
||||
|
||||
export function SystemOverview() {
|
||||
export function SystemOverview({ isActive = true }: SystemOverviewProps) {
|
||||
const [systemData, setSystemData] = useState<SystemData | null>(null)
|
||||
const [vmData, setVmData] = useState<VMData[]>([])
|
||||
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
||||
@@ -231,6 +235,10 @@ export function SystemOverview() {
|
||||
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
@@ -253,6 +261,7 @@ export function SystemOverview() {
|
||||
}
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
fetchData()
|
||||
|
||||
const systemInterval = setInterval(() => {
|
||||
@@ -261,10 +270,11 @@ export function SystemOverview() {
|
||||
})
|
||||
}, 10000)
|
||||
|
||||
// Clean up interval when component unmounts or becomes inactive
|
||||
return () => {
|
||||
clearInterval(systemInterval)
|
||||
}
|
||||
}, [])
|
||||
}, [isActive]) // Add isActive as dependency
|
||||
|
||||
useEffect(() => {
|
||||
const fetchVMs = async () => {
|
||||
@@ -721,7 +731,11 @@ export function SystemOverview() {
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-border">
|
||||
<NetworkTrafficChart timeframe={networkTimeframe} onTotalsCalculated={setNetworkTotals} />
|
||||
<NetworkTrafficChart
|
||||
timeframe={networkTimeframe}
|
||||
onTotalsCalculated={setNetworkTotals}
|
||||
isActive={isActive}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -350,7 +350,7 @@ export function VirtualMachines() {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ action }),
|
||||
body: JSON.JSON.stringify({ action }),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
@@ -451,7 +451,7 @@ export function VirtualMachines() {
|
||||
"/api/system",
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: 30000,
|
||||
refreshInterval: 60000, // Changed from 30s to 60s
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
)
|
||||
@@ -1275,7 +1275,7 @@ export function VirtualMachines() {
|
||||
{/* Real IPs (green, without "Real" label) */}
|
||||
{vmDetails.lxc_ip_info.real_ips.map((ip, index) => (
|
||||
<Badge
|
||||
key={`real-${index}`}
|
||||
key={`ip-real-${selectedVM.vmid}-${ip}-${index}`}
|
||||
variant="outline"
|
||||
className="bg-green-500/10 text-green-500 border-green-500/20"
|
||||
>
|
||||
@@ -1285,7 +1285,7 @@ export function VirtualMachines() {
|
||||
{/* Docker bridge IPs (yellow, with "Bridge" label) */}
|
||||
{vmDetails.lxc_ip_info.docker_ips.map((ip, index) => (
|
||||
<Badge
|
||||
key={`docker-${index}`}
|
||||
key={`ip-docker-${selectedVM.vmid}-${ip}-${index}`}
|
||||
variant="outline"
|
||||
className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
||||
>
|
||||
@@ -1403,7 +1403,7 @@ export function VirtualMachines() {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{vmDetails.hardware_info.gpu_passthrough.map((gpu, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
key={`gpu-${selectedVM.vmid}-${index}-${gpu.substring(0, 20)}`}
|
||||
variant="outline"
|
||||
className={
|
||||
gpu.includes("NVIDIA")
|
||||
@@ -1426,7 +1426,7 @@ export function VirtualMachines() {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{vmDetails.hardware_info.devices.map((device, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
key={`device-${selectedVM.vmid}-${index}-${device.substring(0, 20)}`}
|
||||
variant="outline"
|
||||
className="bg-blue-500/10 text-blue-500 border-blue-500/20"
|
||||
>
|
||||
|
||||
74
AppImage/lib/polling-config.tsx
Normal file
74
AppImage/lib/polling-config.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
|
||||
|
||||
export interface PollingIntervals {
|
||||
storage: number
|
||||
network: number
|
||||
vms: number
|
||||
hardware: number
|
||||
}
|
||||
|
||||
// Default intervals in milliseconds
|
||||
const DEFAULT_INTERVALS: PollingIntervals = {
|
||||
storage: 60000, // 60 seconds
|
||||
network: 60000, // 60 seconds
|
||||
vms: 30000, // 30 seconds
|
||||
hardware: 60000, // 60 seconds
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "proxmenux_polling_intervals"
|
||||
|
||||
interface PollingConfigContextType {
|
||||
intervals: PollingIntervals
|
||||
updateInterval: (key: keyof PollingIntervals, value: number) => void
|
||||
}
|
||||
|
||||
const PollingConfigContext = createContext<PollingConfigContextType | undefined>(undefined)
|
||||
|
||||
export function PollingConfigProvider({ children }: { children: ReactNode }) {
|
||||
const [intervals, setIntervals] = useState<PollingIntervals>(DEFAULT_INTERVALS)
|
||||
|
||||
// Load from localStorage on mount
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored)
|
||||
setIntervals({ ...DEFAULT_INTERVALS, ...parsed })
|
||||
} catch (e) {
|
||||
console.error("[v0] Failed to parse stored polling intervals:", e)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const updateInterval = (key: keyof PollingIntervals, value: number) => {
|
||||
setIntervals((prev) => {
|
||||
const newIntervals = { ...prev, [key]: value }
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newIntervals))
|
||||
return newIntervals
|
||||
})
|
||||
}
|
||||
|
||||
return <PollingConfigContext.Provider value={{ intervals, updateInterval }}>{children}</PollingConfigContext.Provider>
|
||||
}
|
||||
|
||||
export function usePollingConfig() {
|
||||
const context = useContext(PollingConfigContext)
|
||||
if (!context) {
|
||||
throw new Error("usePollingConfig must be used within PollingConfigProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// Interval options for the UI (in milliseconds)
|
||||
export const INTERVAL_OPTIONS = [
|
||||
{ label: "10 seconds", value: 10000 },
|
||||
{ label: "30 seconds", value: 30000 },
|
||||
{ label: "1 minute", value: 60000 },
|
||||
{ label: "2 minutes", value: 120000 },
|
||||
{ label: "5 minutes", value: 300000 },
|
||||
{ label: "10 minutes", value: 600000 },
|
||||
{ label: "30 minutes", value: 1800000 },
|
||||
{ label: "1 hour", value: 3600000 },
|
||||
]
|
||||
Reference in New Issue
Block a user