add alert dialog

add confirmation dialog
fix clickhouse metrics table.
This commit is contained in:
Fizer Khan
2025-12-05 10:37:02 +05:30
parent 330fbcbd42
commit b4f28bda41
12 changed files with 989 additions and 195 deletions
+9 -4
View File
@@ -2,6 +2,11 @@ import { createClient, ClickHouseClient } from '@clickhouse/client';
import { config } from '../config/index';
import type { StreamMetrics, ConsumerMetrics, ClusterMetrics, AuditLog } from '../../../shared/src/index';
// Format timestamp for ClickHouse DateTime64(3)
function formatTimestamp(date: Date): string {
return date.toISOString().replace('T', ' ').replace('Z', '');
}
let client: ClickHouseClient | null = null;
export function getClickHouseClient(): ClickHouseClient {
@@ -32,7 +37,7 @@ export async function insertStreamMetrics(metrics: StreamMetrics[]): Promise<voi
values: metrics.map((m) => ({
cluster_id: m.clusterId,
stream_name: m.streamName,
timestamp: m.timestamp.toISOString(),
timestamp: formatTimestamp(m.timestamp),
messages_total: m.messagesTotal,
bytes_total: m.bytesTotal,
messages_rate: m.messagesRate,
@@ -113,7 +118,7 @@ export async function insertConsumerMetrics(metrics: ConsumerMetrics[]): Promise
cluster_id: m.clusterId,
stream_name: m.streamName,
consumer_name: m.consumerName,
timestamp: m.timestamp.toISOString(),
timestamp: formatTimestamp(m.timestamp),
pending_count: m.pendingCount,
ack_pending: m.ackPending,
redelivered: m.redelivered,
@@ -196,7 +201,7 @@ export async function insertClusterMetrics(metrics: ClusterMetrics[]): Promise<v
cluster_id: m.clusterId,
server_id: m.serverId,
server_name: m.serverName,
timestamp: m.timestamp.toISOString(),
timestamp: formatTimestamp(m.timestamp),
cpu_percent: m.cpuPercent,
memory_bytes: m.memoryBytes,
connections: m.connections,
@@ -223,7 +228,7 @@ export async function insertAuditLog(log: Omit<AuditLog, 'id'>): Promise<void> {
org_id: log.orgId,
user_id: log.userId,
user_email: log.userEmail,
timestamp: log.timestamp.toISOString(),
timestamp: formatTimestamp(log.timestamp),
action: log.action,
resource_type: log.resourceType,
resource_id: log.resourceId,
+522 -145
View File
@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
Plus,
Bell,
@@ -9,54 +9,138 @@ import {
AlertTriangle,
CheckCircle,
Clock,
MoreVertical,
Trash2,
Edit,
Power,
PowerOff,
RefreshCw,
} from 'lucide-react';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
interface AlertRule {
id: string;
name: string;
clusterId: string | null;
condition: {
metric: string;
operator: string;
window: number;
aggregation: string;
};
threshold: {
value: number;
type: string;
};
severity: string;
channels: Array<{ type: string; config: any }>;
isEnabled: boolean;
cooldownMins: number;
lastTriggeredAt: string | null;
createdAt: string;
cluster?: { id: string; name: string } | null;
}
const defaultRule = {
name: '',
clusterId: null as string | null,
condition: {
metric: 'consumer_lag',
operator: 'gt',
window: 300,
aggregation: 'avg',
},
threshold: {
value: 1000,
type: 'absolute',
},
severity: 'warning',
channels: [{ type: 'email', config: {} }],
isEnabled: true,
cooldownMins: 5,
};
export default function AlertsPage() {
const [selectedCluster, setSelectedCluster] = useState<string | null>(null);
const [filter, setFilter] = useState<'all' | 'active' | 'resolved'>('all');
const queryClient = useQueryClient();
const [filter, setFilter] = useState<'all' | 'enabled' | 'disabled'>('all');
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isEditOpen, setIsEditOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [selectedRule, setSelectedRule] = useState<AlertRule | null>(null);
const [formData, setFormData] = useState(defaultRule);
const { data: clustersData } = useQuery({
queryKey: ['clusters'],
queryFn: () => api.clusters.list(),
});
const { data: alertsData, isLoading } = useQuery({
queryKey: ['alerts', selectedCluster],
queryFn: () => (selectedCluster ? api.alerts.list(selectedCluster) : null),
enabled: !!selectedCluster,
const { data: rulesData, isLoading, refetch } = useQuery({
queryKey: ['alert-rules'],
queryFn: () => api.alerts.listRules(),
});
// Auto-select first cluster
if (clustersData?.clusters?.length && !selectedCluster) {
setSelectedCluster(clustersData.clusters[0].id);
}
const createMutation = useMutation({
mutationFn: (data: typeof defaultRule) => api.alerts.createRule(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alert-rules'] });
setIsCreateOpen(false);
setFormData(defaultRule);
},
});
const filteredAlerts = alertsData?.alerts?.filter((alert: any) => {
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<typeof defaultRule> }) =>
api.alerts.updateRule(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alert-rules'] });
setIsEditOpen(false);
setSelectedRule(null);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => api.alerts.deleteRule(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alert-rules'] });
setIsDeleteOpen(false);
setSelectedRule(null);
},
});
const filteredRules = rulesData?.rules?.filter((rule: AlertRule) => {
if (filter === 'all') return true;
if (filter === 'active') return alert.status === 'firing' || alert.status === 'pending';
if (filter === 'resolved') return alert.status === 'resolved';
if (filter === 'enabled') return rule.isEnabled;
if (filter === 'disabled') return !rule.isEnabled;
return true;
});
const getStatusIcon = (status: string) => {
switch (status) {
case 'firing':
return <AlertTriangle className="h-5 w-5 text-red-500" />;
case 'pending':
return <Clock className="h-5 w-5 text-yellow-500" />;
case 'resolved':
return <CheckCircle className="h-5 w-5 text-green-500" />;
default:
return <Bell className="h-5 w-5 text-gray-500" />;
}
};
const getSeverityBadge = (severity: string) => {
const colors: Record<string, string> = {
critical: 'bg-red-100 text-red-700',
@@ -70,7 +154,8 @@ export default function AlertsPage() {
);
};
const formatTimeAgo = (timestamp: string) => {
const formatTimeAgo = (timestamp: string | null) => {
if (!timestamp) return 'Never';
const date = new Date(timestamp);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
@@ -84,6 +169,48 @@ export default function AlertsPage() {
return 'Just now';
};
const handleCreate = () => {
createMutation.mutate(formData);
};
const handleEdit = (rule: AlertRule) => {
setSelectedRule(rule);
setFormData({
name: rule.name,
clusterId: rule.clusterId,
condition: rule.condition,
threshold: rule.threshold,
severity: rule.severity,
channels: rule.channels,
isEnabled: rule.isEnabled,
cooldownMins: rule.cooldownMins,
});
setIsEditOpen(true);
};
const handleUpdate = () => {
if (!selectedRule) return;
updateMutation.mutate({ id: selectedRule.id, data: formData });
};
const handleToggleEnabled = async (rule: AlertRule) => {
await updateMutation.mutateAsync({
id: rule.id,
data: { isEnabled: !rule.isEnabled },
});
};
const handleDelete = (rule: AlertRule) => {
setSelectedRule(rule);
setIsDeleteOpen(true);
};
const confirmDelete = () => {
if (selectedRule) {
deleteMutation.mutate(selectedRule.id);
}
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@@ -91,34 +218,30 @@ export default function AlertsPage() {
<h1 className="text-3xl font-bold">Alerts</h1>
<p className="text-muted-foreground">Monitor and manage alert rules</p>
</div>
<Button disabled={!selectedCluster}>
<Plus className="h-4 w-4" />
Create Alert Rule
</Button>
<div className="flex gap-2">
<Button variant="outline" size="icon" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4" />
</Button>
<Button onClick={() => {
setFormData(defaultRule);
setIsCreateOpen(true);
}}>
<Plus className="h-4 w-4 mr-2" />
Create Rule
</Button>
</div>
</div>
<div className="flex gap-4">
<select
className="h-10 rounded-md border border-input bg-background px-3 py-2 text-sm"
value={selectedCluster || ''}
onChange={(e) => setSelectedCluster(e.target.value)}
>
<option value="">Select cluster...</option>
{clustersData?.clusters?.map((cluster: any) => (
<option key={cluster.id} value={cluster.id}>
{cluster.name}
</option>
))}
</select>
<div className="flex rounded-md border">
{(['all', 'active', 'resolved'] as const).map((f) => (
{(['all', 'enabled', 'disabled'] as const).map((f) => (
<button
key={f}
className={`px-4 py-2 text-sm font-medium transition-colors ${
filter === f
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted'
} ${f === 'all' ? 'rounded-l-md' : ''} ${f === 'resolved' ? 'rounded-r-md' : ''}`}
} ${f === 'all' ? 'rounded-l-md' : ''} ${f === 'disabled' ? 'rounded-r-md' : ''}`}
onClick={() => setFilter(f)}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
@@ -127,33 +250,23 @@ export default function AlertsPage() {
</div>
</div>
{!selectedCluster && (
<Card>
<CardContent className="py-12 text-center">
<Bell className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Select a cluster</h3>
<p className="text-muted-foreground">Choose a cluster to view alerts</p>
</CardContent>
</Card>
)}
{selectedCluster && isLoading && (
{isLoading && (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)}
{selectedCluster && filteredAlerts && filteredAlerts.length === 0 && (
{!isLoading && (!filteredRules || filteredRules.length === 0) && (
<Card>
<CardContent className="py-12 text-center">
<BellOff className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">No alerts found</h3>
<h3 className="text-lg font-semibold mb-2">No alert rules found</h3>
<p className="text-muted-foreground mb-4">
{filter !== 'all' ? `No ${filter} alerts` : 'Create your first alert rule to get started'}
{filter !== 'all' ? `No ${filter} rules` : 'Create your first alert rule to get started'}
</p>
{filter === 'all' && (
<Button>
<Plus className="h-4 w-4" />
<Button onClick={() => setIsCreateOpen(true)}>
<Plus className="h-4 w-4 mr-2" />
Create Alert Rule
</Button>
)}
@@ -161,93 +274,357 @@ export default function AlertsPage() {
</Card>
)}
{filteredAlerts && filteredAlerts.length > 0 && (
<div className="space-y-4">
{/* Alert Rules Section */}
<Card>
<CardHeader>
<CardTitle>Alert Rules</CardTitle>
<CardDescription>Configured alert rules for this cluster</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{filteredAlerts.map((alert: any) => (
<div
key={alert.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/30"
>
<div className="flex items-center gap-4">
{getStatusIcon(alert.status)}
<div>
<h4 className="font-medium">{alert.name}</h4>
<p className="text-sm text-muted-foreground">
{alert.condition} {alert.operator} {alert.threshold}
</p>
</div>
</div>
<div className="flex items-center gap-4">
{getSeverityBadge(alert.severity)}
<span className="text-sm text-muted-foreground">
{alert.lastTriggered ? formatTimeAgo(alert.lastTriggered) : 'Never triggered'}
</span>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4" />
</Button>
</div>
{filteredRules && filteredRules.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Alert Rules</CardTitle>
<CardDescription>{filteredRules.length} rule{filteredRules.length !== 1 ? 's' : ''} configured</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{filteredRules.map((rule: AlertRule) => (
<div
key={rule.id}
className={`flex items-center justify-between p-4 border rounded-lg hover:bg-muted/30 ${
!rule.isEnabled ? 'opacity-60' : ''
}`}
>
<div className="flex items-center gap-4">
{rule.isEnabled ? (
<Bell className="h-5 w-5 text-primary" />
) : (
<BellOff className="h-5 w-5 text-muted-foreground" />
)}
<div>
<h4 className="font-medium">{rule.name}</h4>
<p className="text-sm text-muted-foreground">
{rule.condition.metric} {rule.condition.operator} {rule.threshold.value}
{rule.cluster && ` • ${rule.cluster.name}`}
</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Recent Alert Events */}
<Card>
<CardHeader>
<CardTitle>Recent Events</CardTitle>
<CardDescription>Latest alert activity</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center gap-4 p-3 bg-red-50 rounded-lg">
<AlertTriangle className="h-5 w-5 text-red-500" />
<div className="flex-1">
<p className="text-sm font-medium">Consumer lag exceeded threshold</p>
<p className="text-xs text-muted-foreground">
ORDERS stream - lag-alert rule triggered
</p>
<div className="flex items-center gap-4">
{getSeverityBadge(rule.severity)}
<span className="text-sm text-muted-foreground min-w-[80px]">
{formatTimeAgo(rule.lastTriggeredAt)}
</span>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleToggleEnabled(rule)}
title={rule.isEnabled ? 'Disable' : 'Enable'}
>
{rule.isEnabled ? (
<Power className="h-4 w-4 text-green-500" />
) : (
<PowerOff className="h-4 w-4 text-muted-foreground" />
)}
</Button>
<Button variant="ghost" size="icon" onClick={() => handleEdit(rule)}>
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => handleDelete(rule)}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
<span className="text-xs text-muted-foreground">2m ago</span>
</div>
<div className="flex items-center gap-4 p-3 bg-green-50 rounded-lg">
<CheckCircle className="h-5 w-5 text-green-500" />
<div className="flex-1">
<p className="text-sm font-medium">Alert resolved</p>
<p className="text-xs text-muted-foreground">
EVENTS stream - throughput-alert back to normal
</p>
</div>
<span className="text-xs text-muted-foreground">15m ago</span>
</div>
<div className="flex items-center gap-4 p-3 bg-yellow-50 rounded-lg">
<Clock className="h-5 w-5 text-yellow-500" />
<div className="flex-1">
<p className="text-sm font-medium">Alert pending</p>
<p className="text-xs text-muted-foreground">
LOGS stream - storage-alert approaching threshold
</p>
</div>
<span className="text-xs text-muted-foreground">1h ago</span>
</div>
</div>
</CardContent>
</Card>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Create Alert Rule Dialog */}
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Create Alert Rule</DialogTitle>
<DialogDescription>
Configure a new alert rule to monitor your NATS streams
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Name</label>
<Input
placeholder="e.g., High consumer lag alert"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Cluster (optional)</label>
<Select
value={formData.clusterId || 'all'}
onValueChange={(v) => setFormData({ ...formData, clusterId: v === 'all' ? null : v })}
>
<SelectTrigger>
<SelectValue placeholder="All clusters" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All clusters</SelectItem>
{clustersData?.clusters?.map((cluster: any) => (
<SelectItem key={cluster.id} value={cluster.id}>
{cluster.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Metric</label>
<Select
value={formData.condition.metric}
onValueChange={(v) =>
setFormData({
...formData,
condition: { ...formData.condition, metric: v },
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="consumer_lag">Consumer Lag</SelectItem>
<SelectItem value="message_rate">Message Rate</SelectItem>
<SelectItem value="stream_size">Stream Size</SelectItem>
<SelectItem value="pending_count">Pending Count</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Operator</label>
<Select
value={formData.condition.operator}
onValueChange={(v) =>
setFormData({
...formData,
condition: { ...formData.condition, operator: v },
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gt">Greater than</SelectItem>
<SelectItem value="lt">Less than</SelectItem>
<SelectItem value="gte">Greater or equal</SelectItem>
<SelectItem value="lte">Less or equal</SelectItem>
<SelectItem value="eq">Equals</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Threshold</label>
<Input
type="number"
value={formData.threshold.value}
onChange={(e) =>
setFormData({
...formData,
threshold: { ...formData.threshold, value: Number(e.target.value) },
})
}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Severity</label>
<Select
value={formData.severity}
onValueChange={(v) => setFormData({ ...formData, severity: v })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="info">Info</SelectItem>
<SelectItem value="warning">Warning</SelectItem>
<SelectItem value="critical">Critical</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Cooldown (minutes)</label>
<Input
type="number"
value={formData.cooldownMins}
onChange={(e) => setFormData({ ...formData, cooldownMins: Number(e.target.value) })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreateOpen(false)}>
Cancel
</Button>
<Button onClick={handleCreate} disabled={!formData.name || createMutation.isPending}>
{createMutation.isPending ? 'Creating...' : 'Create Rule'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Edit Alert Rule Dialog */}
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Edit Alert Rule</DialogTitle>
<DialogDescription>
Update the alert rule configuration
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Name</label>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Cluster (optional)</label>
<Select
value={formData.clusterId || 'all'}
onValueChange={(v) => setFormData({ ...formData, clusterId: v === 'all' ? null : v })}
>
<SelectTrigger>
<SelectValue placeholder="All clusters" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All clusters</SelectItem>
{clustersData?.clusters?.map((cluster: any) => (
<SelectItem key={cluster.id} value={cluster.id}>
{cluster.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Metric</label>
<Select
value={formData.condition.metric}
onValueChange={(v) =>
setFormData({
...formData,
condition: { ...formData.condition, metric: v },
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="consumer_lag">Consumer Lag</SelectItem>
<SelectItem value="message_rate">Message Rate</SelectItem>
<SelectItem value="stream_size">Stream Size</SelectItem>
<SelectItem value="pending_count">Pending Count</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Operator</label>
<Select
value={formData.condition.operator}
onValueChange={(v) =>
setFormData({
...formData,
condition: { ...formData.condition, operator: v },
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gt">Greater than</SelectItem>
<SelectItem value="lt">Less than</SelectItem>
<SelectItem value="gte">Greater or equal</SelectItem>
<SelectItem value="lte">Less or equal</SelectItem>
<SelectItem value="eq">Equals</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Threshold</label>
<Input
type="number"
value={formData.threshold.value}
onChange={(e) =>
setFormData({
...formData,
threshold: { ...formData.threshold, value: Number(e.target.value) },
})
}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Severity</label>
<Select
value={formData.severity}
onValueChange={(v) => setFormData({ ...formData, severity: v })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="info">Info</SelectItem>
<SelectItem value="warning">Warning</SelectItem>
<SelectItem value="critical">Critical</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Cooldown (minutes)</label>
<Input
type="number"
value={formData.cooldownMins}
onChange={(e) => setFormData({ ...formData, cooldownMins: Number(e.target.value) })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditOpen(false)}>
Cancel
</Button>
<Button onClick={handleUpdate} disabled={!formData.name || updateMutation.isPending}>
{updateMutation.isPending ? 'Saving...' : 'Save Changes'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Alert Rule</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete &quot;{selectedRule?.name}&quot;? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-red-600 hover:bg-red-700"
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -1,6 +1,6 @@
'use client';
import { Suspense } from 'react';
import { Suspense, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
@@ -24,6 +24,16 @@ import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { TabsList, TabsContent, useTabs, Tab } from '@/components/ui/tabs';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { formatBytes, formatNumber } from '@nats-console/shared';
const tabs: Tab[] = [
@@ -38,6 +48,7 @@ function ClusterDetailContent() {
const router = useRouter();
const queryClient = useQueryClient();
const clusterId = params.id as string;
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { activeTab, setActiveTab } = useTabs(tabs, 'overview');
@@ -151,19 +162,36 @@ function ClusterDetailContent() {
<Button
variant="destructive"
size="sm"
onClick={() => {
if (confirm('Are you sure you want to delete this cluster?')) {
deleteMutation.mutate();
}
}}
onClick={() => setShowDeleteDialog(true)}
disabled={deleteMutation.isPending}
>
<Trash2 className="h-4 w-4" />
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Cluster</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete cluster &quot;{cluster.name}&quot;? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteMutation.mutate()}
className="bg-red-600 hover:bg-red-700"
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Tabs */}
<TabsList tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} />
+11 -6
View File
@@ -2,7 +2,7 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Plus, Server, MoreVertical, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
import { Plus, Server, MoreVertical, CheckCircle, XCircle, AlertCircle, RefreshCw } from 'lucide-react';
import Link from 'next/link';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
@@ -12,7 +12,7 @@ import { CreateClusterDialog } from '@/components/forms/create-cluster-dialog';
export default function ClustersPage() {
const [showCreateDialog, setShowCreateDialog] = useState(false);
const { data, isLoading, error } = useQuery({
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['clusters'],
queryFn: () => api.clusters.list(),
});
@@ -50,10 +50,15 @@ export default function ClustersPage() {
<h1 className="text-3xl font-bold">Clusters</h1>
<p className="text-muted-foreground">Manage your NATS JetStream clusters</p>
</div>
<Button onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4" />
Add Cluster
</Button>
<div className="flex gap-2">
<Button variant="outline" size="icon" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4" />
</Button>
<Button onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
Add Cluster
</Button>
</div>
<CreateClusterDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
</div>
@@ -1,6 +1,6 @@
'use client';
import { Suspense } from 'react';
import { Suspense, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
@@ -22,6 +22,16 @@ import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { TabsList, useTabs, Tab } from '@/components/ui/tabs';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { formatNumber, formatDuration } from '@nats-console/shared';
const tabs: Tab[] = [
@@ -37,6 +47,7 @@ function ConsumerDetailContent() {
const clusterId = params.clusterId as string;
const streamName = params.stream as string;
const consumerName = params.name as string;
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { activeTab, setActiveTab } = useTabs(tabs, 'overview');
@@ -107,26 +118,50 @@ function ConsumerDetailContent() {
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
onClick={() => queryClient.invalidateQueries({ queryKey: ['consumer', clusterId, streamName, consumerName] })}
>
<RefreshCw className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<Edit className="h-4 w-4" />
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => {
if (confirm('Are you sure you want to delete this consumer?')) {
deleteMutation.mutate();
}
}}
onClick={() => setShowDeleteDialog(true)}
disabled={deleteMutation.isPending}
>
<Trash2 className="h-4 w-4" />
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Consumer</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete consumer &quot;{consumerName}&quot;? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteMutation.mutate()}
className="bg-red-600 hover:bg-red-700"
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Tabs */}
<TabsList tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} />
+67 -7
View File
@@ -1,21 +1,33 @@
'use client';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Plus, Users, Search, AlertTriangle, CheckCircle, ChevronRight } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Plus, Users, Search, AlertTriangle, CheckCircle, ChevronRight, RefreshCw, Trash2 } from 'lucide-react';
import Link from 'next/link';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { formatNumber, formatDuration } from '@nats-console/shared';
import { CreateConsumerDialog } from '@/components/forms/create-consumer-dialog';
export default function ConsumersPage() {
const queryClient = useQueryClient();
const [selectedCluster, setSelectedCluster] = useState<string | null>(null);
const [selectedStream, setSelectedStream] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [consumerToDelete, setConsumerToDelete] = useState<string | null>(null);
const { data: clustersData } = useQuery({
queryKey: ['clusters'],
@@ -28,7 +40,7 @@ export default function ConsumersPage() {
enabled: !!selectedCluster,
});
const { data: consumersData, isLoading } = useQuery({
const { data: consumersData, isLoading, refetch } = useQuery({
queryKey: ['consumers', selectedCluster, selectedStream],
queryFn: () =>
selectedCluster && selectedStream
@@ -37,6 +49,15 @@ export default function ConsumersPage() {
enabled: !!selectedCluster && !!selectedStream,
});
const deleteMutation = useMutation({
mutationFn: (consumerName: string) =>
api.consumers.delete(selectedCluster!, selectedStream!, consumerName),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['consumers', selectedCluster, selectedStream] });
setConsumerToDelete(null);
},
});
// Auto-select first cluster
if (clustersData?.clusters?.length && !selectedCluster) {
setSelectedCluster(clustersData.clusters[0].id);
@@ -65,10 +86,15 @@ export default function ConsumersPage() {
<h1 className="text-3xl font-bold">Consumers</h1>
<p className="text-muted-foreground">Manage JetStream consumers</p>
</div>
<Button disabled={!selectedStream} onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4" />
Create Consumer
</Button>
<div className="flex gap-2">
<Button variant="outline" size="icon" onClick={() => refetch()} disabled={!selectedStream}>
<RefreshCw className="h-4 w-4" />
</Button>
<Button disabled={!selectedStream} onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
Create Consumer
</Button>
</div>
{selectedCluster && selectedStream && (
<CreateConsumerDialog
open={showCreateDialog}
@@ -165,6 +191,7 @@ export default function ConsumersPage() {
<th className="text-right p-4 font-medium">Ack Pending</th>
<th className="text-right p-4 font-medium">Ack Wait</th>
<th className="text-center p-4 font-medium">Health</th>
<th className="text-center p-4 font-medium">Actions</th>
</tr>
</thead>
<tbody>
@@ -202,6 +229,18 @@ export default function ConsumersPage() {
<td className="p-4 text-center">
<HealthIcon className={`h-5 w-5 mx-auto ${health.color}`} />
</td>
<td className="p-4 text-center">
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.preventDefault();
setConsumerToDelete(consumer.name);
}}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</td>
</tr>
);
})}
@@ -209,6 +248,27 @@ export default function ConsumersPage() {
</table>
</div>
)}
{/* Delete Confirmation Dialog */}
<AlertDialog open={!!consumerToDelete} onOpenChange={(open) => !open && setConsumerToDelete(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Consumer</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete consumer &quot;{consumerToDelete}&quot;? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => consumerToDelete && deleteMutation.mutate(consumerToDelete)}
className="bg-red-600 hover:bg-red-700"
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -22,6 +22,16 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { TabsList, useTabs, Tab } from '@/components/ui/tabs';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { formatBytes, formatNumber, formatDuration } from '@nats-console/shared';
const tabs: Tab[] = [
@@ -43,6 +53,7 @@ function StreamDetailContent() {
const [messageSubject, setMessageSubject] = useState('');
const [messageData, setMessageData] = useState('');
const [startSeq, setStartSeq] = useState('1');
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { data: streamData, isLoading } = useQuery({
queryKey: ['stream', clusterId, streamName],
@@ -147,19 +158,36 @@ function StreamDetailContent() {
<Button
variant="destructive"
size="sm"
onClick={() => {
if (confirm('Are you sure you want to delete this stream?')) {
deleteMutation.mutate();
}
}}
onClick={() => setShowDeleteDialog(true)}
disabled={deleteMutation.isPending}
>
<Trash2 className="h-4 w-4" />
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Stream</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete stream &quot;{streamName}&quot;? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteMutation.mutate()}
className="bg-red-600 hover:bg-red-700"
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Tabs */}
<TabsList tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} />
+11 -7
View File
@@ -2,7 +2,7 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Plus, Database, Search, ChevronRight } from 'lucide-react';
import { Plus, Database, Search, ChevronRight, RefreshCw } from 'lucide-react';
import Link from 'next/link';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
@@ -21,7 +21,7 @@ export default function StreamsPage() {
queryFn: () => api.clusters.list(),
});
const { data: streamsData, isLoading } = useQuery({
const { data: streamsData, isLoading, refetch } = useQuery({
queryKey: ['streams', selectedCluster],
queryFn: () => (selectedCluster ? api.streams.list(selectedCluster) : null),
enabled: !!selectedCluster,
@@ -43,10 +43,15 @@ export default function StreamsPage() {
<h1 className="text-3xl font-bold">Streams</h1>
<p className="text-muted-foreground">Manage JetStream streams</p>
</div>
<Button disabled={!selectedCluster} onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4" />
Create Stream
</Button>
<div className="flex gap-2">
<Button variant="outline" size="icon" onClick={() => refetch()} disabled={!selectedCluster}>
<RefreshCw className="h-4 w-4" />
</Button>
<Button disabled={!selectedCluster} onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
Create Stream
</Button>
</div>
{selectedCluster && (
<CreateStreamDialog
open={showCreateDialog}
@@ -136,7 +141,6 @@ export default function StreamsPage() {
className="font-medium text-primary hover:underline flex items-center gap-1"
>
{stream.config.name}
<ChevronRight className="h-4 w-4" />
</Link>
</td>
<td className="p-4 text-muted-foreground">
+123
View File
@@ -0,0 +1,123 @@
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Button } from './button';
interface AlertDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
children: React.ReactNode;
}
export function AlertDialog({ open, onOpenChange, children }: AlertDialogProps) {
if (!open) return null;
return (
<div className="fixed inset-0 z-50">
<div
className="fixed inset-0 bg-black/50"
onClick={() => onOpenChange(false)}
/>
<div className="fixed inset-0 flex items-center justify-center p-4">
<div
className="relative bg-background rounded-lg shadow-lg max-h-[90vh] overflow-auto w-full max-w-lg p-6"
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
</div>
);
}
export function AlertDialogContent({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn('', className)} {...props}>
{children}
</div>
);
}
export function AlertDialogHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('flex flex-col space-y-2 text-center sm:text-left', className)}
{...props}
/>
);
}
export function AlertDialogTitle({
className,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h2
className={cn('text-lg font-semibold', className)}
{...props}
/>
);
}
export function AlertDialogDescription({
className,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
);
}
export function AlertDialogFooter({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-4',
className
)}
{...props}
/>
);
}
interface AlertDialogActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}
export function AlertDialogAction({
className,
children,
...props
}: AlertDialogActionProps) {
return (
<Button className={cn(className)} {...props}>
{children}
</Button>
);
}
export function AlertDialogCancel({
className,
children,
...props
}: AlertDialogActionProps) {
return (
<Button variant="outline" className={cn('mt-2 sm:mt-0', className)} {...props}>
{children || 'Cancel'}
</Button>
);
}
+124
View File
@@ -0,0 +1,124 @@
'use client';
import * as React from 'react';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
interface SelectContextValue {
value: string;
onValueChange: (value: string) => void;
open: boolean;
setOpen: (open: boolean) => void;
}
const SelectContext = React.createContext<SelectContextValue | null>(null);
interface SelectProps {
value: string;
onValueChange: (value: string) => void;
children: React.ReactNode;
}
export function Select({ value, onValueChange, children }: SelectProps) {
const [open, setOpen] = React.useState(false);
return (
<SelectContext.Provider value={{ value, onValueChange, open, setOpen }}>
<div className="relative">{children}</div>
</SelectContext.Provider>
);
}
interface SelectTriggerProps extends React.HTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}
export function SelectTrigger({ className, children, ...props }: SelectTriggerProps) {
const context = React.useContext(SelectContext);
if (!context) throw new Error('SelectTrigger must be used within Select');
return (
<button
type="button"
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
onClick={() => context.setOpen(!context.open)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 opacity-50" />
</button>
);
}
interface SelectValueProps {
placeholder?: string;
}
export function SelectValue({ placeholder }: SelectValueProps) {
const context = React.useContext(SelectContext);
if (!context) throw new Error('SelectValue must be used within Select');
return (
<span className={!context.value ? 'text-muted-foreground' : ''}>
{context.value || placeholder}
</span>
);
}
interface SelectContentProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export function SelectContent({ className, children, ...props }: SelectContentProps) {
const context = React.useContext(SelectContext);
if (!context) throw new Error('SelectContent must be used within Select');
if (!context.open) return null;
return (
<>
<div className="fixed inset-0 z-40" onClick={() => context.setOpen(false)} />
<div
className={cn(
'absolute z-50 min-w-[8rem] w-full mt-1 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95',
className
)}
{...props}
>
<div className="p-1">{children}</div>
</div>
</>
);
}
interface SelectItemProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
children: React.ReactNode;
}
export function SelectItem({ className, value, children, ...props }: SelectItemProps) {
const context = React.useContext(SelectContext);
if (!context) throw new Error('SelectItem must be used within Select');
const isSelected = context.value === value;
return (
<div
className={cn(
'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground',
isSelected && 'bg-accent text-accent-foreground',
className
)}
onClick={() => {
context.onValueChange(value);
context.setOpen(false);
}}
{...props}
>
{children}
</div>
);
}
+8 -3
View File
@@ -7,6 +7,11 @@ import type { StreamMetrics, ConsumerMetrics, ClusterMetrics } from '../../../sh
const logger = pino({ name: 'metrics-collector' });
// Format timestamp for ClickHouse DateTime64(3)
function formatTimestamp(date: Date): string {
return date.toISOString().replace('T', ' ').replace('Z', '');
}
interface ClusterConnection {
id: string;
nc: NatsConnection;
@@ -203,7 +208,7 @@ export class MetricsCollector {
values: streamMetrics.map((m) => ({
cluster_id: m.clusterId,
stream_name: m.streamName,
timestamp: m.timestamp.toISOString(),
timestamp: formatTimestamp(m.timestamp),
messages_total: m.messagesTotal,
bytes_total: m.bytesTotal,
messages_rate: m.messagesRate,
@@ -230,7 +235,7 @@ export class MetricsCollector {
cluster_id: m.clusterId,
stream_name: m.streamName,
consumer_name: m.consumerName,
timestamp: m.timestamp.toISOString(),
timestamp: formatTimestamp(m.timestamp),
pending_count: m.pendingCount,
ack_pending: m.ackPending,
redelivered: m.redelivered,
@@ -288,7 +293,7 @@ export class MetricsCollector {
cluster_id: m.clusterId,
server_id: m.serverId,
server_name: m.serverName,
timestamp: m.timestamp.toISOString(),
timestamp: formatTimestamp(m.timestamp),
cpu_percent: m.cpuPercent,
memory_bytes: m.memoryBytes,
connections: m.connections,
+2 -2
View File
@@ -15,7 +15,7 @@ async function main() {
// Stream 1: ORDERS - typical e-commerce order stream
await createStreamIfNotExists(jsm, {
name: 'ORDERS',
subjects: ['orders.>', 'orders.created', 'orders.updated', 'orders.shipped', 'orders.completed'],
subjects: ['orders.>'],
retention: RetentionPolicy.Limits,
storage: StorageType.File,
max_msgs: 100000,
@@ -37,7 +37,7 @@ async function main() {
// Stream 3: LOGS - log aggregation stream
await createStreamIfNotExists(jsm, {
name: 'LOGS',
subjects: ['logs.>', 'logs.info', 'logs.warn', 'logs.error'],
subjects: ['logs.>'],
retention: RetentionPolicy.Limits,
storage: StorageType.File,
max_msgs: 1000000,