@djangocfg/centrifugo 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +345 -34
- package/package.json +6 -4
- package/src/config.ts +1 -1
- package/src/core/client/CentrifugoRPCClient.ts +281 -0
- package/src/core/client/index.ts +5 -0
- package/src/core/index.ts +15 -0
- package/src/core/logger/LogsStore.ts +101 -0
- package/src/core/logger/createLogger.ts +79 -0
- package/src/core/logger/index.ts +9 -0
- package/src/core/types/index.ts +68 -0
- package/src/debug/ConnectionTab/ConnectionTab.tsx +160 -0
- package/src/debug/ConnectionTab/index.ts +5 -0
- package/src/debug/DebugPanel/DebugPanel.tsx +88 -0
- package/src/debug/DebugPanel/index.ts +5 -0
- package/src/debug/LogsTab/LogsTab.tsx +236 -0
- package/src/debug/LogsTab/index.ts +5 -0
- package/src/debug/SubscriptionsTab/SubscriptionsTab.tsx +135 -0
- package/src/debug/SubscriptionsTab/index.ts +5 -0
- package/src/debug/index.ts +11 -0
- package/src/hooks/index.ts +2 -5
- package/src/hooks/useSubscription.ts +66 -65
- package/src/index.ts +94 -13
- package/src/providers/CentrifugoProvider/CentrifugoProvider.tsx +380 -0
- package/src/providers/CentrifugoProvider/index.ts +6 -0
- package/src/providers/LogsProvider/LogsProvider.tsx +107 -0
- package/src/providers/LogsProvider/index.ts +6 -0
- package/src/providers/index.ts +9 -0
- package/API_GENERATOR.md +0 -253
- package/src/components/CentrifugoDebug.tsx +0 -182
- package/src/components/index.ts +0 -5
- package/src/context/CentrifugoProvider.tsx +0 -228
- package/src/context/index.ts +0 -5
- package/src/hooks/useLogger.ts +0 -69
- package/src/types/index.ts +0 -45
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Tab
|
|
3
|
+
*
|
|
4
|
+
* Shows WebSocket connection status, uptime, and controls.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { Wifi, WifiOff, Clock, RefreshCw } from 'lucide-react';
|
|
10
|
+
import {
|
|
11
|
+
Card,
|
|
12
|
+
CardContent,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
Badge,
|
|
16
|
+
Button,
|
|
17
|
+
Separator,
|
|
18
|
+
} from '@djangocfg/ui';
|
|
19
|
+
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Helper: Format uptime
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function formatUptime(seconds: number): string {
|
|
26
|
+
if (seconds === 0) return '0s';
|
|
27
|
+
|
|
28
|
+
const hours = Math.floor(seconds / 3600);
|
|
29
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
30
|
+
const secs = seconds % 60;
|
|
31
|
+
|
|
32
|
+
if (hours > 0) {
|
|
33
|
+
return `${hours}h ${minutes}m ${secs}s`;
|
|
34
|
+
} else if (minutes > 0) {
|
|
35
|
+
return `${minutes}m ${secs}s`;
|
|
36
|
+
} else {
|
|
37
|
+
return `${secs}s`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// Component
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export function ConnectionTab() {
|
|
46
|
+
const {
|
|
47
|
+
isConnected,
|
|
48
|
+
isConnecting,
|
|
49
|
+
connectionState,
|
|
50
|
+
error,
|
|
51
|
+
uptime,
|
|
52
|
+
connect,
|
|
53
|
+
disconnect,
|
|
54
|
+
reconnect,
|
|
55
|
+
} = useCentrifugo();
|
|
56
|
+
|
|
57
|
+
const statusIcon = isConnected ? (
|
|
58
|
+
<Wifi className="h-5 w-5 text-green-600" />
|
|
59
|
+
) : (
|
|
60
|
+
<WifiOff className="h-5 w-5 text-red-600" />
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const statusBadge = isConnected ? (
|
|
64
|
+
<Badge variant="default" className="flex items-center gap-1">
|
|
65
|
+
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
|
66
|
+
Connected
|
|
67
|
+
</Badge>
|
|
68
|
+
) : isConnecting ? (
|
|
69
|
+
<Badge variant="secondary" className="flex items-center gap-1">
|
|
70
|
+
<RefreshCw className="h-3 w-3 animate-spin" />
|
|
71
|
+
Connecting...
|
|
72
|
+
</Badge>
|
|
73
|
+
) : (
|
|
74
|
+
<Badge variant="destructive">Disconnected</Badge>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="space-y-4">
|
|
79
|
+
{/* Status Card */}
|
|
80
|
+
<Card>
|
|
81
|
+
<CardHeader>
|
|
82
|
+
<CardTitle className="flex items-center justify-between">
|
|
83
|
+
<span className="flex items-center gap-2">
|
|
84
|
+
{statusIcon}
|
|
85
|
+
Connection Status
|
|
86
|
+
</span>
|
|
87
|
+
{statusBadge}
|
|
88
|
+
</CardTitle>
|
|
89
|
+
</CardHeader>
|
|
90
|
+
<CardContent className="space-y-4">
|
|
91
|
+
{/* State */}
|
|
92
|
+
<div className="flex justify-between text-sm">
|
|
93
|
+
<span className="text-muted-foreground">State:</span>
|
|
94
|
+
<span className="font-mono font-medium capitalize">{connectionState}</span>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Uptime */}
|
|
98
|
+
{isConnected && (
|
|
99
|
+
<div className="flex justify-between text-sm">
|
|
100
|
+
<span className="text-muted-foreground">Uptime:</span>
|
|
101
|
+
<span className="font-mono font-medium flex items-center gap-1">
|
|
102
|
+
<Clock className="h-3 w-3" />
|
|
103
|
+
{formatUptime(uptime)}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Error */}
|
|
109
|
+
{error && (
|
|
110
|
+
<>
|
|
111
|
+
<Separator />
|
|
112
|
+
<div className="space-y-2">
|
|
113
|
+
<span className="text-sm text-muted-foreground">Error:</span>
|
|
114
|
+
<div className="text-xs text-red-600 bg-red-50 dark:bg-red-950/20 p-2 rounded">
|
|
115
|
+
{error.message}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Controls */}
|
|
122
|
+
<Separator />
|
|
123
|
+
<div className="flex gap-2">
|
|
124
|
+
{isConnected ? (
|
|
125
|
+
<>
|
|
126
|
+
<Button
|
|
127
|
+
variant="destructive"
|
|
128
|
+
size="sm"
|
|
129
|
+
onClick={disconnect}
|
|
130
|
+
className="flex-1"
|
|
131
|
+
>
|
|
132
|
+
Disconnect
|
|
133
|
+
</Button>
|
|
134
|
+
<Button
|
|
135
|
+
variant="outline"
|
|
136
|
+
size="sm"
|
|
137
|
+
onClick={reconnect}
|
|
138
|
+
className="flex-1"
|
|
139
|
+
>
|
|
140
|
+
<RefreshCw className="h-3 w-3 mr-1" />
|
|
141
|
+
Reconnect
|
|
142
|
+
</Button>
|
|
143
|
+
</>
|
|
144
|
+
) : (
|
|
145
|
+
<Button
|
|
146
|
+
variant="default"
|
|
147
|
+
size="sm"
|
|
148
|
+
onClick={connect}
|
|
149
|
+
disabled={isConnecting}
|
|
150
|
+
className="flex-1"
|
|
151
|
+
>
|
|
152
|
+
{isConnecting ? 'Connecting...' : 'Connect'}
|
|
153
|
+
</Button>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
</CardContent>
|
|
157
|
+
</Card>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Panel
|
|
3
|
+
*
|
|
4
|
+
* Main debug UI with FAB button + Sheet modal + Tabs.
|
|
5
|
+
* Visibility controlled by CentrifugoProvider (dev mode OR admin users).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { Bug } from 'lucide-react';
|
|
12
|
+
import {
|
|
13
|
+
Sheet,
|
|
14
|
+
SheetContent,
|
|
15
|
+
SheetHeader,
|
|
16
|
+
SheetTitle,
|
|
17
|
+
SheetDescription,
|
|
18
|
+
Tabs,
|
|
19
|
+
TabsList,
|
|
20
|
+
TabsTrigger,
|
|
21
|
+
TabsContent,
|
|
22
|
+
} from '@djangocfg/ui';
|
|
23
|
+
import { ConnectionTab } from '../ConnectionTab';
|
|
24
|
+
import { LogsTab } from '../LogsTab';
|
|
25
|
+
import { SubscriptionsTab } from '../SubscriptionsTab';
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// Component
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export function DebugPanel() {
|
|
32
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
33
|
+
const [activeTab, setActiveTab] = useState('connection');
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
{/* FAB Button (fixed bottom-left) */}
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => setIsOpen(true)}
|
|
40
|
+
className="rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-all duration-200 flex items-center justify-center"
|
|
41
|
+
style={{
|
|
42
|
+
position: 'fixed',
|
|
43
|
+
bottom: '1rem',
|
|
44
|
+
left: '1rem',
|
|
45
|
+
width: '56px',
|
|
46
|
+
height: '56px',
|
|
47
|
+
zIndex: 9999,
|
|
48
|
+
}}
|
|
49
|
+
aria-label="Open Centrifugo Debug Panel"
|
|
50
|
+
>
|
|
51
|
+
<Bug className="h-6 w-6" />
|
|
52
|
+
</button>
|
|
53
|
+
|
|
54
|
+
{/* Sheet Modal */}
|
|
55
|
+
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
|
56
|
+
<SheetContent side="right" className="w-full sm:max-w-2xl">
|
|
57
|
+
<SheetHeader>
|
|
58
|
+
<SheetTitle>Centrifugo Debug</SheetTitle>
|
|
59
|
+
<SheetDescription>
|
|
60
|
+
WebSocket connection status, logs, and subscriptions
|
|
61
|
+
</SheetDescription>
|
|
62
|
+
</SheetHeader>
|
|
63
|
+
|
|
64
|
+
{/* Tabs */}
|
|
65
|
+
<Tabs value={activeTab} onValueChange={setActiveTab} className="mt-6">
|
|
66
|
+
<TabsList className="grid w-full grid-cols-3">
|
|
67
|
+
<TabsTrigger value="connection">Connection</TabsTrigger>
|
|
68
|
+
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
69
|
+
<TabsTrigger value="subscriptions">Subscriptions</TabsTrigger>
|
|
70
|
+
</TabsList>
|
|
71
|
+
|
|
72
|
+
<TabsContent value="connection" className="mt-4">
|
|
73
|
+
<ConnectionTab />
|
|
74
|
+
</TabsContent>
|
|
75
|
+
|
|
76
|
+
<TabsContent value="logs" className="mt-4">
|
|
77
|
+
<LogsTab />
|
|
78
|
+
</TabsContent>
|
|
79
|
+
|
|
80
|
+
<TabsContent value="subscriptions" className="mt-4">
|
|
81
|
+
<SubscriptionsTab />
|
|
82
|
+
</TabsContent>
|
|
83
|
+
</Tabs>
|
|
84
|
+
</SheetContent>
|
|
85
|
+
</Sheet>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logs Tab
|
|
3
|
+
*
|
|
4
|
+
* Bash-like logs viewer with filters, search, and auto-scroll.
|
|
5
|
+
* Uses PrettyCode for syntax highlighting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { useRef, useEffect, useState } from 'react';
|
|
11
|
+
import { Trash2, Search, Filter } from 'lucide-react';
|
|
12
|
+
import moment from 'moment';
|
|
13
|
+
import {
|
|
14
|
+
Card,
|
|
15
|
+
CardContent,
|
|
16
|
+
CardHeader,
|
|
17
|
+
CardTitle,
|
|
18
|
+
Button,
|
|
19
|
+
Input,
|
|
20
|
+
Select,
|
|
21
|
+
SelectContent,
|
|
22
|
+
SelectItem,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
25
|
+
ScrollArea,
|
|
26
|
+
Badge,
|
|
27
|
+
PrettyCode,
|
|
28
|
+
} from '@djangocfg/ui';
|
|
29
|
+
import { useLogs } from '../../providers/LogsProvider';
|
|
30
|
+
import type { LogLevel, LogEntry } from '../../core/types';
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// Helpers
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function formatTimestamp(date: Date): string {
|
|
37
|
+
return moment(date).format('HH:mm:ss.SSS');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getLevelColor(level: LogLevel): string {
|
|
41
|
+
switch (level) {
|
|
42
|
+
case 'debug':
|
|
43
|
+
return 'text-blue-600 dark:text-blue-400';
|
|
44
|
+
case 'info':
|
|
45
|
+
return 'text-gray-600 dark:text-gray-400';
|
|
46
|
+
case 'success':
|
|
47
|
+
return 'text-green-600 dark:text-green-400';
|
|
48
|
+
case 'warning':
|
|
49
|
+
return 'text-yellow-600 dark:text-yellow-400';
|
|
50
|
+
case 'error':
|
|
51
|
+
return 'text-red-600 dark:text-red-400';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getLevelBadgeVariant(level: LogLevel): 'default' | 'secondary' | 'destructive' | 'outline' {
|
|
56
|
+
switch (level) {
|
|
57
|
+
case 'error':
|
|
58
|
+
return 'destructive';
|
|
59
|
+
case 'warning':
|
|
60
|
+
return 'outline';
|
|
61
|
+
case 'success':
|
|
62
|
+
return 'default';
|
|
63
|
+
default:
|
|
64
|
+
return 'secondary';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
69
|
+
// Component
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export function LogsTab() {
|
|
73
|
+
const { filteredLogs, filter, setFilter, clearLogs, count } = useLogs();
|
|
74
|
+
const [search, setSearch] = useState('');
|
|
75
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
76
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
77
|
+
|
|
78
|
+
// Auto-scroll to bottom when new logs arrive
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (autoScroll && scrollRef.current) {
|
|
81
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
82
|
+
}
|
|
83
|
+
}, [filteredLogs, autoScroll]);
|
|
84
|
+
|
|
85
|
+
// Handle search with debounce
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const timeout = setTimeout(() => {
|
|
88
|
+
setFilter({ search: search || undefined });
|
|
89
|
+
}, 300);
|
|
90
|
+
|
|
91
|
+
return () => clearTimeout(timeout);
|
|
92
|
+
}, [search, setFilter]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="space-y-4">
|
|
96
|
+
{/* Controls */}
|
|
97
|
+
<Card>
|
|
98
|
+
<CardHeader>
|
|
99
|
+
<CardTitle className="flex items-center justify-between">
|
|
100
|
+
<span className="flex items-center gap-2">
|
|
101
|
+
Logs
|
|
102
|
+
<Badge variant="secondary">{count}</Badge>
|
|
103
|
+
</span>
|
|
104
|
+
<div className="flex gap-2">
|
|
105
|
+
<Button
|
|
106
|
+
variant="outline"
|
|
107
|
+
size="sm"
|
|
108
|
+
onClick={() => setAutoScroll(!autoScroll)}
|
|
109
|
+
>
|
|
110
|
+
Auto-scroll: {autoScroll ? 'ON' : 'OFF'}
|
|
111
|
+
</Button>
|
|
112
|
+
<Button
|
|
113
|
+
variant="destructive"
|
|
114
|
+
size="sm"
|
|
115
|
+
onClick={clearLogs}
|
|
116
|
+
>
|
|
117
|
+
<Trash2 className="h-3 w-3 mr-1" />
|
|
118
|
+
Clear
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</CardTitle>
|
|
122
|
+
</CardHeader>
|
|
123
|
+
<CardContent className="space-y-3">
|
|
124
|
+
{/* Search */}
|
|
125
|
+
<div className="flex gap-2">
|
|
126
|
+
<div className="relative flex-1">
|
|
127
|
+
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
128
|
+
<Input
|
|
129
|
+
placeholder="Search logs..."
|
|
130
|
+
value={search}
|
|
131
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
132
|
+
className="pl-8"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Filters */}
|
|
138
|
+
<div className="flex gap-2">
|
|
139
|
+
<Select
|
|
140
|
+
value={filter.level || 'all'}
|
|
141
|
+
onValueChange={(value) =>
|
|
142
|
+
setFilter({ level: value === 'all' ? undefined : (value as LogLevel) })
|
|
143
|
+
}
|
|
144
|
+
>
|
|
145
|
+
<SelectTrigger className="w-[140px]">
|
|
146
|
+
<Filter className="h-3 w-3 mr-1" />
|
|
147
|
+
<SelectValue placeholder="Level" />
|
|
148
|
+
</SelectTrigger>
|
|
149
|
+
<SelectContent>
|
|
150
|
+
<SelectItem value="all">All Levels</SelectItem>
|
|
151
|
+
<SelectItem value="debug">Debug</SelectItem>
|
|
152
|
+
<SelectItem value="info">Info</SelectItem>
|
|
153
|
+
<SelectItem value="success">Success</SelectItem>
|
|
154
|
+
<SelectItem value="warning">Warning</SelectItem>
|
|
155
|
+
<SelectItem value="error">Error</SelectItem>
|
|
156
|
+
</SelectContent>
|
|
157
|
+
</Select>
|
|
158
|
+
|
|
159
|
+
<Select
|
|
160
|
+
value={filter.source || 'all'}
|
|
161
|
+
onValueChange={(value) =>
|
|
162
|
+
setFilter({ source: value === 'all' ? undefined : (value as LogEntry['source']) })
|
|
163
|
+
}
|
|
164
|
+
>
|
|
165
|
+
<SelectTrigger className="w-[140px]">
|
|
166
|
+
<Filter className="h-3 w-3 mr-1" />
|
|
167
|
+
<SelectValue placeholder="Source" />
|
|
168
|
+
</SelectTrigger>
|
|
169
|
+
<SelectContent>
|
|
170
|
+
<SelectItem value="all">All Sources</SelectItem>
|
|
171
|
+
<SelectItem value="client">Client</SelectItem>
|
|
172
|
+
<SelectItem value="provider">Provider</SelectItem>
|
|
173
|
+
<SelectItem value="subscription">Subscription</SelectItem>
|
|
174
|
+
<SelectItem value="system">System</SelectItem>
|
|
175
|
+
</SelectContent>
|
|
176
|
+
</Select>
|
|
177
|
+
</div>
|
|
178
|
+
</CardContent>
|
|
179
|
+
</Card>
|
|
180
|
+
|
|
181
|
+
{/* Logs Viewer (Bash-like) */}
|
|
182
|
+
<Card>
|
|
183
|
+
<CardContent className="p-0">
|
|
184
|
+
<ScrollArea ref={scrollRef} className="h-[400px] w-full">
|
|
185
|
+
<div className="p-4 font-mono text-xs space-y-1 bg-slate-950 text-slate-50">
|
|
186
|
+
{filteredLogs.length === 0 ? (
|
|
187
|
+
<div className="text-slate-500 text-center py-8">
|
|
188
|
+
No logs to display
|
|
189
|
+
</div>
|
|
190
|
+
) : (
|
|
191
|
+
filteredLogs.map((log) => (
|
|
192
|
+
<div key={log.id} className="flex gap-2 hover:bg-slate-900 px-1 py-0.5 rounded">
|
|
193
|
+
{/* Timestamp */}
|
|
194
|
+
<span className="text-slate-500 shrink-0">
|
|
195
|
+
[{formatTimestamp(log.timestamp)}]
|
|
196
|
+
</span>
|
|
197
|
+
|
|
198
|
+
{/* Level */}
|
|
199
|
+
<span className={`${getLevelColor(log.level)} font-bold uppercase shrink-0 w-16`}>
|
|
200
|
+
{log.level}
|
|
201
|
+
</span>
|
|
202
|
+
|
|
203
|
+
{/* Source */}
|
|
204
|
+
<span className="text-blue-400 shrink-0 w-24">
|
|
205
|
+
[{log.source}]
|
|
206
|
+
</span>
|
|
207
|
+
|
|
208
|
+
{/* Message */}
|
|
209
|
+
<span className="text-slate-200">{log.message}</span>
|
|
210
|
+
|
|
211
|
+
{/* Data (if present) */}
|
|
212
|
+
{log.data && (
|
|
213
|
+
<details className="text-slate-400 cursor-pointer">
|
|
214
|
+
<summary className="inline">
|
|
215
|
+
<Badge variant="outline" className="text-xs ml-2">
|
|
216
|
+
data
|
|
217
|
+
</Badge>
|
|
218
|
+
</summary>
|
|
219
|
+
<div className="mt-2 ml-4">
|
|
220
|
+
<PrettyCode
|
|
221
|
+
data={typeof log.data === 'object' ? log.data : { value: log.data }}
|
|
222
|
+
language="json"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
</details>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
))
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</ScrollArea>
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscriptions Tab
|
|
3
|
+
*
|
|
4
|
+
* Shows active channel subscriptions with controls.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { Fragment } from 'react';
|
|
10
|
+
import { Radio, Trash2 } from 'lucide-react';
|
|
11
|
+
import {
|
|
12
|
+
Card,
|
|
13
|
+
CardContent,
|
|
14
|
+
CardHeader,
|
|
15
|
+
CardTitle,
|
|
16
|
+
Badge,
|
|
17
|
+
Button,
|
|
18
|
+
ScrollArea,
|
|
19
|
+
Separator,
|
|
20
|
+
} from '@djangocfg/ui';
|
|
21
|
+
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Helper: Format subscription time
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function formatSubscribedTime(timestamp: number): string {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const diff = Math.floor((now - timestamp) / 1000);
|
|
30
|
+
|
|
31
|
+
if (diff < 60) {
|
|
32
|
+
return `${diff}s ago`;
|
|
33
|
+
} else if (diff < 3600) {
|
|
34
|
+
return `${Math.floor(diff / 60)}m ago`;
|
|
35
|
+
} else if (diff < 86400) {
|
|
36
|
+
return `${Math.floor(diff / 3600)}h ago`;
|
|
37
|
+
} else {
|
|
38
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Component
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export function SubscriptionsTab() {
|
|
47
|
+
const { activeSubscriptions, unsubscribe } = useCentrifugo();
|
|
48
|
+
|
|
49
|
+
const handleUnsubscribe = (channel: string) => {
|
|
50
|
+
unsubscribe(channel);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-4">
|
|
55
|
+
{/* Header Card */}
|
|
56
|
+
<Card>
|
|
57
|
+
<CardHeader>
|
|
58
|
+
<CardTitle className="flex items-center justify-between">
|
|
59
|
+
<span className="flex items-center gap-2">
|
|
60
|
+
<Radio className="h-5 w-5" />
|
|
61
|
+
Active Subscriptions
|
|
62
|
+
</span>
|
|
63
|
+
<Badge variant="secondary">{activeSubscriptions.length}</Badge>
|
|
64
|
+
</CardTitle>
|
|
65
|
+
</CardHeader>
|
|
66
|
+
</Card>
|
|
67
|
+
|
|
68
|
+
{/* Subscriptions List */}
|
|
69
|
+
<Card>
|
|
70
|
+
<CardContent className="p-0">
|
|
71
|
+
{activeSubscriptions.length === 0 ? (
|
|
72
|
+
<div className="p-8 text-center text-muted-foreground">
|
|
73
|
+
No active subscriptions
|
|
74
|
+
</div>
|
|
75
|
+
) : (
|
|
76
|
+
<ScrollArea className="h-[450px]">
|
|
77
|
+
<div className="p-4 space-y-3">
|
|
78
|
+
{activeSubscriptions.map((sub, index) => (
|
|
79
|
+
<Fragment key={sub.channel}>
|
|
80
|
+
{index > 0 && <Separator />}
|
|
81
|
+
<div className="flex items-start justify-between gap-4 p-3 rounded-lg hover:bg-muted/50 transition-colors">
|
|
82
|
+
<div className="flex-1 space-y-2">
|
|
83
|
+
{/* Channel Name */}
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<Radio className="h-4 w-4 text-green-600 animate-pulse" />
|
|
86
|
+
<span className="font-mono text-sm font-medium">
|
|
87
|
+
{sub.channel}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Metadata */}
|
|
92
|
+
<div className="flex gap-3 text-xs text-muted-foreground">
|
|
93
|
+
<span className="flex items-center gap-1">
|
|
94
|
+
<Badge variant="outline" className="text-xs">
|
|
95
|
+
{sub.type}
|
|
96
|
+
</Badge>
|
|
97
|
+
</span>
|
|
98
|
+
<span>
|
|
99
|
+
Subscribed: {formatSubscribedTime(sub.subscribedAt)}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Additional Info */}
|
|
104
|
+
{sub.data && (
|
|
105
|
+
<div className="text-xs text-muted-foreground">
|
|
106
|
+
<details className="cursor-pointer">
|
|
107
|
+
<summary className="inline">View data</summary>
|
|
108
|
+
<pre className="mt-2 p-2 bg-muted rounded text-xs overflow-auto">
|
|
109
|
+
{JSON.stringify(sub.data, null, 2)}
|
|
110
|
+
</pre>
|
|
111
|
+
</details>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* Unsubscribe Button */}
|
|
117
|
+
<Button
|
|
118
|
+
variant="ghost"
|
|
119
|
+
size="sm"
|
|
120
|
+
onClick={() => handleUnsubscribe(sub.channel)}
|
|
121
|
+
className="shrink-0"
|
|
122
|
+
>
|
|
123
|
+
<Trash2 className="h-3 w-3 text-red-600" />
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</Fragment>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</ScrollArea>
|
|
130
|
+
)}
|
|
131
|
+
</CardContent>
|
|
132
|
+
</Card>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Components
|
|
3
|
+
*
|
|
4
|
+
* Development-only debug UI for monitoring Centrifugo connections,
|
|
5
|
+
* logs, and subscriptions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { DebugPanel } from './DebugPanel';
|
|
9
|
+
export { ConnectionTab } from './ConnectionTab';
|
|
10
|
+
export { LogsTab } from './LogsTab';
|
|
11
|
+
export { SubscriptionsTab } from './SubscriptionsTab';
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Hooks Module
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export { useCentrifugoLogger, useRPCLogger } from './useLogger';
|
|
6
|
-
export type { CentrifugoLogger, RPCLogger } from './useLogger';
|
|
7
|
-
|
|
8
5
|
export { useSubscription } from './useSubscription';
|
|
9
|
-
export type {
|
|
6
|
+
export type { UseSubscriptionOptions, UseSubscriptionResult } from './useSubscription';
|