feat(app): refetch on new nodes

This commit is contained in:
dextmorgn
2025-11-27 15:39:34 +01:00
parent df127738f6
commit 2cac709bbc
5 changed files with 93 additions and 14 deletions

View File

@@ -19,6 +19,7 @@ import GraphMain from './graph-main'
import Settings, { KeyboardShortcuts } from './settings'
import { type GraphNode, type GraphEdge } from '@/types'
import { MergeDialog } from './merge-nodes'
import { useGraphRefresh } from '@/hooks/use-graph-refresh'
const RelationshipsTable = lazy(() => import('@/components/table/relationships-view'))
// Separate component for the drag overlay
@@ -50,11 +51,14 @@ const GraphPanel = ({ graphData, isLoading }: GraphPanelProps) => {
const filters = useGraphStore((s) => s.filters)
const { actionItems, isLoading: isLoadingActionItems } = useActionItems()
const { sketch } = useLoaderData({
const { params, sketch } = useLoaderData({
from: '/_auth/dashboard/investigations/$investigationId/$type/$id'
})
const [isDraggingOver, setIsDraggingOver] = useState(false)
// Dedicated hook for graph refresh on transform completion
useGraphRefresh(params.id)
useEffect(() => {
if (graphData?.nds && graphData?.rls) {
updateGraphData(graphData.nds, graphData.rls)

View File

@@ -1,8 +1,6 @@
import { useEffect, useState, useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { logService } from '@/api/log-service'
import { EventLevel } from '@/types'
import { useGraphControls } from '@/stores/graph-controls-store'
import { queryKeys } from '@/api/query-keys'
const API_URL = import.meta.env.VITE_API_URL
@@ -10,8 +8,6 @@ const API_URL = import.meta.env.VITE_API_URL
export function useEvents(sketch_id: string | undefined) {
const [liveLogs, setLiveLogs] = useState<Event[]>([])
const refetchGraph = useGraphControls((s) => s.refetchGraph)
const setShouldRegenerateLayoutOnNextRefetch = useGraphControls((s) => s.setShouldRegenerateLayoutOnNextRefetch)
const { data: previousLogs = [], refetch } = useQuery({
queryKey: queryKeys.logs.bySketch(sketch_id as string),
@@ -40,11 +36,6 @@ export function useEvents(sketch_id: string | undefined) {
try {
const raw = JSON.parse(e.data) as any
const event = JSON.parse(raw.data) as Event
if (event.type === EventLevel.COMPLETED) {
// Set flag to regenerate layout after refetch completes
setShouldRegenerateLayoutOnNextRefetch(true)
refetchGraph()
}
setLiveLogs((prev) => [...prev.slice(-99), event])
} catch (error) {
console.error('[useSketchEvents] Failed to parse SSE event:', error)
@@ -59,7 +50,7 @@ export function useEvents(sketch_id: string | undefined) {
return () => {
eventSource.close()
}
}, [sketch_id, refetchGraph])
}, [sketch_id])
const logs = useMemo(
() => [...previousLogs, ...liveLogs].slice(-100),

View File

@@ -0,0 +1,72 @@
import { useEffect } from 'react'
import { useGraphControls } from '@/stores/graph-controls-store'
import { EventLevel } from '@/types'
const API_URL = import.meta.env.VITE_API_URL
/**
* Dedicated hook for graph refresh on transform completion.
*
* This hook listens ONLY to COMPLETED events via SSE and triggers
* a graph refetch followed by layout regeneration. It's completely
* separate from the logging system (use-events.ts).
*
* Architecture:
* - Transform completes → Logger.completed() → SSE event
* - This hook receives COMPLETED event
* - Calls refetchGraph() with callback
* - Callback triggers regenerateLayout() with fresh data
*/
export function useGraphRefresh(sketch_id: string | undefined) {
const refetchGraph = useGraphControls((s) => s.refetchGraph)
const regenerateLayout = useGraphControls((s) => s.regenerateLayout)
const currentLayoutType = useGraphControls((s) => s.currentLayoutType)
useEffect(() => {
if (!sketch_id) return
console.log('[useGraphRefresh] Connecting to SSE for sketch:', sketch_id)
const eventSource = new EventSource(
`${API_URL}/api/events/sketch/${sketch_id}/stream`
)
eventSource.onopen = () => {
console.log('[useGraphRefresh] SSE connection opened')
}
eventSource.onmessage = (e) => {
try {
const raw = JSON.parse(e.data) as any
const event = JSON.parse(raw.data) as any
console.log('[useGraphRefresh] Received event:', event.type, event)
// Only handle COMPLETED events
if (event.type === EventLevel.COMPLETED) {
console.log('[useGraphRefresh] COMPLETED event detected, triggering refetch')
console.log('[useGraphRefresh] refetchGraph function:', refetchGraph)
console.log('[useGraphRefresh] currentLayoutType:', currentLayoutType)
// Refetch graph data, then regenerate layout with fresh data
refetchGraph(() => {
console.log('[useGraphRefresh] Refetch callback executing, regenerating layout')
if (currentLayoutType) {
regenerateLayout(currentLayoutType)
}
})
}
} catch (error) {
console.error('[useGraphRefresh] Failed to parse SSE event:', error)
}
}
eventSource.onerror = (error) => {
console.error('[useGraphRefresh] EventSource error:', error)
eventSource.close()
}
return () => {
console.log('[useGraphRefresh] Disconnecting SSE')
eventSource.close()
}
}, [sketch_id, refetchGraph, regenerateLayout, currentLayoutType])
}

View File

@@ -45,7 +45,19 @@ const GraphPageContent = () => {
}, [id, reset])
useEffect(() => {
setActions({ refetchGraph: refetch })
const refetchWithCallback = async (onSuccess?: () => void) => {
console.log('[Route] refetchWithCallback called with callback:', !!onSuccess)
await refetch()
console.log('[Route] refetch completed')
// Execute callback after refetch completes and data is updated
if (onSuccess) {
console.log('[Route] executing callback after refetch')
// Small delay to ensure React Query has updated the data in the store
setTimeout(onSuccess, 0)
}
}
console.log('[Route] Setting refetchGraph in store')
setActions({ refetchGraph: refetchWithCallback })
}, [refetch, setActions, id])
if (type === 'graph') {

View File

@@ -16,7 +16,7 @@ type GraphControlsStore = {
zoomOut: () => void
onLayout: (layout: any) => void
setActions: (actions: Partial<GraphControlsStore>) => void
refetchGraph: () => void
refetchGraph: (onSuccess?: () => void) => void
regenerateLayout: (layoutType: LayoutType) => void
setCurrentLayoutType: (layoutType: LayoutType) => void
setShouldRegenerateLayoutOnNextRefetch: (should: boolean) => void
@@ -38,7 +38,7 @@ export const useGraphControls = create<GraphControlsStore>()(
zoomOut: () => { },
onLayout: () => { },
setActions: (actions) => set(actions),
refetchGraph: () => { },
refetchGraph: (onSuccess) => { },
regenerateLayout: () => { },
setCurrentLayoutType: (layoutType) => set({ currentLayoutType: layoutType }),
setShouldRegenerateLayoutOnNextRefetch: (should) => set({ shouldRegenerateLayoutOnNextRefetch: should }),