mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 08:56:21 +00:00
update storage settings
This commit is contained in:
@@ -1426,13 +1426,34 @@ export function NotificationSettings() {
|
||||
{config.ai_provider === "ollama" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-foreground/80">Ollama URL</Label>
|
||||
<Input
|
||||
className="h-9 text-sm font-mono"
|
||||
placeholder="http://localhost:11434"
|
||||
value={config.ai_ollama_url}
|
||||
onChange={e => updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))}
|
||||
disabled={!editMode}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
className="h-9 text-sm font-mono flex-1"
|
||||
placeholder="http://localhost:11434"
|
||||
value={config.ai_ollama_url}
|
||||
onChange={e => updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))}
|
||||
disabled={!editMode}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 px-3 shrink-0"
|
||||
onClick={() => fetchOllamaModels(config.ai_ollama_url)}
|
||||
disabled={!editMode || loadingOllamaModels || !config.ai_ollama_url}
|
||||
>
|
||||
{loadingOllamaModels ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
Load
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{ollamaModels.length > 0 && (
|
||||
<p className="text-xs text-green-500">{ollamaModels.length} models found</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1495,38 +1516,28 @@ export function NotificationSettings() {
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-foreground/80">Model</Label>
|
||||
{config.ai_provider === "ollama" ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={config.ai_model || ""}
|
||||
onValueChange={v => updateConfig(p => ({ ...p, ai_model: v }))}
|
||||
disabled={!editMode || loadingOllamaModels}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm font-mono flex-1">
|
||||
<SelectValue placeholder={loadingOllamaModels ? "Loading models..." : "Select model"}>
|
||||
{config.ai_model || (loadingOllamaModels ? "Loading..." : "Select model")}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ollamaModels.length > 0 ? (
|
||||
ollamaModels.map(m => (
|
||||
<SelectItem key={m} value={m} className="font-mono">{m}</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="_none" disabled className="text-muted-foreground">
|
||||
{loadingOllamaModels ? "Loading models..." : "No models found"}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<button
|
||||
onClick={() => fetchOllamaModels(config.ai_ollama_url)}
|
||||
disabled={!editMode || loadingOllamaModels}
|
||||
className="h-9 w-9 flex items-center justify-center rounded-md border border-border hover:bg-muted transition-colors shrink-0 disabled:opacity-50"
|
||||
title="Refresh models"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loadingOllamaModels ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
<Select
|
||||
value={config.ai_model || ""}
|
||||
onValueChange={v => updateConfig(p => ({ ...p, ai_model: v }))}
|
||||
disabled={!editMode || loadingOllamaModels || ollamaModels.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm font-mono">
|
||||
<SelectValue placeholder={ollamaModels.length === 0 ? "Click 'Load' to fetch models" : "Select model"}>
|
||||
{config.ai_model || (ollamaModels.length === 0 ? "Click 'Load' to fetch models" : "Select model")}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ollamaModels.length > 0 ? (
|
||||
ollamaModels.map(m => (
|
||||
<SelectItem key={m} value={m} className="font-mono">{m}</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="_none" disabled className="text-muted-foreground">
|
||||
No models loaded - click Load button
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="h-9 px-3 flex items-center rounded-md border border-border bg-muted/50 text-sm font-mono text-muted-foreground">
|
||||
{AI_PROVIDERS.find(p => p.value === config.ai_provider)?.model || "default"}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check } from "lucide-react"
|
||||
import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff } from "lucide-react"
|
||||
import { NotificationSettings } from "./notification-settings"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||
import { Switch } from "./ui/switch"
|
||||
import { Input } from "./ui/input"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { getNetworkUnit } from "../lib/format-network"
|
||||
@@ -47,6 +48,20 @@ interface ProxMenuxTool {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface RemoteStorage {
|
||||
name: string
|
||||
type: string
|
||||
status: string
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
percent: number
|
||||
exclude_health: boolean
|
||||
exclude_notifications: boolean
|
||||
excluded_at?: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export function Settings() {
|
||||
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
|
||||
const [loadingTools, setLoadingTools] = useState(true)
|
||||
@@ -61,11 +76,17 @@ export function Settings() {
|
||||
const [savedAllHealth, setSavedAllHealth] = useState(false)
|
||||
const [pendingChanges, setPendingChanges] = useState<Record<string, number>>({})
|
||||
const [customValues, setCustomValues] = useState<Record<string, string>>({})
|
||||
|
||||
// Remote Storage Exclusions
|
||||
const [remoteStorages, setRemoteStorages] = useState<RemoteStorage[]>([])
|
||||
const [loadingStorages, setLoadingStorages] = useState(true)
|
||||
const [savingStorage, setSavingStorage] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadProxmenuxTools()
|
||||
getUnitsSettings()
|
||||
loadHealthSettings()
|
||||
loadRemoteStorages()
|
||||
}, [])
|
||||
|
||||
const loadProxmenuxTools = async () => {
|
||||
@@ -114,6 +135,53 @@ export function Settings() {
|
||||
}
|
||||
}
|
||||
|
||||
const loadRemoteStorages = async () => {
|
||||
try {
|
||||
const data = await fetchApi("/api/health/remote-storages")
|
||||
if (data.storages) {
|
||||
setRemoteStorages(data.storages)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load remote storages:", err)
|
||||
} finally {
|
||||
setLoadingStorages(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStorageExclusionChange = async (storageName: string, storageType: string, excludeHealth: boolean, excludeNotifications: boolean) => {
|
||||
setSavingStorage(storageName)
|
||||
try {
|
||||
// If both are false, remove the exclusion
|
||||
if (!excludeHealth && !excludeNotifications) {
|
||||
await fetchApi(`/api/health/storage-exclusions/${encodeURIComponent(storageName)}`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
} else {
|
||||
await fetchApi("/api/health/storage-exclusions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
storage_name: storageName,
|
||||
storage_type: storageType,
|
||||
exclude_health: excludeHealth,
|
||||
exclude_notifications: excludeNotifications
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Update local state
|
||||
setRemoteStorages(prev => prev.map(s =>
|
||||
s.name === storageName
|
||||
? { ...s, exclude_health: excludeHealth, exclude_notifications: excludeNotifications }
|
||||
: s
|
||||
))
|
||||
} catch (err) {
|
||||
console.error("Failed to update storage exclusion:", err)
|
||||
} finally {
|
||||
setSavingStorage(null)
|
||||
}
|
||||
}
|
||||
|
||||
const getSelectValue = (hours: number, key: string): string => {
|
||||
if (hours === -1) return "-1"
|
||||
const preset = SUPPRESSION_OPTIONS.find(o => o.value === String(hours))
|
||||
@@ -439,6 +507,120 @@ export function Settings() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Remote Storage Exclusions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5 text-purple-500" />
|
||||
<CardTitle>Remote Storage Exclusions</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Exclude remote storages (PBS, NFS, CIFS, etc.) from health monitoring and notifications.
|
||||
Use this for storages that are intentionally offline or have limited API access.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loadingStorages ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin h-8 w-8 border-4 border-purple-500 border-t-transparent rounded-full" />
|
||||
</div>
|
||||
) : remoteStorages.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<CloudOff className="h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50" />
|
||||
<p className="text-muted-foreground">No remote storages detected</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
PBS, NFS, CIFS, and other remote storages will appear here when configured
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0">
|
||||
{/* Header */}
|
||||
<div className="grid grid-cols-[1fr_auto_auto] gap-4 pb-2 mb-1 border-b border-border">
|
||||
<span className="text-xs font-medium text-muted-foreground">Storage</span>
|
||||
<span className="text-xs font-medium text-muted-foreground text-center w-20">Health</span>
|
||||
<span className="text-xs font-medium text-muted-foreground text-center w-20">Alerts</span>
|
||||
</div>
|
||||
|
||||
{/* Storage rows */}
|
||||
<div className="divide-y divide-border/50">
|
||||
{remoteStorages.map((storage) => {
|
||||
const isExcluded = storage.exclude_health || storage.exclude_notifications
|
||||
const isSaving = savingStorage === storage.name
|
||||
const isOffline = storage.status === 'error' || storage.total === 0
|
||||
|
||||
return (
|
||||
<div key={storage.name} className="grid grid-cols-[1fr_auto_auto] gap-4 py-3 items-center">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className={`w-2 h-2 rounded-full shrink-0 ${
|
||||
isOffline ? 'bg-red-500' : 'bg-green-500'
|
||||
}`} />
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">{storage.name}</span>
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 shrink-0">
|
||||
{storage.type}
|
||||
</Badge>
|
||||
</div>
|
||||
{isOffline && (
|
||||
<p className="text-[11px] text-red-400 mt-0.5">Offline or unavailable</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center w-20">
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<Switch
|
||||
checked={!storage.exclude_health}
|
||||
onCheckedChange={(checked) => {
|
||||
handleStorageExclusionChange(
|
||||
storage.name,
|
||||
storage.type,
|
||||
!checked,
|
||||
storage.exclude_notifications
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center w-20">
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<Switch
|
||||
checked={!storage.exclude_notifications}
|
||||
onCheckedChange={(checked) => {
|
||||
handleStorageExclusionChange(
|
||||
storage.name,
|
||||
storage.type,
|
||||
storage.exclude_health,
|
||||
!checked
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Info footer */}
|
||||
<div className="flex items-start gap-2 mt-3 pt-3 border-t border-border">
|
||||
<Info className="h-3.5 w-3.5 text-purple-400 shrink-0 mt-0.5" />
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
<strong>Health:</strong> When OFF, the storage won't trigger warnings/critical alerts in the Health Monitor.
|
||||
<br />
|
||||
<strong>Alerts:</strong> When OFF, no notifications will be sent for this storage.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Notification Settings */}
|
||||
<NotificationSettings />
|
||||
|
||||
|
||||
@@ -674,11 +674,20 @@ export function StorageOverview() {
|
||||
{proxmoxStorage.storage
|
||||
.filter((storage) => storage && storage.name && storage.used >= 0 && storage.available >= 0)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((storage) => (
|
||||
.map((storage) => {
|
||||
// Check if storage is excluded from monitoring
|
||||
const isExcluded = storage.excluded === true
|
||||
const hasError = storage.status === "error" && !isExcluded
|
||||
|
||||
return (
|
||||
<div
|
||||
key={storage.name}
|
||||
className={`border rounded-lg p-4 ${
|
||||
storage.status === "error" ? "border-red-500/50 bg-red-500/5" : ""
|
||||
hasError
|
||||
? "border-red-500/50 bg-red-500/5"
|
||||
: isExcluded
|
||||
? "border-purple-500/30 bg-purple-500/5 opacity-75"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
@@ -687,27 +696,40 @@ export function StorageOverview() {
|
||||
<Database className="h-5 w-5 text-muted-foreground" />
|
||||
<h3 className="font-semibold text-lg">{storage.name}</h3>
|
||||
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
|
||||
{isExcluded && (
|
||||
<Badge className="bg-purple-500/10 text-purple-400 border-purple-500/20 text-[10px]">
|
||||
excluded
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex md:hidden items-center gap-2 flex-1">
|
||||
<Database className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
|
||||
<h3 className="font-semibold text-base flex-1 min-w-0 truncate">{storage.name}</h3>
|
||||
{getStatusIcon(storage.status)}
|
||||
{isExcluded ? (
|
||||
<Badge className="bg-purple-500/10 text-purple-400 border-purple-500/20 text-[10px]">
|
||||
excluded
|
||||
</Badge>
|
||||
) : (
|
||||
getStatusIcon(storage.status)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Badge active + Porcentaje */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Badge
|
||||
className={
|
||||
storage.status === "active"
|
||||
? "bg-green-500/10 text-green-500 border-green-500/20"
|
||||
: storage.status === "error"
|
||||
? "bg-red-500/10 text-red-500 border-red-500/20"
|
||||
: "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||
isExcluded
|
||||
? "bg-purple-500/10 text-purple-400 border-purple-500/20"
|
||||
: storage.status === "active"
|
||||
? "bg-green-500/10 text-green-500 border-green-500/20"
|
||||
: storage.status === "error"
|
||||
? "bg-red-500/10 text-red-500 border-red-500/20"
|
||||
: "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||
}
|
||||
>
|
||||
{storage.status}
|
||||
{isExcluded ? "not monitored" : storage.status}
|
||||
</Badge>
|
||||
<span className="text-sm font-medium">{storage.percent}%</span>
|
||||
</div>
|
||||
@@ -750,7 +772,8 @@ export function StorageOverview() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user