diff --git a/lightrag/api/routers/graph_routes.py b/lightrag/api/routers/graph_routes.py index e77e959a..107a7952 100644 --- a/lightrag/api/routers/graph_routes.py +++ b/lightrag/api/routers/graph_routes.py @@ -95,7 +95,6 @@ def create_graph_routes(rag, api_key: Optional[str] = None): Dict: Updated entity information """ try: - print(request.entity_name, request.updated_data, request.allow_rename) result = await rag.aedit_entity( entity_name=request.entity_name, updated_data=request.updated_data, diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index bfd88802..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, useRef } from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import Text from '@/components/ui/Text' -import Input from '@/components/ui/Input' import { toast } from 'sonner' import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' -import { useGraphStore } from '@/stores/graph' -import { PencilIcon } from 'lucide-react' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip' +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,25 +22,13 @@ 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, value: initialValue, onClick, - tooltip, entityId, entityType, sourceId, @@ -52,260 +36,27 @@ const EditablePropertyRow = ({ onValueChange, isEditable = false }: EditablePropertyRowProps) => { - // Component state const { t } = useTranslation() const [isEditing, setIsEditing] = useState(false) - const [editValue, setEditValue] = useState('') 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) { - setEditValue(String(currentValue)) - // 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, currentValue]) - - /** - * 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 double-click event to enter edit mode - */ - const handleDoubleClick = () => { + const handleEditClick = () => { if (isEditable && !isEditing) { setIsEditing(true) } } - /** - * Handle keyboard events in the input field - * - Enter: Save changes - * - Escape: Cancel editing - */ - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleSave() - } else if (e.key === 'Escape') { - setIsEditing(false) - } + 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 () => { - // Prevent duplicate submissions - if (isSubmitting) return - - // Skip if value hasn't changed - if (editValue === String(currentValue)) { + const handleSave = async (value: string) => { + if (isSubmitting || value === String(currentValue)) { setIsEditing(false) return } @@ -313,47 +64,31 @@ const EditablePropertyRow = ({ setIsSubmitting(true) try { - // Handle node property updates if (entityType === 'node' && entityId) { - let updatedData = { [name]: editValue } + 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(editValue) + 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': editValue } + updatedData = { 'entity_name': value } } - // Update entity in API await updateEntity(entityId, updatedData, true) - // Update graph visualization - await updateGraphNode(entityId, name, editValue) + await updateGraphNode(entityId, name, value) toast.success(t('graphPanel.propertiesView.success.entityUpdated')) - } - // Handle edge property updates - else if (entityType === 'edge' && sourceId && targetId) { - const updatedData = { [name]: editValue } - // Update relation in API + } else if (entityType === 'edge' && sourceId && targetId) { + const updatedData = { [name]: value } await updateRelation(sourceId, targetId, updatedData) - // Update graph visualization - await updateGraphEdge(sourceId, targetId, name, editValue) + await updateGraphEdge(sourceId, targetId, name, value) toast.success(t('graphPanel.propertiesView.success.relationUpdated')) } - // Update local state setIsEditing(false) - setCurrentValue(editValue) - - // Notify parent component if callback provided - if (onValueChange) { - onValueChange(editValue) - } + setCurrentValue(value) + onValueChange?.(value) } catch (error) { console.error('Error updating property:', error) toast.error(t('graphPanel.propertiesView.errors.updateFailed')) @@ -362,60 +97,19 @@ const EditablePropertyRow = ({ } } - /** - * Render the property row with edit functionality - * Shows property name, edit icon, and either the editable input or the current value - */ return ( -
- {/* Property name with translation */} - - {getPropertyNameTranslation(name)} - - - {/* Edit icon with tooltip */} - - - -
- setIsEditing(true)} - /> -
-
- - {t('graphPanel.propertiesView.doubleClickToEdit')} - -
-
: - - {/* Conditional rendering based on edit state */} - {isEditing ? ( - // Input field for editing - setEditValue(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={handleSave} - className="h-6 text-xs" - disabled={isSubmitting} - /> - ) : ( - // Text display when not editing -
- -
- )} +
+ + : + +
) } diff --git a/lightrag_webui/src/components/graph/PropertiesView.tsx b/lightrag_webui/src/components/graph/PropertiesView.tsx index 257b69ec..804a7f09 100644 --- a/lightrag_webui/src/components/graph/PropertiesView.tsx +++ b/lightrag_webui/src/components/graph/PropertiesView.tsx @@ -202,7 +202,6 @@ const PropertyRow = ({ name={name} value={value} onClick={onClick} - tooltip={tooltip} entityId={entityId} entityType={entityType} sourceId={sourceId} diff --git a/lightrag_webui/src/components/graph/PropertyEditDialog.tsx b/lightrag_webui/src/components/graph/PropertyEditDialog.tsx new file mode 100644 index 00000000..612ffaf9 --- /dev/null +++ b/lightrag_webui/src/components/graph/PropertyEditDialog.tsx @@ -0,0 +1,105 @@ +import { useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter +} from '@/components/ui/Dialog' +import Button from '@/components/ui/Button' +import Input from '@/components/ui/Input' + +interface PropertyEditDialogProps { + isOpen: boolean + onClose: () => void + onSave: (value: string) => void + propertyName: string + initialValue: string + isSubmitting?: boolean +} + +/** + * Dialog component for editing property values + * Provides a modal with a title, multi-line text input, and save/cancel buttons + */ +const PropertyEditDialog = ({ + isOpen, + onClose, + onSave, + propertyName, + initialValue, + isSubmitting = false +}: PropertyEditDialogProps) => { + const { t } = useTranslation() + const [value, setValue] = useState('') + + // Initialize value when dialog opens + useEffect(() => { + if (isOpen) { + setValue(initialValue) + } + }, [isOpen, initialValue]) + + // Get translated property name + const getPropertyNameTranslation = (name: string) => { + const translationKey = `graphPanel.propertiesView.node.propertyNames.${name}` + const translation = t(translationKey) + return translation === translationKey ? name : translation + } + + const handleSave = () => { + if (value.trim() !== '') { + onSave(value) + onClose() + } +} + + return ( + !open && onClose()}> + + + + {t('graphPanel.propertiesView.editProperty', { + property: getPropertyNameTranslation(propertyName) + })} + +

+ {t('graphPanel.propertiesView.editPropertyDescription')} +

+
+ + {/* Multi-line text input using textarea */} +
+