mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-07 01:14:14 +00:00
311 lines
15 KiB
TypeScript
311 lines
15 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from '@/components/ui/sheet';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
import { Trash2, HelpCircle } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { apiFetch } from '@/lib/api';
|
|
|
|
interface StackAlert {
|
|
id?: number;
|
|
stack_name: string;
|
|
metric: string;
|
|
operator: string;
|
|
threshold: number;
|
|
duration_mins: number;
|
|
cooldown_mins: number;
|
|
}
|
|
|
|
interface StackAlertSheetProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
stackName: string;
|
|
}
|
|
|
|
export function StackAlertSheet({ isOpen, onClose, stackName }: StackAlertSheetProps) {
|
|
const [alerts, setAlerts] = useState<StackAlert[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// New Alert Form State
|
|
const [metric, setMetric] = useState('cpu_percent');
|
|
const [operator, setOperator] = useState('>');
|
|
const [threshold, setThreshold] = useState('');
|
|
const [duration, setDuration] = useState('5');
|
|
const [cooldown, setCooldown] = useState('60');
|
|
|
|
useEffect(() => {
|
|
if (isOpen && stackName) {
|
|
fetchAlerts();
|
|
}
|
|
}, [isOpen, stackName]);
|
|
|
|
const fetchAlerts = async () => {
|
|
try {
|
|
const res = await apiFetch(`/alerts?stackName=${stackName}`);
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setAlerts(data);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to fetch alerts', e);
|
|
}
|
|
};
|
|
|
|
const addAlert = async () => {
|
|
if (!threshold) {
|
|
toast.error('Please enter a threshold.');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
const newAlert = {
|
|
stack_name: stackName,
|
|
metric,
|
|
operator,
|
|
threshold: parseFloat(threshold),
|
|
duration_mins: parseInt(duration, 10),
|
|
cooldown_mins: parseInt(cooldown, 10)
|
|
};
|
|
|
|
try {
|
|
const res = await apiFetch('/alerts', {
|
|
method: 'POST',
|
|
body: JSON.stringify(newAlert)
|
|
});
|
|
if (res.ok) {
|
|
toast.success('Alert rule added.');
|
|
setThreshold('');
|
|
fetchAlerts();
|
|
} else {
|
|
toast.error('Failed to add alert rule.');
|
|
}
|
|
} catch (e) {
|
|
toast.error('Network error.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const deleteAlert = async (id: number) => {
|
|
setIsLoading(true);
|
|
try {
|
|
const res = await apiFetch(`/alerts/${id}`, { method: 'DELETE' });
|
|
if (res.ok) {
|
|
toast.success('Alert rule deleted.');
|
|
fetchAlerts();
|
|
} else {
|
|
toast.error('Failed to delete alert rule.');
|
|
}
|
|
} catch (e) {
|
|
toast.error('Network error.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const metricLabels: Record<string, string> = {
|
|
cpu_percent: 'CPU Usage (%)',
|
|
memory_percent: 'Memory Usage (%)',
|
|
memory_mb: 'Memory Usage (MB)',
|
|
net_rx: 'Network In (MB)',
|
|
net_tx: 'Network Out (MB)',
|
|
restart_count: 'Restart Count'
|
|
};
|
|
|
|
return (
|
|
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
<SheetContent className="overflow-y-auto sm:max-w-[400px]">
|
|
<SheetHeader>
|
|
<SheetTitle>Stack Alerts: {stackName}</SheetTitle>
|
|
<SheetDescription>
|
|
Configure metric thresholds to trigger notifications for this stack.
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<TooltipProvider>
|
|
<div className="mt-6 space-y-6">
|
|
{/* List Existing Alerts */}
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-semibold">Existing Rules</h4>
|
|
{alerts.length === 0 ? (
|
|
<div className="text-sm text-muted-foreground p-4 bg-muted/50 rounded-lg text-center">
|
|
No active alert rules for this stack.
|
|
</div>
|
|
) : (
|
|
alerts.map(alert => (
|
|
<div key={alert.id} className="flex flex-col gap-2 p-3 bg-muted/50 rounded-lg border text-sm">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<span className="font-semibold text-foreground">
|
|
{metricLabels[alert.metric] || alert.metric} {alert.operator} {alert.threshold}
|
|
</span>
|
|
<div className="text-muted-foreground mt-1">
|
|
Trigger after {alert.duration_mins}m • Cooldown: {alert.cooldown_mins}m
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-muted-foreground hover:text-destructive shrink-0"
|
|
onClick={() => alert.id && deleteAlert(alert.id)}
|
|
disabled={isLoading}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
{/* Add New Alert Form */}
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-semibold">Add New Rule</h4>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Label>Metric</Label>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<HelpCircle className="h-4 w-4 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p className="max-w-[200px] text-sm">The system resource or metric to monitor. Select from CPU, Memory, Network I/O, or Restarts.</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<Select value={metric} onValueChange={setMetric}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.entries(metricLabels).map(([val, label]) => (
|
|
<SelectItem key={val} value={val}>{label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Label>Operator</Label>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<HelpCircle className="h-4 w-4 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p className="max-w-[200px] text-sm">The comparison condition to trigger the alert against the threshold.</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<Select value={operator} onValueChange={setOperator}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value=">">Greater than</SelectItem>
|
|
<SelectItem value=">=">Greater or eq</SelectItem>
|
|
<SelectItem value="<">Less than</SelectItem>
|
|
<SelectItem value="<=">Less or eq</SelectItem>
|
|
<SelectItem value="==">Equals</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Label>Threshold</Label>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<HelpCircle className="h-4 w-4 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p className="max-w-[200px] text-sm">The numerical value the metric needs to breach to trigger the conditions.</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
value={threshold}
|
|
onChange={e => {
|
|
let val = e.target.value;
|
|
if (val !== '' && Number(val) < 0) val = '0';
|
|
setThreshold(val);
|
|
}}
|
|
placeholder="e.g. 90"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Label>Duration (mins)</Label>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<HelpCircle className="h-4 w-4 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p className="max-w-[200px] text-sm">How long the metric must stay in breach of the threshold before sending an alert.</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
value={duration}
|
|
onChange={e => {
|
|
let val = e.target.value;
|
|
if (val !== '' && Number(val) < 0) val = '0';
|
|
setDuration(val);
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Label>Cooldown (mins)</Label>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<HelpCircle className="h-4 w-4 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p className="max-w-[200px] text-sm">How long to wait before sending another alert if the stack continues to breach.</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
value={cooldown}
|
|
onChange={e => {
|
|
let val = e.target.value;
|
|
if (val !== '' && Number(val) < 0) val = '0';
|
|
setCooldown(val);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Button className="w-full mt-2" onClick={addAlert} disabled={isLoading}>
|
|
Add Rule
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</TooltipProvider>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|