diff --git a/frontend/src/components/AlertBasicView.tsx b/frontend/src/components/AlertBasicView.tsx index 358230e..8ecaeec 100644 --- a/frontend/src/components/AlertBasicView.tsx +++ b/frontend/src/components/AlertBasicView.tsx @@ -75,7 +75,7 @@ export default function AlertBasicView({ record, onOpenResource }: AlertBasicVie
{mono(stringValue(record, 'rule_id'))} {stringValue(record, 'rule_name')} - {mono(stringValue(record, 'correlation_uid'))} + {mono(stringValue(record, 'correlation_uid'))}
@@ -86,10 +86,10 @@ export default function AlertBasicView({ record, onOpenResource }: AlertBasicVie
- {statusTag(String(value(record, 'status') || ''))} - {block(stringValue(record, 'status_detail'))} {alertDispositionTag(String(value(record, 'disposition') || ''))} {alertActionTag(String(value(record, 'action') || ''))} + {statusTag(String(value(record, 'status') || ''))} + {block(stringValue(record, 'status_detail'))} {block(stringValue(record, 'remediation'))}
@@ -106,7 +106,7 @@ export default function AlertBasicView({ record, onOpenResource }: AlertBasicVie
{stringValue(record, 'analytic_name')} {alertAnalyticTypeTag(String(value(record, 'analytic_type') || ''))} - {choiceTag(String(value(record, 'analytic_state') || ''), 'green')} + {choiceTag(String(value(record, 'analytic_state') || ''), 'green')} {block(stringValue(record, 'analytic_desc'))} {stringValue(record, 'policy_name')} {choiceTag(String(value(record, 'policy_type') || ''), 'volcano')} diff --git a/frontend/src/components/AppMessageProvider.tsx b/frontend/src/components/AppMessageProvider.tsx new file mode 100644 index 0000000..92898ec --- /dev/null +++ b/frontend/src/components/AppMessageProvider.tsx @@ -0,0 +1,14 @@ +import {useLayoutEffect} from 'react' +import {App as AntApp} from 'antd' +import {clearAppMessage, setAppMessage} from '../utils/appMessage' + +export default function AppMessageProvider() { + const {message} = AntApp.useApp() + + useLayoutEffect(() => { + setAppMessage(message) + return () => clearAppMessage(message) + }, [message]) + + return null +} diff --git a/frontend/src/components/AvatarUpload.tsx b/frontend/src/components/AvatarUpload.tsx index 43d85d0..5e8d7fe 100644 --- a/frontend/src/components/AvatarUpload.tsx +++ b/frontend/src/components/AvatarUpload.tsx @@ -1,5 +1,6 @@ import type {UploadProps} from 'antd' -import {message, Upload} from 'antd' +import {Upload} from 'antd' +import {message} from '../utils/appMessage' import {CameraOutlined} from '@ant-design/icons' import ImgCrop from 'antd-img-crop' import client from '../api/client' diff --git a/frontend/src/components/CaseKnowledgeView.tsx b/frontend/src/components/CaseKnowledgeView.tsx index b3acafc..e645389 100644 --- a/frontend/src/components/CaseKnowledgeView.tsx +++ b/frontend/src/components/CaseKnowledgeView.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react' -import {Empty, message, Spin, theme} from 'antd' +import {Empty, Spin, theme} from 'antd' +import {message} from '../utils/appMessage' import client from '../api/client' import type {EditableFieldConfig, FieldEditingController, OpenResourceOptions} from '../types/records' import DetailDraftActionBar from './DetailDraftActionBar' diff --git a/frontend/src/components/CasePlaybookRunModal.tsx b/frontend/src/components/CasePlaybookRunModal.tsx index b803c09..41b0242 100644 --- a/frontend/src/components/CasePlaybookRunModal.tsx +++ b/frontend/src/components/CasePlaybookRunModal.tsx @@ -1,5 +1,6 @@ import {type CSSProperties, useCallback, useEffect, useMemo, useRef, useState} from 'react' -import {Alert, Button, Empty, Input, message, Modal, Space, Table, Tag, theme, Tooltip, Typography} from 'antd' +import {Alert, Button, Empty, Input, Modal, Space, Table, Tag, theme, Tooltip, Typography} from 'antd' +import {message} from '../utils/appMessage' import {ReloadOutlined, SearchOutlined, ThunderboltOutlined} from '@ant-design/icons' import type {ColumnsType} from 'antd/es/table' import client from '../api/client' diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/DataTable.tsx index 8045dfd..581f090 100644 --- a/frontend/src/components/DataTable.tsx +++ b/frontend/src/components/DataTable.tsx @@ -1,6 +1,7 @@ import type {Key} from 'react' import {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react' -import {Button, Checkbox, Divider, Input, message, Pagination, Popconfirm, Popover, Select, Space, Table, Tooltip} from 'antd' +import {Button, Checkbox, Divider, Input, Pagination, Popconfirm, Popover, Select, Space, Table, Tooltip} from 'antd' +import {message} from '../utils/appMessage' import {ClearOutlined, CloseOutlined, DeleteOutlined, FilterOutlined, HolderOutlined, PushpinFilled, PushpinOutlined, ReloadOutlined, SettingOutlined} from '@ant-design/icons' import {closestCenter, DndContext, type DragEndEvent, PointerSensor, useSensor, useSensors} from '@dnd-kit/core' import {arrayMove, SortableContext, useSortable, verticalListSortingStrategy} from '@dnd-kit/sortable' diff --git a/frontend/src/components/DiscussionThread.tsx b/frontend/src/components/DiscussionThread.tsx index 8ab0b94..884a9ef 100644 --- a/frontend/src/components/DiscussionThread.tsx +++ b/frontend/src/components/DiscussionThread.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useMemo, useState} from 'react' -import {Alert, Button, Input, List, message, Popconfirm, Space, Tooltip} from 'antd' +import {Alert, Button, Input, List, Popconfirm, Space, Tooltip} from 'antd' +import {message} from '../utils/appMessage' import {MessageOutlined, ReloadOutlined, SearchOutlined} from '@ant-design/icons' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' @@ -170,7 +171,6 @@ export default function DiscussionThread({ contentType, objectId }: DiscussionTh useEffect(() => { if (reconnectToken === 0) return - // eslint-disable-next-line react-hooks/set-state-in-effect refreshComments() }, [reconnectToken, refreshComments]) diff --git a/frontend/src/components/EnrichmentCreateModal.tsx b/frontend/src/components/EnrichmentCreateModal.tsx index 0a52208..28d61b4 100644 --- a/frontend/src/components/EnrichmentCreateModal.tsx +++ b/frontend/src/components/EnrichmentCreateModal.tsx @@ -1,6 +1,7 @@ import {useEffect, useState} from 'react' import {ExperimentOutlined} from '@ant-design/icons' -import {Form, Input, message, Modal, Select, theme} from 'antd' +import {Form, Input, Modal, Select, theme} from 'antd' +import {message} from '../utils/appMessage' import client from '../api/client' import {fetchResourceMetadata} from '../api/metadata' import type {ChoiceOption} from '../types/records' diff --git a/frontend/src/components/InboxDrawer.tsx b/frontend/src/components/InboxDrawer.tsx index 9342a5c..b26a86b 100644 --- a/frontend/src/components/InboxDrawer.tsx +++ b/frontend/src/components/InboxDrawer.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useState} from 'react' -import {Alert, Badge, Button, Drawer, Empty, List, message, Popconfirm, Segmented, Space, Spin, Tag, Tooltip, Typography} from 'antd' +import {Alert, Badge, Button, Drawer, Empty, List, Popconfirm, Segmented, Space, Spin, Tag, Tooltip, Typography} from 'antd' +import {message} from '../utils/appMessage' import {CheckCircleOutlined, CloseOutlined, InboxOutlined, MailOutlined, MessageOutlined, ReloadOutlined} from '@ant-design/icons' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' @@ -222,7 +223,6 @@ export default function InboxDrawer({ onOpenResource }: InboxDrawerProps) { useEffect(() => { if (!open || reconnectToken === 0) return - // eslint-disable-next-line react-hooks/set-state-in-effect refreshMessages() }, [open, reconnectToken, refreshMessages]) @@ -379,7 +379,7 @@ export default function InboxDrawer({ onOpenResource }: InboxDrawerProps) { )} title={( -
+
{row.kind} @@ -423,7 +423,7 @@ export default function InboxDrawer({ onOpenResource }: InboxDrawerProps) {
)} - )} + )} /> ) : loadError ? ( diff --git a/frontend/src/components/KnowledgeCreateModal.tsx b/frontend/src/components/KnowledgeCreateModal.tsx index d0abb49..ee5bee6 100644 --- a/frontend/src/components/KnowledgeCreateModal.tsx +++ b/frontend/src/components/KnowledgeCreateModal.tsx @@ -1,6 +1,7 @@ import {useState} from 'react' import type {SelectProps} from 'antd' -import {DatePicker, Form, Input, message, Modal, Select, Tag, theme, Typography} from 'antd' +import {DatePicker, Form, Input, Modal, Select, Tag, theme, Typography} from 'antd' +import {message} from '../utils/appMessage' import {BookOutlined} from '@ant-design/icons' import type {Dayjs} from 'dayjs' import client from '../api/client' diff --git a/frontend/src/components/MarkdownEditor.tsx b/frontend/src/components/MarkdownEditor.tsx index 2bf4f76..16623c1 100644 --- a/frontend/src/components/MarkdownEditor.tsx +++ b/frontend/src/components/MarkdownEditor.tsx @@ -1,6 +1,6 @@ import {type ClipboardEvent, useState} from 'react' import axios from 'axios' -import {message} from 'antd' +import {message} from '../utils/appMessage' import type {PreviewType} from '@uiw/react-md-editor' import MDEditor from '@uiw/react-md-editor' import client from '../api/client' diff --git a/frontend/src/components/MessageComposer.tsx b/frontend/src/components/MessageComposer.tsx index 2d63fe2..110dd66 100644 --- a/frontend/src/components/MessageComposer.tsx +++ b/frontend/src/components/MessageComposer.tsx @@ -1,6 +1,7 @@ import {useMemo, useState} from 'react' import type {UploadProps} from 'antd' -import {Alert, Button, Mentions, message, Tooltip, Upload} from 'antd' +import {Alert, Button, Mentions, Tooltip, Upload} from 'antd' +import {message} from '../utils/appMessage' import {PaperClipOutlined, SendOutlined} from '@ant-design/icons' import {type Attachment, uploadAttachment} from '../api/attachments' import {fetchMentionUsers, type MentionUser} from '../api/comments' diff --git a/frontend/src/components/PersonalCenterModal.tsx b/frontend/src/components/PersonalCenterModal.tsx index 91c099a..87f33dd 100644 --- a/frontend/src/components/PersonalCenterModal.tsx +++ b/frontend/src/components/PersonalCenterModal.tsx @@ -1,5 +1,6 @@ import {useEffect, useState} from 'react' -import {Button, Col, DatePicker, Form, Input, message, Modal, Popconfirm, Row, Space, Switch, Table, Tabs, Tag, Typography} from 'antd' +import {Button, Col, DatePicker, Form, Input, Modal, Popconfirm, Row, Space, Switch, Table, Tabs, Tag, Typography} from 'antd' +import {message} from '../utils/appMessage' import {ApiOutlined, CopyOutlined, DeleteOutlined, KeyOutlined, LockOutlined, ReloadOutlined, SettingOutlined, UserOutlined} from '@ant-design/icons' import type {ColumnsType} from 'antd/es/table' import type {Dayjs} from 'dayjs' diff --git a/frontend/src/components/RecordDetailModal.tsx b/frontend/src/components/RecordDetailModal.tsx index 4f890e8..cc58408 100644 --- a/frontend/src/components/RecordDetailModal.tsx +++ b/frontend/src/components/RecordDetailModal.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react' -import {Button, Divider, Empty, message, Modal, Spin, theme, Tooltip} from 'antd' +import {Button, Divider, Empty, Modal, Spin, theme, Tooltip} from 'antd' +import {message} from '../utils/appMessage' import {CloseOutlined, FileTextOutlined, HistoryOutlined, MenuFoldOutlined, MenuUnfoldOutlined, MessageOutlined, ReloadOutlined, ShareAltOutlined} from '@ant-design/icons' import client from '../api/client' import type {FieldEditingController, OpenResourceOptions, ResourceConfig} from '../types/records' diff --git a/frontend/src/components/RelatedRecordsTable.tsx b/frontend/src/components/RelatedRecordsTable.tsx index 1e4cab2..21d1647 100644 --- a/frontend/src/components/RelatedRecordsTable.tsx +++ b/frontend/src/components/RelatedRecordsTable.tsx @@ -1,5 +1,5 @@ import {type ReactNode, useCallback, useEffect, useMemo, useState} from 'react' -import {message} from 'antd' +import {message} from '../utils/appMessage' import DataTable from './DataTable' import {fetchResourceMetadata} from '../api/metadata' import type {AdvancedFilterFieldConfig, MetadataResponse, OpenResourceOptions, ResourceColumn, ResourceFilterConfig} from '../types/records' diff --git a/frontend/src/components/TagList.tsx b/frontend/src/components/TagList.tsx index f4a9ea0..b81d0c7 100644 --- a/frontend/src/components/TagList.tsx +++ b/frontend/src/components/TagList.tsx @@ -1,3 +1,4 @@ +import {Fragment} from 'react' import {choiceTag} from '../utils/recordDisplay' interface TagListProps { @@ -10,7 +11,11 @@ export default function TagList({ items, color = 'blue' }: TagListProps) { return ( - {items.map((item) => choiceTag(String(item), color))} + {items.map((item, index) => ( + + {choiceTag(String(item), color)} + + ))} ) } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 29d2f03..ceacea5 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client' import {BrowserRouter} from 'react-router-dom' import {App as AntApp, ConfigProvider} from 'antd' import App from './App' +import AppMessageProvider from './components/AppMessageProvider' import themeConfig from './theme' import './index.css' @@ -11,6 +12,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render( + diff --git a/frontend/src/pages/CustomDefinitions.tsx b/frontend/src/pages/CustomDefinitions.tsx index 9482608..3954107 100644 --- a/frontend/src/pages/CustomDefinitions.tsx +++ b/frontend/src/pages/CustomDefinitions.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useMemo, useState} from 'react' -import {Alert, Button, Descriptions, Drawer, Empty, Flex, Input, InputNumber, List, message, Select, Space, Table, Tabs, Tag, Typography} from 'antd' +import {Alert, Button, Descriptions, Drawer, Empty, Flex, Input, InputNumber, List, Select, Space, Table, Tabs, Tag, Typography} from 'antd' +import {message} from '../utils/appMessage' import {ReloadOutlined} from '@ant-design/icons' import type {ColumnsType} from 'antd/es/table' import {Boxes, BrainCircuit, DatabaseZap} from 'lucide-react' diff --git a/frontend/src/pages/LDAPSettings.tsx b/frontend/src/pages/LDAPSettings.tsx index 79d6b72..bfa5c42 100644 --- a/frontend/src/pages/LDAPSettings.tsx +++ b/frontend/src/pages/LDAPSettings.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useState} from 'react' -import {Button, Card, Col, Form, Input, message, Row, Space, Switch} from 'antd' +import {Button, Card, Col, Form, Input, Row, Space, Switch} from 'antd' +import {message} from '../utils/appMessage' import client from '../api/client' interface LDAPConfig { diff --git a/frontend/src/pages/LLMProviderSettings.tsx b/frontend/src/pages/LLMProviderSettings.tsx index adfcaa3..4f5d56a 100644 --- a/frontend/src/pages/LLMProviderSettings.tsx +++ b/frontend/src/pages/LLMProviderSettings.tsx @@ -1,5 +1,6 @@ import {useMemo, useState} from 'react' -import {Button, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch} from 'antd' +import {Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch} from 'antd' +import {message} from '../utils/appMessage' import {CheckCircleOutlined, EditOutlined, PlusOutlined, StopOutlined, ThunderboltOutlined} from '@ant-design/icons' import client from '../api/client' import DataTable from '../components/DataTable' diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index f1677ff..3dade37 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,6 +1,7 @@ import {useState} from 'react' import {useNavigate, useSearchParams} from 'react-router-dom' -import {BorderBeam, Button, Card, Form, Input, message, Segmented} from 'antd' +import {BorderBeam, Button, Card, Form, Input, Segmented} from 'antd' +import {message} from '../utils/appMessage' import {LockOutlined, UserOutlined} from '@ant-design/icons' import type {AuthType} from '../api/auth' import {login} from '../api/auth' diff --git a/frontend/src/pages/RuntimeSettings.tsx b/frontend/src/pages/RuntimeSettings.tsx index 05b7d9b..6ae77ac 100644 --- a/frontend/src/pages/RuntimeSettings.tsx +++ b/frontend/src/pages/RuntimeSettings.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useState} from 'react' -import {Button, Card, Col, Divider, Form, InputNumber, message, Row, Select, Space, Tooltip, Typography} from 'antd' +import {Button, Card, Col, Divider, Form, InputNumber, Row, Select, Space, Tooltip, Typography} from 'antd' +import {message} from '../utils/appMessage' import {QuestionCircleOutlined} from '@ant-design/icons' import client from '../api/client' diff --git a/frontend/src/pages/SIEMSettings.tsx b/frontend/src/pages/SIEMSettings.tsx index 2bbe9f8..d7264e7 100644 --- a/frontend/src/pages/SIEMSettings.tsx +++ b/frontend/src/pages/SIEMSettings.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useState} from 'react' -import {Button, Card, Col, Form, Input, InputNumber, message, Row, Select, Space, Switch, Tabs} from 'antd' +import {Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch, Tabs} from 'antd' +import {message} from '../utils/appMessage' import {Layers, Search} from 'lucide-react' import client from '../api/client' import IconTabLabel from '../components/IconTabLabel' diff --git a/frontend/src/pages/ThreatIntelligenceSettings.tsx b/frontend/src/pages/ThreatIntelligenceSettings.tsx index 54ce977..523ef15 100644 --- a/frontend/src/pages/ThreatIntelligenceSettings.tsx +++ b/frontend/src/pages/ThreatIntelligenceSettings.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useState} from 'react' -import {Button, Card, Form, Input, InputNumber, message, Space, Switch, Tabs} from 'antd' +import {Button, Card, Form, Input, InputNumber, Space, Switch, Tabs} from 'antd' +import {message} from '../utils/appMessage' import {ChartNetwork, SatelliteDish} from 'lucide-react' import client from '../api/client' diff --git a/frontend/src/pages/UserCreateModal.tsx b/frontend/src/pages/UserCreateModal.tsx index 12f1585..c1d691d 100644 --- a/frontend/src/pages/UserCreateModal.tsx +++ b/frontend/src/pages/UserCreateModal.tsx @@ -1,4 +1,5 @@ -import {Col, Divider, Form, Input, message, Modal, Row, Select, Typography} from 'antd' +import {Col, Divider, Form, Input, Modal, Row, Select, Typography} from 'antd' +import {message} from '../utils/appMessage' import client from '../api/client' import type {AuthType, UserMutationResponse} from '../api/auth' diff --git a/frontend/src/pages/UserDetailModal.tsx b/frontend/src/pages/UserDetailModal.tsx index 920c18d..d2ef178 100644 --- a/frontend/src/pages/UserDetailModal.tsx +++ b/frontend/src/pages/UserDetailModal.tsx @@ -1,5 +1,6 @@ import {useEffect, useState} from 'react' -import {Button, Col, Divider, Form, Input, message, Modal, Row, Select, Space, theme, Typography} from 'antd' +import {Button, Col, Divider, Form, Input, Modal, Row, Select, Space, theme, Typography} from 'antd' +import {message} from '../utils/appMessage' import client from '../api/client' import type {AuthUser} from '../stores/auth' import AvatarUpload from '../components/AvatarUpload' @@ -27,13 +28,8 @@ export default function UserDetailModal({ userId, open, onClose, onSaved }: User const [saving, setSaving] = useState(false) useEffect(() => { - if (!open || userId === null) { - // eslint-disable-next-line react-hooks/set-state-in-effect - setUser(null) - form.resetFields() - return - } - setLoading(true) + if (!open || userId === null) return + client.get(`/auth/users/${userId}/`) .then(({ data }) => { setUser(data) @@ -89,6 +85,14 @@ export default function UserDetailModal({ userId, open, onClose, onSaved }: User footer: { background: token.colorBgContainer }, }} destroyOnHidden + afterOpenChange={(nextOpen) => { + if (nextOpen) { + if (userId !== null) setLoading(true) + return + } + setUser(null) + form.resetFields() + }} >
diff --git a/frontend/src/pages/UserManagement.tsx b/frontend/src/pages/UserManagement.tsx index 28e1fa9..8d69992 100644 --- a/frontend/src/pages/UserManagement.tsx +++ b/frontend/src/pages/UserManagement.tsx @@ -1,5 +1,6 @@ import {useEffect, useMemo, useState} from 'react' -import {Button, Input, message, Modal, Popconfirm, Space, Tooltip} from 'antd' +import {Button, Input, Modal, Popconfirm, Space, Tooltip} from 'antd' +import {message} from '../utils/appMessage' import {CheckCircleOutlined, DeleteOutlined, KeyOutlined, PlusOutlined, StopOutlined} from '@ant-design/icons' import client from '../api/client' import DataTable from '../components/DataTable' diff --git a/frontend/src/realtime.tsx b/frontend/src/realtime.tsx index 0e0b1ca..23aed2b 100644 --- a/frontend/src/realtime.tsx +++ b/frontend/src/realtime.tsx @@ -7,6 +7,20 @@ function websocketUrl(token: string) { return `${protocol}//${window.location.host}/ws/events/?token=${encodeURIComponent(token)}` } +function closeSocket(socket: WebSocket) { + if (socket.readyState === WebSocket.CONNECTING) { + socket.onopen = () => socket.close() + socket.onmessage = null + socket.onerror = null + socket.onclose = null + return + } + + if (socket.readyState === WebSocket.OPEN) { + socket.close() + } +} + export function RealtimeProvider({ children }: { children: React.ReactNode }) { const token = useAuthStore((state) => state.token) const [status, setStatus] = useState('disconnected') @@ -32,8 +46,9 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) { useEffect(() => { if (!token) { shouldReconnectRef.current = false - socketRef.current?.close() + const socket = socketRef.current socketRef.current = null + if (socket) closeSocket(socket) return } @@ -53,6 +68,10 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) { socketRef.current = socket socket.onopen = () => { + if (socketRef.current !== socket) { + socket.close() + return + } reconnectAttemptRef.current = 0 setStatus('connected') setReconnectToken((current) => current + 1) @@ -62,6 +81,7 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) { } socket.onmessage = (event) => { + if (socketRef.current !== socket) return let data: unknown try { data = JSON.parse(event.data) @@ -75,9 +95,8 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) { } socket.onclose = () => { - if (socketRef.current === socket) { - socketRef.current = null - } + if (socketRef.current !== socket) return + socketRef.current = null setStatus('disconnected') if (!shouldReconnectRef.current) return const attempt = reconnectAttemptRef.current + 1 @@ -87,17 +106,19 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) { } socket.onerror = () => { - socket.close() + if (socketRef.current !== socket) return + if (socket.readyState === WebSocket.OPEN) socket.close() } } - connect() + reconnectTimerRef.current = window.setTimeout(connect, 0) return () => { shouldReconnectRef.current = false clearReconnectTimer() - socketRef.current?.close() + const socket = socketRef.current socketRef.current = null + if (socket) closeSocket(socket) } }, [sendCommentSubscription, token]) diff --git a/frontend/src/utils/appMessage.ts b/frontend/src/utils/appMessage.ts new file mode 100644 index 0000000..5a0ac67 --- /dev/null +++ b/frontend/src/utils/appMessage.ts @@ -0,0 +1,28 @@ +import {App as AntApp} from 'antd' + +type MessageApi = ReturnType['message'] + +let activeMessage: MessageApi | null = null + +export function setAppMessage(message: MessageApi) { + activeMessage = message +} + +export function clearAppMessage(message: MessageApi) { + if (activeMessage === message) activeMessage = null +} + +function getMessage() { + if (!activeMessage) { + throw new Error('Ant Design message API is not ready') + } + return activeMessage +} + +export const message = new Proxy({} as MessageApi, { + get(_target, property: keyof MessageApi) { + const api = getMessage() + const value = api[property] + return typeof value === 'function' ? value.bind(api) : value + }, +})