mirror of
https://github.com/langgenius/dify.git
synced 2026-04-28 20:17:29 +08:00
stabilize MCP service card collaboration subscription
This commit is contained in:
parent
f0cd2e8465
commit
5e0e0982bc
@ -26,6 +26,23 @@ const mockHandleGenCode = vi.fn()
|
|||||||
const mockOpenConfirmDelete = vi.fn()
|
const mockOpenConfirmDelete = vi.fn()
|
||||||
const mockCloseConfirmDelete = vi.fn()
|
const mockCloseConfirmDelete = vi.fn()
|
||||||
const mockOpenServerModal = vi.fn()
|
const mockOpenServerModal = vi.fn()
|
||||||
|
const mockOnMcpServerUpdate = vi.hoisted(() => vi.fn())
|
||||||
|
const mockUnsubscribeMcpServerUpdate = vi.hoisted(() => vi.fn())
|
||||||
|
const invalidateMCPServerDetailFns = vi.hoisted(() => [] as Array<ReturnType<typeof vi.fn>>)
|
||||||
|
|
||||||
|
vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () => ({
|
||||||
|
collaborationManager: {
|
||||||
|
onMcpServerUpdate: mockOnMcpServerUpdate,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/service/use-tools', () => ({
|
||||||
|
useInvalidateMCPServerDetail: () => {
|
||||||
|
const invalidateFn = vi.fn()
|
||||||
|
invalidateMCPServerDetailFns.push(invalidateFn)
|
||||||
|
return invalidateFn
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
type MockHookState = {
|
type MockHookState = {
|
||||||
genLoading: boolean
|
genLoading: boolean
|
||||||
@ -106,12 +123,15 @@ describe('MCPServiceCard', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockHookState = createDefaultHookState()
|
mockHookState = createDefaultHookState()
|
||||||
|
invalidateMCPServerDetailFns.length = 0
|
||||||
mockHandleStatusChange.mockClear().mockResolvedValue({ activated: true })
|
mockHandleStatusChange.mockClear().mockResolvedValue({ activated: true })
|
||||||
mockHandleServerModalHide.mockClear().mockReturnValue({ shouldDeactivate: false })
|
mockHandleServerModalHide.mockClear().mockReturnValue({ shouldDeactivate: false })
|
||||||
mockHandleGenCode.mockClear()
|
mockHandleGenCode.mockClear()
|
||||||
mockOpenConfirmDelete.mockClear()
|
mockOpenConfirmDelete.mockClear()
|
||||||
mockCloseConfirmDelete.mockClear()
|
mockCloseConfirmDelete.mockClear()
|
||||||
mockOpenServerModal.mockClear()
|
mockOpenServerModal.mockClear()
|
||||||
|
mockUnsubscribeMcpServerUpdate.mockClear()
|
||||||
|
mockOnMcpServerUpdate.mockReset().mockReturnValue(mockUnsubscribeMcpServerUpdate)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
@ -431,4 +451,26 @@ describe('MCPServiceCard', () => {
|
|||||||
expect(screen.getByRole('switch')).toBeInTheDocument()
|
expect(screen.getByRole('switch')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Collaboration Sync', () => {
|
||||||
|
it('should keep a stable MCP update subscription across rerenders and invalidate with the latest callback', async () => {
|
||||||
|
let mcpUpdateHandler: ((payload: unknown) => void) | undefined
|
||||||
|
mockOnMcpServerUpdate.mockImplementation((handler: (payload: unknown) => void) => {
|
||||||
|
mcpUpdateHandler = handler
|
||||||
|
return mockUnsubscribeMcpServerUpdate
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rerender } = render(<MCPServiceCard appInfo={createMockAppInfo()} />, { wrapper: createWrapper() })
|
||||||
|
|
||||||
|
rerender(<MCPServiceCard appInfo={createMockAppInfo()} />, { wrapper: createWrapper() })
|
||||||
|
|
||||||
|
expect(mockOnMcpServerUpdate).toHaveBeenCalledTimes(1)
|
||||||
|
expect(invalidateMCPServerDetailFns).toHaveLength(2)
|
||||||
|
|
||||||
|
mcpUpdateHandler?.({ type: 'mcp_server_update' })
|
||||||
|
|
||||||
|
expect(invalidateMCPServerDetailFns[0]).not.toHaveBeenCalled()
|
||||||
|
expect(invalidateMCPServerDetailFns[1]).toHaveBeenCalledWith('app-123')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { CollaborationUpdate } from '@/app/components/workflow/collaboratio
|
|||||||
import type { AppDetailResponse } from '@/models/app'
|
import type { AppDetailResponse } from '@/models/app'
|
||||||
import type { AppSSO } from '@/types/app'
|
import type { AppSSO } from '@/types/app'
|
||||||
import { RiEditLine, RiLoopLeftLine } from '@remixicon/react'
|
import { RiEditLine, RiLoopLeftLine } from '@remixicon/react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Confirm from '@/app/components/base/confirm'
|
import Confirm from '@/app/components/base/confirm'
|
||||||
@ -16,6 +16,7 @@ import Switch from '@/app/components/base/switch'
|
|||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import Indicator from '@/app/components/header/indicator'
|
import Indicator from '@/app/components/header/indicator'
|
||||||
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
|
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
|
||||||
|
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
import { useInvalidateMCPServerDetail } from '@/service/use-tools'
|
import { useInvalidateMCPServerDetail } from '@/service/use-tools'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
@ -166,6 +167,11 @@ const MCPServiceCard: FC<IAppCardProps> = ({
|
|||||||
const docLink = useDocLink()
|
const docLink = useDocLink()
|
||||||
const appId = appInfo.id
|
const appId = appInfo.id
|
||||||
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
|
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
|
||||||
|
const invalidateMCPServerDetailRef = useRef(invalidateMCPServerDetail)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
invalidateMCPServerDetailRef.current = invalidateMCPServerDetail
|
||||||
|
}, [invalidateMCPServerDetail])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
genLoading,
|
genLoading,
|
||||||
@ -258,34 +264,17 @@ const MCPServiceCard: FC<IAppCardProps> = ({
|
|||||||
if (!appId)
|
if (!appId)
|
||||||
return
|
return
|
||||||
|
|
||||||
let unsubscribe: (() => void) | undefined
|
const unsubscribe = collaborationManager.onMcpServerUpdate((_update: CollaborationUpdate) => {
|
||||||
let cancelled = false
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
try {
|
try {
|
||||||
const { collaborationManager } = await import('@/app/components/workflow/collaboration/core/collaboration-manager')
|
invalidateMCPServerDetailRef.current(appId)
|
||||||
if (cancelled)
|
|
||||||
return
|
|
||||||
|
|
||||||
unsubscribe = collaborationManager.onMcpServerUpdate((_update: CollaborationUpdate) => {
|
|
||||||
try {
|
|
||||||
invalidateMCPServerDetail(appId)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('MCP server update failed:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('MCP collaboration subscription failed:', error)
|
console.error('MCP server update failed:', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return unsubscribe
|
||||||
cancelled = true
|
}, [appId])
|
||||||
unsubscribe?.()
|
|
||||||
}
|
|
||||||
}, [appId, invalidateMCPServerDetail])
|
|
||||||
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return null
|
return null
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user