From 88947c7ece3897f19caf18d14592a0e4dc928813 Mon Sep 17 00:00:00 2001 From: choizhang Date: Mon, 14 Apr 2025 10:13:54 +0800 Subject: [PATCH] refactor(graph): Refactoring the attribute line component to extract common logic into a separate file --- .../components/graph/EditablePropertyRow.tsx | 301 +----------------- .../src/components/graph/PropertiesView.tsx | 1 - .../graph/PropertyRowComponents.tsx | 53 +++ lightrag_webui/src/utils/graphOperations.ts | 175 ++++++++++ 4 files changed, 237 insertions(+), 293 deletions(-) create mode 100644 lightrag_webui/src/components/graph/PropertyRowComponents.tsx create mode 100644 lightrag_webui/src/utils/graphOperations.ts diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index bca482b8..a8e84fce 100644 --- a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -1,22 +1,18 @@ import { useState, useEffect } from 'react' -import { useRef } from 'react' import { useTranslation } from 'react-i18next' -import Text from '@/components/ui/Text' import { toast } from 'sonner' import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' -import { useGraphStore } from '@/stores/graph' -import { PencilIcon } from 'lucide-react' +import { updateGraphNode, updateGraphEdge } from '@/utils/graphOperations' +import { PropertyName, EditIcon, PropertyValue } from './PropertyRowComponents' import PropertyEditDialog from './PropertyEditDialog' /** * Interface for the EditablePropertyRow component props - * Defines all possible properties that can be passed to the component */ interface EditablePropertyRowProps { name: string // Property name to display and edit value: any // Initial value of the property onClick?: () => void // Optional click handler for the property value - tooltip?: string // Optional tooltip text entityId?: string // ID of the entity (for node type) entityType?: 'node' | 'edge' // Type of graph entity sourceId?: string // Source node ID (for edge type) @@ -26,19 +22,8 @@ interface EditablePropertyRowProps { } /** - * Interface for tracking edges that need updating when a node ID changes - */ -interface EdgeToUpdate { - originalDynamicId: string; - newEdgeId: string; - edgeIndex: number; -} - -/** - * EditablePropertyRow component that supports double-click to edit property values + * EditablePropertyRow component that supports editing property values * This component is used in the graph properties panel to display and edit entity properties - * - * @component */ const EditablePropertyRow = ({ name, @@ -51,253 +36,27 @@ const EditablePropertyRow = ({ onValueChange, isEditable = false }: EditablePropertyRowProps) => { - // Component state const { t } = useTranslation() const [isEditing, setIsEditing] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [currentValue, setCurrentValue] = useState(initialValue) - const inputRef = useRef(null) - - /** - * Update currentValue when initialValue changes from parent - */ useEffect(() => { setCurrentValue(initialValue) }, [initialValue]) - /** - * Initialize edit value and focus input when entering edit mode - */ - useEffect(() => { - if (isEditing) { - // Focus the input element when entering edit mode with a small delay - // to ensure the input is rendered before focusing - setTimeout(() => { - if (inputRef.current) { - inputRef.current.focus() - inputRef.current.select() - } - }, 50) - } - }, [isEditing]) - - /** - * Get translated property name from i18n - * Falls back to the original name if no translation is found - */ - const getPropertyNameTranslation = (propName: string) => { - const translationKey = `graphPanel.propertiesView.node.propertyNames.${propName}` - const translation = t(translationKey) - return translation === translationKey ? propName : translation - } - - /** - * Handle edit icon click to open dialog - */ const handleEditClick = () => { if (isEditable && !isEditing) { setIsEditing(true) } } - /** - * Handle dialog close without saving - */ const handleCancel = () => { setIsEditing(false) } - /** - * Update node in the graph visualization after API update - * Handles both property updates and entity ID changes - * - * @param nodeId - ID of the node to update - * @param propertyName - Name of the property being updated - * @param newValue - New value for the property - */ - const updateGraphNode = async (nodeId: string, propertyName: string, newValue: string) => { - // Get graph state from store - const sigmaInstance = useGraphStore.getState().sigmaInstance - const sigmaGraph = useGraphStore.getState().sigmaGraph - const rawGraph = useGraphStore.getState().rawGraph - - // Validate graph state - if (!sigmaInstance || !sigmaGraph || !rawGraph || !sigmaGraph.hasNode(String(nodeId))) { - return - } - - try { - const nodeAttributes = sigmaGraph.getNodeAttributes(String(nodeId)) - - // Special handling for entity_id changes (node renaming) - if (propertyName === 'entity_id') { - // Create new node with updated ID but same attributes - sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue }) - - const edgesToUpdate: EdgeToUpdate[] = []; - - // Process all edges connected to this node - sigmaGraph.forEachEdge(String(nodeId), (edge, attributes, source, target) => { - const otherNode = source === String(nodeId) ? target : source - const isOutgoing = source === String(nodeId) - - // Get original edge dynamic ID for later reference - const originalEdgeDynamicId = edge - const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId] - - // Create new edge with updated node reference - const newEdgeId = sigmaGraph.addEdge( - isOutgoing ? newValue : otherNode, - isOutgoing ? otherNode : newValue, - attributes - ) - - // Track edges that need updating in the raw graph - if (edgeIndexInRawGraph !== undefined) { - edgesToUpdate.push({ - originalDynamicId: originalEdgeDynamicId, - newEdgeId: newEdgeId, - edgeIndex: edgeIndexInRawGraph - }) - } - - // Remove the old edge - sigmaGraph.dropEdge(edge) - }) - - // Remove the old node after all edges are processed - sigmaGraph.dropNode(String(nodeId)) - - // Update node reference in raw graph data - const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] - if (nodeIndex !== undefined) { - rawGraph.nodes[nodeIndex].id = newValue - rawGraph.nodes[nodeIndex].properties.entity_id = newValue - delete rawGraph.nodeIdMap[String(nodeId)] - rawGraph.nodeIdMap[newValue] = nodeIndex - } - - // Update all edge references in raw graph data - edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => { - if (rawGraph.edges[edgeIndex]) { - // Update source/target references - if (rawGraph.edges[edgeIndex].source === String(nodeId)) { - rawGraph.edges[edgeIndex].source = newValue - } - if (rawGraph.edges[edgeIndex].target === String(nodeId)) { - rawGraph.edges[edgeIndex].target = newValue - } - - // Update dynamic ID mappings - rawGraph.edges[edgeIndex].dynamicId = newEdgeId - delete rawGraph.edgeDynamicIdMap[originalDynamicId] - rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex - } - }) - - // Update selected node in store - useGraphStore.getState().setSelectedNode(newValue) - } else { - // For other properties, just update the property in raw graph - const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] - if (nodeIndex !== undefined) { - rawGraph.nodes[nodeIndex].properties[propertyName] = newValue - } - } - } catch (error) { - console.error('Error updating node in graph:', error) - throw new Error('Failed to update node in graph') - } - } - - /** - * Update edge in the graph visualization after API update - * - * @param sourceId - ID of the source node - * @param targetId - ID of the target node - * @param propertyName - Name of the property being updated - * @param newValue - New value for the property - */ - const updateGraphEdge = async (sourceId: string, targetId: string, propertyName: string, newValue: string) => { - // Get graph state from store - const sigmaInstance = useGraphStore.getState().sigmaInstance - const sigmaGraph = useGraphStore.getState().sigmaGraph - const rawGraph = useGraphStore.getState().rawGraph - - // Validate graph state - if (!sigmaInstance || !sigmaGraph || !rawGraph) { - return - } - - try { - // Find the edge between source and target nodes - const allEdges = sigmaGraph.edges() - let keyToUse = null - - for (const edge of allEdges) { - const edgeSource = sigmaGraph.source(edge) - const edgeTarget = sigmaGraph.target(edge) - - // Match edge in either direction (undirected graph support) - if ((edgeSource === sourceId && edgeTarget === targetId) || - (edgeSource === targetId && edgeTarget === sourceId)) { - keyToUse = edge - break - } - } - - if (keyToUse !== null) { - // Special handling for keywords property (updates edge label) - if(propertyName === 'keywords') { - sigmaGraph.setEdgeAttribute(keyToUse, 'label', newValue); - } else { - sigmaGraph.setEdgeAttribute(keyToUse, propertyName, newValue); - } - - // Update edge in raw graph data using dynamic ID mapping - if (keyToUse && rawGraph.edgeDynamicIdMap[keyToUse] !== undefined) { - const edgeIndex = rawGraph.edgeDynamicIdMap[keyToUse]; - if (rawGraph.edges[edgeIndex]) { - rawGraph.edges[edgeIndex].properties[propertyName] = newValue; - } else { - console.warn(`Edge index ${edgeIndex} found but edge data missing in rawGraph for dynamicId ${entityId}`); - } - } else { - // Fallback: try to find edge by key in edge ID map - console.warn(`Could not find edge with dynamicId ${entityId} in rawGraph.edgeDynamicIdMap to update properties.`); - if (keyToUse !== null) { - const edgeIndexByKey = rawGraph.edgeIdMap[keyToUse]; - if (edgeIndexByKey !== undefined && rawGraph.edges[edgeIndexByKey]) { - rawGraph.edges[edgeIndexByKey].properties[propertyName] = newValue; - console.log(`Updated rawGraph edge using constructed key ${keyToUse}`); - } else { - console.warn(`Could not find edge in rawGraph using key ${keyToUse} either.`); - } - } else { - console.warn('Cannot update edge properties: edge key is null'); - } - } - } else { - console.warn(`Edge not found in sigmaGraph with key ${keyToUse}`); - } - } catch (error) { - // Log the specific edge key that caused the error - console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error); - throw new Error('Failed to update edge in graph') - } - } - - /** - * Save changes to the property value - * Updates both the API and the graph visualization - */ const handleSave = async (value: string) => { - // Prevent duplicate submissions - if (isSubmitting) return - - // Skip if value hasn't changed - if (value === String(currentValue)) { + if (isSubmitting || value === String(currentValue)) { setIsEditing(false) return } @@ -305,47 +64,31 @@ const EditablePropertyRow = ({ setIsSubmitting(true) try { - // Handle node property updates if (entityType === 'node' && entityId) { let updatedData = { [name]: value } - // Special handling for entity_id (name) changes if (name === 'entity_id') { - // Check if the new name already exists const exists = await checkEntityNameExists(value) if (exists) { toast.error(t('graphPanel.propertiesView.errors.duplicateName')) - setIsSubmitting(false) return } - // For entity_id, we update entity_name in the API updatedData = { 'entity_name': value } } - // Update entity in API await updateEntity(entityId, updatedData, true) - // Update graph visualization await updateGraphNode(entityId, name, value) toast.success(t('graphPanel.propertiesView.success.entityUpdated')) - } - // Handle edge property updates - else if (entityType === 'edge' && sourceId && targetId) { + } else if (entityType === 'edge' && sourceId && targetId) { const updatedData = { [name]: value } - // Update relation in API await updateRelation(sourceId, targetId, updatedData) - // Update graph visualization await updateGraphEdge(sourceId, targetId, name, value) toast.success(t('graphPanel.propertiesView.success.relationUpdated')) } - // Update local state setIsEditing(false) setCurrentValue(value) - - // Notify parent component if callback provided - if (onValueChange) { - onValueChange(value) - } + onValueChange?.(value) } catch (error) { console.error('Error updating property:', error) toast.error(t('graphPanel.propertiesView.errors.updateFailed')) @@ -354,37 +97,11 @@ const EditablePropertyRow = ({ } } - /** - * Render the property row with edit functionality - * Shows property name, edit icon, and the current value - */ return (
- {/* Property name with translation */} - - {getPropertyNameTranslation(name)} - - - {/* Edit icon without tooltip */} -
- -
: - - {/* Text display */} -
- -
- - {/* Edit dialog */} + + : + { + const { t } = useTranslation() + + const getPropertyNameTranslation = (propName: string) => { + const translationKey = `graphPanel.propertiesView.node.propertyNames.${propName}` + const translation = t(translationKey) + return translation === translationKey ? propName : translation + } + + return ( + + {getPropertyNameTranslation(name)} + + ) +} + +interface EditIconProps { + onClick: () => void +} + +export const EditIcon = ({ onClick }: EditIconProps) => ( +
+ +
+) + +interface PropertyValueProps { + value: any + onClick?: () => void +} + +export const PropertyValue = ({ value, onClick }: PropertyValueProps) => ( +
+ +
+) diff --git a/lightrag_webui/src/utils/graphOperations.ts b/lightrag_webui/src/utils/graphOperations.ts new file mode 100644 index 00000000..c99ac17d --- /dev/null +++ b/lightrag_webui/src/utils/graphOperations.ts @@ -0,0 +1,175 @@ +import { useGraphStore } from '@/stores/graph' + +/** + * Interface for tracking edges that need updating when a node ID changes + */ +interface EdgeToUpdate { + originalDynamicId: string + newEdgeId: string + edgeIndex: number +} + +/** + * Update node in the graph visualization + * Handles both property updates and entity ID changes + * + * @param nodeId - ID of the node to update + * @param propertyName - Name of the property being updated + * @param newValue - New value for the property + */ +export const updateGraphNode = async (nodeId: string, propertyName: string, newValue: string) => { + // Get graph state from store + const sigmaGraph = useGraphStore.getState().sigmaGraph + const rawGraph = useGraphStore.getState().rawGraph + + // Validate graph state + if (!sigmaGraph || !rawGraph || !sigmaGraph.hasNode(String(nodeId))) { + return + } + + try { + const nodeAttributes = sigmaGraph.getNodeAttributes(String(nodeId)) + + // Special handling for entity_id changes (node renaming) + if (propertyName === 'entity_id') { + // Create new node with updated ID but same attributes + sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue }) + + const edgesToUpdate: EdgeToUpdate[] = [] + + // Process all edges connected to this node + sigmaGraph.forEachEdge(String(nodeId), (edge, attributes, source, target) => { + const otherNode = source === String(nodeId) ? target : source + const isOutgoing = source === String(nodeId) + + // Get original edge dynamic ID for later reference + const originalEdgeDynamicId = edge + const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId] + + // Create new edge with updated node reference + const newEdgeId = sigmaGraph.addEdge( + isOutgoing ? newValue : otherNode, + isOutgoing ? otherNode : newValue, + attributes + ) + + // Track edges that need updating in the raw graph + if (edgeIndexInRawGraph !== undefined) { + edgesToUpdate.push({ + originalDynamicId: originalEdgeDynamicId, + newEdgeId: newEdgeId, + edgeIndex: edgeIndexInRawGraph + }) + } + + // Remove the old edge + sigmaGraph.dropEdge(edge) + }) + + // Remove the old node after all edges are processed + sigmaGraph.dropNode(String(nodeId)) + + // Update node reference in raw graph data + const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] + if (nodeIndex !== undefined) { + rawGraph.nodes[nodeIndex].id = newValue + rawGraph.nodes[nodeIndex].properties.entity_id = newValue + delete rawGraph.nodeIdMap[String(nodeId)] + rawGraph.nodeIdMap[newValue] = nodeIndex + } + + // Update all edge references in raw graph data + edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => { + if (rawGraph.edges[edgeIndex]) { + // Update source/target references + if (rawGraph.edges[edgeIndex].source === String(nodeId)) { + rawGraph.edges[edgeIndex].source = newValue + } + if (rawGraph.edges[edgeIndex].target === String(nodeId)) { + rawGraph.edges[edgeIndex].target = newValue + } + + // Update dynamic ID mappings + rawGraph.edges[edgeIndex].dynamicId = newEdgeId + delete rawGraph.edgeDynamicIdMap[originalDynamicId] + rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex + } + }) + + // Update selected node in store + useGraphStore.getState().setSelectedNode(newValue) + } else { + // For other properties, just update the property in raw graph + const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] + if (nodeIndex !== undefined) { + rawGraph.nodes[nodeIndex].properties[propertyName] = newValue + } + } + } catch (error) { + console.error('Error updating node in graph:', error) + throw new Error('Failed to update node in graph') + } +} + +/** + * Update edge in the graph visualization + * + * @param sourceId - ID of the source node + * @param targetId - ID of the target node + * @param propertyName - Name of the property being updated + * @param newValue - New value for the property + */ +export const updateGraphEdge = async (sourceId: string, targetId: string, propertyName: string, newValue: string) => { + // Get graph state from store + const sigmaGraph = useGraphStore.getState().sigmaGraph + const rawGraph = useGraphStore.getState().rawGraph + + // Validate graph state + if (!sigmaGraph || !rawGraph) { + return + } + + try { + // Find the edge between source and target nodes + const allEdges = sigmaGraph.edges() + let keyToUse = null + + for (const edge of allEdges) { + const edgeSource = sigmaGraph.source(edge) + const edgeTarget = sigmaGraph.target(edge) + + // Match edge in either direction (undirected graph support) + if ((edgeSource === sourceId && edgeTarget === targetId) || + (edgeSource === targetId && edgeTarget === sourceId)) { + keyToUse = edge + break + } + } + + if (keyToUse !== null) { + // Special handling for keywords property (updates edge label) + if(propertyName === 'keywords') { + sigmaGraph.setEdgeAttribute(keyToUse, 'label', newValue) + } else { + sigmaGraph.setEdgeAttribute(keyToUse, propertyName, newValue) + } + + // Update edge in raw graph data using dynamic ID mapping + if (keyToUse && rawGraph.edgeDynamicIdMap[keyToUse] !== undefined) { + const edgeIndex = rawGraph.edgeDynamicIdMap[keyToUse] + if (rawGraph.edges[edgeIndex]) { + rawGraph.edges[edgeIndex].properties[propertyName] = newValue + } + } else if (keyToUse !== null) { + // Fallback: try to find edge by key in edge ID map + const edgeIndexByKey = rawGraph.edgeIdMap[keyToUse] + if (edgeIndexByKey !== undefined && rawGraph.edges[edgeIndexByKey]) { + rawGraph.edges[edgeIndexByKey].properties[propertyName] = newValue + } + } + } + } catch (error) { + console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error) + throw new Error('Failed to update edge in graph') + } +}