@djangocfg/centrifugo 1.0.3 → 1.0.4
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 +534 -83
- package/package.json +5 -5
- package/src/components/CentrifugoMonitor/CentrifugoMonitor.tsx +137 -0
- package/src/components/CentrifugoMonitor/CentrifugoMonitorDialog.tsx +64 -0
- package/src/components/CentrifugoMonitor/CentrifugoMonitorFAB.tsx +81 -0
- package/src/components/CentrifugoMonitor/CentrifugoMonitorWidget.tsx +74 -0
- package/src/components/CentrifugoMonitor/index.ts +14 -0
- package/src/components/ConnectionStatus/ConnectionStatus.tsx +192 -0
- package/src/components/ConnectionStatus/ConnectionStatusCard.tsx +56 -0
- package/src/components/ConnectionStatus/index.ts +9 -0
- package/src/components/MessagesFeed/MessageFilters.tsx +163 -0
- package/src/components/MessagesFeed/MessagesFeed.tsx +383 -0
- package/src/components/MessagesFeed/index.ts +9 -0
- package/src/components/MessagesFeed/types.ts +31 -0
- package/src/components/SubscriptionsList/SubscriptionsList.tsx +179 -0
- package/src/components/SubscriptionsList/index.ts +7 -0
- package/src/components/index.ts +18 -0
- package/src/core/client/CentrifugoRPCClient.ts +212 -15
- package/src/core/logger/createLogger.ts +26 -3
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useRPC.ts +149 -0
- package/src/hooks/useSubscription.ts +44 -10
- package/src/index.ts +3 -4
- package/src/providers/CentrifugoProvider/CentrifugoProvider.tsx +3 -20
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/centrifugo",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "WebSocket RPC
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Production-ready Centrifugo WebSocket client with React integration, RPC pattern, composable UI components, and comprehensive monitoring tools",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"centrifuge": "^5.2.2"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@djangocfg/ui": "^1.2.
|
|
23
|
-
"@djangocfg/layouts": "^1.2.
|
|
22
|
+
"@djangocfg/ui": "^1.2.29",
|
|
23
|
+
"@djangocfg/layouts": "^1.2.29",
|
|
24
24
|
"consola": "^3.4.2",
|
|
25
25
|
"lucide-react": "^0.468.0",
|
|
26
26
|
"moment": "^2.30.1",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"react-dom": "^19.0.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@djangocfg/typescript-config": "^1.2.
|
|
31
|
+
"@djangocfg/typescript-config": "^1.2.29",
|
|
32
32
|
"@types/react": "^19.0.6",
|
|
33
33
|
"@types/react-dom": "^19.0.2",
|
|
34
34
|
"moment": "^2.30.1",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrifugo Monitor Component
|
|
3
|
+
*
|
|
4
|
+
* Universal monitoring component that can be embedded anywhere
|
|
5
|
+
* Combines ConnectionStatus, MessagesFeed, and SubscriptionsList
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@djangocfg/ui';
|
|
12
|
+
import { ConnectionStatus } from '../ConnectionStatus';
|
|
13
|
+
import { MessagesFeed } from '../MessagesFeed';
|
|
14
|
+
import { SubscriptionsList } from '../SubscriptionsList';
|
|
15
|
+
import type { CentrifugoMessage } from '../MessagesFeed';
|
|
16
|
+
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Types
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface CentrifugoMonitorProps {
|
|
22
|
+
variant?: 'compact' | 'full' | 'minimal';
|
|
23
|
+
showConnectionStatus?: boolean;
|
|
24
|
+
showMessagesFeed?: boolean;
|
|
25
|
+
showSubscriptions?: boolean;
|
|
26
|
+
showFilters?: boolean;
|
|
27
|
+
showControls?: boolean;
|
|
28
|
+
maxMessages?: number;
|
|
29
|
+
channels?: string[]; // Pre-filter by channels
|
|
30
|
+
autoScroll?: boolean;
|
|
31
|
+
onMessageClick?: (message: CentrifugoMessage) => void;
|
|
32
|
+
onSubscriptionClick?: (channel: string) => void;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
37
|
+
// Component
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export function CentrifugoMonitor({
|
|
41
|
+
variant = 'full',
|
|
42
|
+
showConnectionStatus = true,
|
|
43
|
+
showMessagesFeed = true,
|
|
44
|
+
showSubscriptions = true,
|
|
45
|
+
showFilters = true,
|
|
46
|
+
showControls = true,
|
|
47
|
+
maxMessages = 100,
|
|
48
|
+
channels = [],
|
|
49
|
+
autoScroll = true,
|
|
50
|
+
onMessageClick,
|
|
51
|
+
onSubscriptionClick,
|
|
52
|
+
className = '',
|
|
53
|
+
}: CentrifugoMonitorProps) {
|
|
54
|
+
// Minimal variant - only connection status
|
|
55
|
+
if (variant === 'minimal') {
|
|
56
|
+
return (
|
|
57
|
+
<div className={className}>
|
|
58
|
+
<ConnectionStatus variant="detailed" showUptime showSubscriptions />
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Compact variant - connection status + messages feed
|
|
64
|
+
if (variant === 'compact') {
|
|
65
|
+
return (
|
|
66
|
+
<div className={`space-y-4 ${className}`}>
|
|
67
|
+
{showConnectionStatus && (
|
|
68
|
+
<ConnectionStatus variant="detailed" showUptime showSubscriptions />
|
|
69
|
+
)}
|
|
70
|
+
{showMessagesFeed && (
|
|
71
|
+
<MessagesFeed
|
|
72
|
+
maxMessages={maxMessages}
|
|
73
|
+
showFilters={false}
|
|
74
|
+
showControls={showControls}
|
|
75
|
+
channels={channels}
|
|
76
|
+
autoScroll={autoScroll}
|
|
77
|
+
onMessageClick={onMessageClick}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Full variant - tabs with all features
|
|
85
|
+
const tabsToShow = [
|
|
86
|
+
showConnectionStatus && { value: 'connection', label: 'Connection' },
|
|
87
|
+
showMessagesFeed && { value: 'messages', label: 'Messages' },
|
|
88
|
+
showSubscriptions && { value: 'subscriptions', label: 'Subscriptions' },
|
|
89
|
+
].filter(Boolean) as { value: string; label: string }[];
|
|
90
|
+
|
|
91
|
+
if (tabsToShow.length === 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={className}>
|
|
97
|
+
<Tabs defaultValue={tabsToShow[0].value}>
|
|
98
|
+
<TabsList className={`grid w-full grid-cols-${tabsToShow.length}`}>
|
|
99
|
+
{tabsToShow.map((tab) => (
|
|
100
|
+
<TabsTrigger key={tab.value} value={tab.value}>
|
|
101
|
+
{tab.label}
|
|
102
|
+
</TabsTrigger>
|
|
103
|
+
))}
|
|
104
|
+
</TabsList>
|
|
105
|
+
|
|
106
|
+
{showConnectionStatus && (
|
|
107
|
+
<TabsContent value="connection" className="mt-4">
|
|
108
|
+
<ConnectionStatus variant="detailed" showUptime showSubscriptions />
|
|
109
|
+
</TabsContent>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{showMessagesFeed && (
|
|
113
|
+
<TabsContent value="messages" className="mt-4">
|
|
114
|
+
<MessagesFeed
|
|
115
|
+
maxMessages={maxMessages}
|
|
116
|
+
showFilters={showFilters}
|
|
117
|
+
showControls={showControls}
|
|
118
|
+
channels={channels}
|
|
119
|
+
autoScroll={autoScroll}
|
|
120
|
+
onMessageClick={onMessageClick}
|
|
121
|
+
/>
|
|
122
|
+
</TabsContent>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{showSubscriptions && (
|
|
126
|
+
<TabsContent value="subscriptions" className="mt-4">
|
|
127
|
+
<SubscriptionsList
|
|
128
|
+
showControls={showControls}
|
|
129
|
+
onSubscriptionClick={onSubscriptionClick}
|
|
130
|
+
/>
|
|
131
|
+
</TabsContent>
|
|
132
|
+
)}
|
|
133
|
+
</Tabs>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrifugo Monitor Dialog Component
|
|
3
|
+
*
|
|
4
|
+
* Sheet/Dialog wrapper for CentrifugoMonitor
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import {
|
|
11
|
+
Sheet,
|
|
12
|
+
SheetContent,
|
|
13
|
+
SheetHeader,
|
|
14
|
+
SheetTitle,
|
|
15
|
+
SheetDescription,
|
|
16
|
+
} from '@djangocfg/ui';
|
|
17
|
+
import { Activity } from 'lucide-react';
|
|
18
|
+
import { CentrifugoMonitor, type CentrifugoMonitorProps } from './CentrifugoMonitor';
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Types
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface CentrifugoMonitorDialogProps extends Omit<CentrifugoMonitorProps, 'className'> {
|
|
25
|
+
open: boolean;
|
|
26
|
+
onOpenChange: (open: boolean) => void;
|
|
27
|
+
title?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
side?: 'left' | 'right' | 'top' | 'bottom';
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
34
|
+
// Component
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export function CentrifugoMonitorDialog({
|
|
38
|
+
open,
|
|
39
|
+
onOpenChange,
|
|
40
|
+
title = 'Centrifugo Monitor',
|
|
41
|
+
description = 'Real-time WebSocket monitoring and debugging',
|
|
42
|
+
side = 'right',
|
|
43
|
+
className = '',
|
|
44
|
+
...monitorProps
|
|
45
|
+
}: CentrifugoMonitorDialogProps) {
|
|
46
|
+
return (
|
|
47
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
48
|
+
<SheetContent side={side} className={`w-full sm:max-w-2xl ${className}`}>
|
|
49
|
+
<SheetHeader>
|
|
50
|
+
<SheetTitle className="flex items-center gap-2">
|
|
51
|
+
<Activity className="h-5 w-5" />
|
|
52
|
+
{title}
|
|
53
|
+
</SheetTitle>
|
|
54
|
+
{description && <SheetDescription>{description}</SheetDescription>}
|
|
55
|
+
</SheetHeader>
|
|
56
|
+
|
|
57
|
+
<div className="mt-6">
|
|
58
|
+
<CentrifugoMonitor {...monitorProps} />
|
|
59
|
+
</div>
|
|
60
|
+
</SheetContent>
|
|
61
|
+
</Sheet>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrifugo Monitor FAB Component
|
|
3
|
+
*
|
|
4
|
+
* Floating Action Button that opens CentrifugoMonitorDialog
|
|
5
|
+
* Replaces the old DebugPanel FAB
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { useState } from 'react';
|
|
11
|
+
import { Activity } from 'lucide-react';
|
|
12
|
+
import { CentrifugoMonitorDialog, type CentrifugoMonitorDialogProps } from './CentrifugoMonitorDialog';
|
|
13
|
+
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// Types
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface CentrifugoMonitorFABProps extends Omit<CentrifugoMonitorDialogProps, 'open' | 'onOpenChange'> {
|
|
19
|
+
position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
|
|
20
|
+
size?: 'sm' | 'md' | 'lg';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Component
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export function CentrifugoMonitorFAB({
|
|
28
|
+
position = 'bottom-left',
|
|
29
|
+
size = 'md',
|
|
30
|
+
...dialogProps
|
|
31
|
+
}: CentrifugoMonitorFABProps) {
|
|
32
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
33
|
+
|
|
34
|
+
// Position styles
|
|
35
|
+
const positionStyles = {
|
|
36
|
+
'bottom-left': { bottom: '1rem', left: '1rem' },
|
|
37
|
+
'bottom-right': { bottom: '1rem', right: '1rem' },
|
|
38
|
+
'top-left': { top: '1rem', left: '1rem' },
|
|
39
|
+
'top-right': { top: '1rem', right: '1rem' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Size styles
|
|
43
|
+
const sizeStyles = {
|
|
44
|
+
sm: { width: '48px', height: '48px' },
|
|
45
|
+
md: { width: '56px', height: '56px' },
|
|
46
|
+
lg: { width: '64px', height: '64px' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const iconSizes = {
|
|
50
|
+
sm: 'h-5 w-5',
|
|
51
|
+
md: 'h-6 w-6',
|
|
52
|
+
lg: 'h-7 w-7',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{/* FAB Button */}
|
|
58
|
+
<button
|
|
59
|
+
onClick={() => setIsOpen(true)}
|
|
60
|
+
className="rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-all duration-200 flex items-center justify-center hover:scale-110"
|
|
61
|
+
style={{
|
|
62
|
+
position: 'fixed',
|
|
63
|
+
...positionStyles[position],
|
|
64
|
+
...sizeStyles[size],
|
|
65
|
+
zIndex: 9999,
|
|
66
|
+
}}
|
|
67
|
+
aria-label="Open Centrifugo Monitor"
|
|
68
|
+
>
|
|
69
|
+
<Activity className={iconSizes[size]} />
|
|
70
|
+
</button>
|
|
71
|
+
|
|
72
|
+
{/* Dialog */}
|
|
73
|
+
<CentrifugoMonitorDialog
|
|
74
|
+
open={isOpen}
|
|
75
|
+
onOpenChange={setIsOpen}
|
|
76
|
+
{...dialogProps}
|
|
77
|
+
/>
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrifugo Monitor Widget Component
|
|
3
|
+
*
|
|
4
|
+
* Card-based widget for dashboards
|
|
5
|
+
* Combines ConnectionStatusCard with compact monitor view
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { useState } from 'react';
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle, Button } from '@djangocfg/ui';
|
|
12
|
+
import { Activity, Maximize2 } from 'lucide-react';
|
|
13
|
+
import { ConnectionStatus } from '../ConnectionStatus';
|
|
14
|
+
import { CentrifugoMonitorDialog } from './CentrifugoMonitorDialog';
|
|
15
|
+
import type { CentrifugoMonitorProps } from './CentrifugoMonitor';
|
|
16
|
+
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Types
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface CentrifugoMonitorWidgetProps extends Omit<CentrifugoMonitorProps, 'variant' | 'className'> {
|
|
22
|
+
title?: string;
|
|
23
|
+
showExpandButton?: boolean;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// Component
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export function CentrifugoMonitorWidget({
|
|
32
|
+
title = 'WebSocket Monitor',
|
|
33
|
+
showExpandButton = true,
|
|
34
|
+
className = '',
|
|
35
|
+
...monitorProps
|
|
36
|
+
}: CentrifugoMonitorWidgetProps) {
|
|
37
|
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<Card className={className}>
|
|
42
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
43
|
+
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
44
|
+
<Activity className="h-4 w-4" />
|
|
45
|
+
{title}
|
|
46
|
+
</CardTitle>
|
|
47
|
+
{showExpandButton && (
|
|
48
|
+
<Button
|
|
49
|
+
size="sm"
|
|
50
|
+
variant="ghost"
|
|
51
|
+
onClick={() => setIsDialogOpen(true)}
|
|
52
|
+
>
|
|
53
|
+
<Maximize2 className="h-4 w-4" />
|
|
54
|
+
</Button>
|
|
55
|
+
)}
|
|
56
|
+
</CardHeader>
|
|
57
|
+
<CardContent>
|
|
58
|
+
<ConnectionStatus variant="detailed" showUptime showSubscriptions />
|
|
59
|
+
</CardContent>
|
|
60
|
+
</Card>
|
|
61
|
+
|
|
62
|
+
{/* Full monitor dialog */}
|
|
63
|
+
{showExpandButton && (
|
|
64
|
+
<CentrifugoMonitorDialog
|
|
65
|
+
open={isDialogOpen}
|
|
66
|
+
onOpenChange={setIsDialogOpen}
|
|
67
|
+
variant="full"
|
|
68
|
+
{...monitorProps}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrifugo Monitor Components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { CentrifugoMonitor } from './CentrifugoMonitor';
|
|
6
|
+
export { CentrifugoMonitorDialog } from './CentrifugoMonitorDialog';
|
|
7
|
+
export { CentrifugoMonitorFAB } from './CentrifugoMonitorFAB';
|
|
8
|
+
export { CentrifugoMonitorWidget } from './CentrifugoMonitorWidget';
|
|
9
|
+
|
|
10
|
+
export type { CentrifugoMonitorProps } from './CentrifugoMonitor';
|
|
11
|
+
export type { CentrifugoMonitorDialogProps } from './CentrifugoMonitorDialog';
|
|
12
|
+
export type { CentrifugoMonitorFABProps } from './CentrifugoMonitorFAB';
|
|
13
|
+
export type { CentrifugoMonitorWidgetProps } from './CentrifugoMonitorWidget';
|
|
14
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Status Component
|
|
3
|
+
*
|
|
4
|
+
* Universal component for displaying Centrifugo connection status
|
|
5
|
+
* Supports multiple variants: badge, inline, card, detailed
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { useState, useEffect } from 'react';
|
|
11
|
+
import { Badge } from '@djangocfg/ui';
|
|
12
|
+
import { Wifi, WifiOff, Radio, Clock } from 'lucide-react';
|
|
13
|
+
import moment from 'moment';
|
|
14
|
+
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
|
15
|
+
import { createLogger } from '../../core/logger/createLogger';
|
|
16
|
+
|
|
17
|
+
const logger = createLogger('ConnectionStatus');
|
|
18
|
+
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
20
|
+
// Types
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface ConnectionStatusProps {
|
|
24
|
+
variant?: 'badge' | 'inline' | 'detailed';
|
|
25
|
+
showUptime?: boolean;
|
|
26
|
+
showSubscriptions?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Component
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export function ConnectionStatus({
|
|
35
|
+
variant = 'badge',
|
|
36
|
+
showUptime = false,
|
|
37
|
+
showSubscriptions = false,
|
|
38
|
+
className = '',
|
|
39
|
+
}: ConnectionStatusProps) {
|
|
40
|
+
const { isConnected, client } = useCentrifugo();
|
|
41
|
+
const [connectionTime, setConnectionTime] = useState<moment.Moment | null>(null);
|
|
42
|
+
const [uptime, setUptime] = useState<string>('');
|
|
43
|
+
const [activeSubscriptions, setActiveSubscriptions] = useState<number>(0);
|
|
44
|
+
|
|
45
|
+
// Track connection time
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (isConnected && !connectionTime) {
|
|
48
|
+
setConnectionTime(moment.utc());
|
|
49
|
+
} else if (!isConnected) {
|
|
50
|
+
setConnectionTime(null);
|
|
51
|
+
}
|
|
52
|
+
}, [isConnected, connectionTime]);
|
|
53
|
+
|
|
54
|
+
// Update uptime every second
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!isConnected || !connectionTime) {
|
|
57
|
+
setUptime('');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const updateUptime = () => {
|
|
62
|
+
const now = moment.utc();
|
|
63
|
+
const duration = moment.duration(now.diff(connectionTime));
|
|
64
|
+
|
|
65
|
+
const hours = Math.floor(duration.asHours());
|
|
66
|
+
const minutes = duration.minutes();
|
|
67
|
+
const seconds = duration.seconds();
|
|
68
|
+
|
|
69
|
+
if (hours > 0) {
|
|
70
|
+
setUptime(`${hours}h ${minutes}m`);
|
|
71
|
+
} else if (minutes > 0) {
|
|
72
|
+
setUptime(`${minutes}m ${seconds}s`);
|
|
73
|
+
} else {
|
|
74
|
+
setUptime(`${seconds}s`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
updateUptime();
|
|
79
|
+
const interval = setInterval(updateUptime, 1000);
|
|
80
|
+
|
|
81
|
+
return () => clearInterval(interval);
|
|
82
|
+
}, [isConnected, connectionTime]);
|
|
83
|
+
|
|
84
|
+
// Update active subscriptions count
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!client || !isConnected) {
|
|
87
|
+
setActiveSubscriptions(0);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const updateCount = () => {
|
|
92
|
+
try {
|
|
93
|
+
const centrifuge = client.getCentrifuge();
|
|
94
|
+
const subs = centrifuge.subscriptions();
|
|
95
|
+
setActiveSubscriptions(Object.keys(subs).length);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger.error('Failed to get active subscriptions', error);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
updateCount();
|
|
102
|
+
const interval = setInterval(updateCount, 2000);
|
|
103
|
+
|
|
104
|
+
return () => clearInterval(interval);
|
|
105
|
+
}, [client, isConnected]);
|
|
106
|
+
|
|
107
|
+
// Badge variant
|
|
108
|
+
if (variant === 'badge') {
|
|
109
|
+
return (
|
|
110
|
+
<Badge
|
|
111
|
+
variant={isConnected ? 'default' : 'destructive'}
|
|
112
|
+
className={`flex items-center gap-1 ${isConnected ? 'animate-pulse' : ''} ${className}`}
|
|
113
|
+
>
|
|
114
|
+
<span className={`h-2 w-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
|
|
115
|
+
{isConnected ? 'Connected' : 'Disconnected'}
|
|
116
|
+
</Badge>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Inline variant
|
|
121
|
+
if (variant === 'inline') {
|
|
122
|
+
return (
|
|
123
|
+
<div className={`flex items-center gap-2 ${className}`}>
|
|
124
|
+
{isConnected ? (
|
|
125
|
+
<Wifi className="h-4 w-4 text-green-600" />
|
|
126
|
+
) : (
|
|
127
|
+
<WifiOff className="h-4 w-4 text-red-600" />
|
|
128
|
+
)}
|
|
129
|
+
<span className="text-sm font-medium">
|
|
130
|
+
{isConnected ? 'Connected' : 'Disconnected'}
|
|
131
|
+
</span>
|
|
132
|
+
{showUptime && uptime && (
|
|
133
|
+
<span className="text-xs text-muted-foreground">({uptime})</span>
|
|
134
|
+
)}
|
|
135
|
+
{showSubscriptions && (
|
|
136
|
+
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
|
137
|
+
<Radio className="h-3 w-3" />
|
|
138
|
+
{activeSubscriptions}
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Detailed variant
|
|
146
|
+
return (
|
|
147
|
+
<div className={`space-y-3 ${className}`}>
|
|
148
|
+
{/* Status Badge */}
|
|
149
|
+
<div className="flex items-center gap-2">
|
|
150
|
+
<Badge
|
|
151
|
+
variant={isConnected ? 'default' : 'destructive'}
|
|
152
|
+
className={`flex items-center gap-1 ${isConnected ? 'animate-pulse' : ''}`}
|
|
153
|
+
>
|
|
154
|
+
<span className={`h-2 w-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
|
|
155
|
+
{isConnected ? 'Connected' : 'Disconnected'}
|
|
156
|
+
</Badge>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Connection Info */}
|
|
160
|
+
{isConnected ? (
|
|
161
|
+
<div className="space-y-2">
|
|
162
|
+
{/* Uptime */}
|
|
163
|
+
{showUptime && uptime && (
|
|
164
|
+
<div className="flex items-center justify-between text-xs">
|
|
165
|
+
<span className="text-muted-foreground flex items-center gap-1">
|
|
166
|
+
<Clock className="h-3 w-3" />
|
|
167
|
+
Uptime:
|
|
168
|
+
</span>
|
|
169
|
+
<span className="font-mono font-medium">{uptime}</span>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{/* Active Subscriptions */}
|
|
174
|
+
{showSubscriptions && (
|
|
175
|
+
<div className="flex items-center justify-between text-xs">
|
|
176
|
+
<span className="text-muted-foreground flex items-center gap-1">
|
|
177
|
+
<Radio className="h-3 w-3" />
|
|
178
|
+
Subscriptions:
|
|
179
|
+
</span>
|
|
180
|
+
<span className="font-mono font-medium">{activeSubscriptions}</span>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
) : (
|
|
185
|
+
<div className="text-xs text-muted-foreground p-2 rounded bg-red-50 dark:bg-red-950/20">
|
|
186
|
+
Real-time features unavailable
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Status Card Component
|
|
3
|
+
*
|
|
4
|
+
* Card wrapper for ConnectionStatus - ready for dashboard widgets
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@djangocfg/ui';
|
|
11
|
+
import { Wifi, WifiOff } from 'lucide-react';
|
|
12
|
+
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
|
13
|
+
import { ConnectionStatus } from './ConnectionStatus';
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Types
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface ConnectionStatusCardProps {
|
|
20
|
+
showUptime?: boolean;
|
|
21
|
+
showSubscriptions?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Component
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export function ConnectionStatusCard({
|
|
30
|
+
showUptime = true,
|
|
31
|
+
showSubscriptions = true,
|
|
32
|
+
className = '',
|
|
33
|
+
}: ConnectionStatusCardProps) {
|
|
34
|
+
const { isConnected } = useCentrifugo();
|
|
35
|
+
|
|
36
|
+
const statusColor = isConnected ? 'border-green-500' : 'border-red-500';
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Card className={`${statusColor} ${className}`} style={{ borderLeftWidth: '4px' }}>
|
|
40
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
41
|
+
<CardTitle className="text-sm font-medium">WebSocket Status</CardTitle>
|
|
42
|
+
<div className={isConnected ? 'text-green-600' : 'text-red-600'}>
|
|
43
|
+
{isConnected ? <Wifi className="h-4 w-4" /> : <WifiOff className="h-4 w-4" />}
|
|
44
|
+
</div>
|
|
45
|
+
</CardHeader>
|
|
46
|
+
<CardContent>
|
|
47
|
+
<ConnectionStatus
|
|
48
|
+
variant="detailed"
|
|
49
|
+
showUptime={showUptime}
|
|
50
|
+
showSubscriptions={showSubscriptions}
|
|
51
|
+
/>
|
|
52
|
+
</CardContent>
|
|
53
|
+
</Card>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Status Components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { ConnectionStatus } from './ConnectionStatus';
|
|
6
|
+
export { ConnectionStatusCard } from './ConnectionStatusCard';
|
|
7
|
+
export type { ConnectionStatusProps } from './ConnectionStatus';
|
|
8
|
+
export type { ConnectionStatusCardProps } from './ConnectionStatusCard';
|
|
9
|
+
|