yangdx commited on
Commit
f4c3104
·
1 Parent(s): 6d16de8

Fix Neo4j node and edge edit problem

Browse files
lightrag_webui/src/components/graph/EditablePropertyRow.tsx CHANGED
@@ -13,7 +13,10 @@ interface EditablePropertyRowProps {
13
  name: string // Property name to display and edit
14
  value: any // Initial value of the property
15
  onClick?: () => void // Optional click handler for the property value
 
16
  entityId?: string // ID of the entity (for node type)
 
 
17
  entityType?: 'node' | 'edge' // Type of graph entity
18
  sourceId?: string // Source node ID (for edge type)
19
  targetId?: string // Target node ID (for edge type)
@@ -30,7 +33,10 @@ const EditablePropertyRow = ({
30
  name,
31
  value: initialValue,
32
  onClick,
 
 
33
  entityId,
 
34
  entityType,
35
  sourceId,
36
  targetId,
@@ -66,7 +72,7 @@ const EditablePropertyRow = ({
66
  setIsSubmitting(true)
67
 
68
  try {
69
- if (entityType === 'node' && entityId) {
70
  let updatedData = { [name]: value }
71
 
72
  if (name === 'entity_id') {
@@ -79,12 +85,12 @@ const EditablePropertyRow = ({
79
  }
80
 
81
  await updateEntity(entityId, updatedData, true)
82
- await updateGraphNode(entityId, name, value)
83
  toast.success(t('graphPanel.propertiesView.success.entityUpdated'))
84
- } else if (entityType === 'edge' && sourceId && targetId) {
85
  const updatedData = { [name]: value }
86
  await updateRelation(sourceId, targetId, updatedData)
87
- await updateGraphEdge(sourceId, targetId, name, value)
88
  toast.success(t('graphPanel.propertiesView.success.relationUpdated'))
89
  }
90
 
 
13
  name: string // Property name to display and edit
14
  value: any // Initial value of the property
15
  onClick?: () => void // Optional click handler for the property value
16
+ nodeId?: string // ID of the node (for node type)
17
  entityId?: string // ID of the entity (for node type)
18
+ edgeId?: string // ID of the edge (for edge type)
19
+ dynamicId?: string
20
  entityType?: 'node' | 'edge' // Type of graph entity
21
  sourceId?: string // Source node ID (for edge type)
22
  targetId?: string // Target node ID (for edge type)
 
33
  name,
34
  value: initialValue,
35
  onClick,
36
+ nodeId,
37
+ edgeId,
38
  entityId,
39
+ dynamicId,
40
  entityType,
41
  sourceId,
42
  targetId,
 
72
  setIsSubmitting(true)
73
 
74
  try {
75
+ if (entityType === 'node' && entityId && nodeId) {
76
  let updatedData = { [name]: value }
77
 
78
  if (name === 'entity_id') {
 
85
  }
86
 
87
  await updateEntity(entityId, updatedData, true)
88
+ await updateGraphNode(nodeId, entityId, name, value)
89
  toast.success(t('graphPanel.propertiesView.success.entityUpdated'))
90
+ } else if (entityType === 'edge' && sourceId && targetId && edgeId && dynamicId) {
91
  const updatedData = { [name]: value }
92
  await updateRelation(sourceId, targetId, updatedData)
93
+ await updateGraphEdge(edgeId, dynamicId, sourceId, targetId, name, value)
94
  toast.success(t('graphPanel.propertiesView.success.relationUpdated'))
95
  }
96
 
lightrag_webui/src/components/graph/PropertiesView.tsx CHANGED
@@ -93,6 +93,7 @@ const refineNodeProperties = (node: RawNodeType): NodeType => {
93
  if (state.sigmaGraph && state.rawGraph) {
94
  try {
95
  if (!state.sigmaGraph.hasNode(node.id)) {
 
96
  return {
97
  ...node,
98
  relationships: []
@@ -139,7 +140,8 @@ const refineEdgeProperties = (edge: RawEdgeType): EdgeType => {
139
 
140
  if (state.sigmaGraph && state.rawGraph) {
141
  try {
142
- if (!state.sigmaGraph.hasEdge(edge.id)) {
 
143
  return {
144
  ...edge,
145
  sourceNode: undefined,
@@ -171,6 +173,9 @@ const PropertyRow = ({
171
  value,
172
  onClick,
173
  tooltip,
 
 
 
174
  entityId,
175
  entityType,
176
  sourceId,
@@ -181,7 +186,10 @@ const PropertyRow = ({
181
  value: any
182
  onClick?: () => void
183
  tooltip?: string
 
184
  entityId?: string
 
 
185
  entityType?: 'node' | 'edge'
186
  sourceId?: string
187
  targetId?: string
@@ -202,7 +210,10 @@ const PropertyRow = ({
202
  name={name}
203
  value={value}
204
  onClick={onClick}
 
205
  entityId={entityId}
 
 
206
  entityType={entityType}
207
  sourceId={sourceId}
208
  targetId={targetId}
@@ -265,7 +276,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
265
  </div>
266
  </div>
267
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
268
- <PropertyRow name={t('graphPanel.propertiesView.node.id')} value={node.id} />
269
  <PropertyRow
270
  name={t('graphPanel.propertiesView.node.labels')}
271
  value={node.labels.join(', ')}
@@ -285,7 +296,8 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
285
  key={name}
286
  name={name}
287
  value={node.properties[name]}
288
- entityId={node.properties['entity_id'] || node.id}
 
289
  entityType="node"
290
  isEditable={name === 'description' || name === 'entity_id'}
291
  />
@@ -350,10 +362,11 @@ const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
350
  key={name}
351
  name={name}
352
  value={edge.properties[name]}
353
- entityId={edge.id}
 
354
  entityType="edge"
355
- sourceId={edge.source}
356
- targetId={edge.target}
357
  isEditable={name === 'description' || name === 'keywords'}
358
  />
359
  )
 
93
  if (state.sigmaGraph && state.rawGraph) {
94
  try {
95
  if (!state.sigmaGraph.hasNode(node.id)) {
96
+ console.warn('Node not found in sigmaGraph:', node.id)
97
  return {
98
  ...node,
99
  relationships: []
 
140
 
141
  if (state.sigmaGraph && state.rawGraph) {
142
  try {
143
+ if (!state.sigmaGraph.hasEdge(edge.dynamicId)) {
144
+ console.warn('Edge not found in sigmaGraph:', edge.id, 'dynamicId:', edge.dynamicId)
145
  return {
146
  ...edge,
147
  sourceNode: undefined,
 
173
  value,
174
  onClick,
175
  tooltip,
176
+ nodeId,
177
+ edgeId,
178
+ dynamicId,
179
  entityId,
180
  entityType,
181
  sourceId,
 
186
  value: any
187
  onClick?: () => void
188
  tooltip?: string
189
+ nodeId?: string
190
  entityId?: string
191
+ edgeId?: string
192
+ dynamicId?: string
193
  entityType?: 'node' | 'edge'
194
  sourceId?: string
195
  targetId?: string
 
210
  name={name}
211
  value={value}
212
  onClick={onClick}
213
+ nodeId={nodeId}
214
  entityId={entityId}
215
+ edgeId={edgeId}
216
+ dynamicId={dynamicId}
217
  entityType={entityType}
218
  sourceId={sourceId}
219
  targetId={targetId}
 
276
  </div>
277
  </div>
278
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
279
+ <PropertyRow name={t('graphPanel.propertiesView.node.id')} value={String(node.id)} />
280
  <PropertyRow
281
  name={t('graphPanel.propertiesView.node.labels')}
282
  value={node.labels.join(', ')}
 
296
  key={name}
297
  name={name}
298
  value={node.properties[name]}
299
+ nodeId={String(node.id)}
300
+ entityId={node.properties['entity_id']}
301
  entityType="node"
302
  isEditable={name === 'description' || name === 'entity_id'}
303
  />
 
362
  key={name}
363
  name={name}
364
  value={edge.properties[name]}
365
+ edgeId={String(edge.id)}
366
+ dynamicId={String(edge.dynamicId)}
367
  entityType="edge"
368
+ sourceId={edge.sourceNode?.properties['entity_id'] || edge.source}
369
+ targetId={edge.targetNode?.properties['entity_id'] || edge.target}
370
  isEditable={name === 'description' || name === 'keywords'}
371
  />
372
  )
lightrag_webui/src/stores/graph.ts CHANGED
@@ -5,6 +5,8 @@ import { getGraphLabels } from '@/api/lightrag'
5
  import MiniSearch from 'minisearch'
6
 
7
  export type RawNodeType = {
 
 
8
  id: string
9
  labels: string[]
10
  properties: Record<string, any>
@@ -18,20 +20,25 @@ export type RawNodeType = {
18
  }
19
 
20
  export type RawEdgeType = {
 
 
21
  id: string
22
  source: string
23
  target: string
24
  type?: string
25
  properties: Record<string, any>
26
-
27
  dynamicId: string
28
  }
29
 
30
  export class RawGraph {
31
  nodes: RawNodeType[] = []
32
  edges: RawEdgeType[] = []
 
33
  nodeIdMap: Record<string, number> = {}
 
34
  edgeIdMap: Record<string, number> = {}
 
35
  edgeDynamicIdMap: Record<string, number> = {}
36
 
37
  getNode = (nodeId: string) => {
 
5
  import MiniSearch from 'minisearch'
6
 
7
  export type RawNodeType = {
8
+ // for NetworkX: id is identical to properties['entity_id']
9
+ // for Neo4j: id is unique identifier for each node
10
  id: string
11
  labels: string[]
12
  properties: Record<string, any>
 
20
  }
21
 
22
  export type RawEdgeType = {
23
+ // for NetworkX: id is "source-target"
24
+ // for Neo4j: id is unique identifier for each edge
25
  id: string
26
  source: string
27
  target: string
28
  type?: string
29
  properties: Record<string, any>
30
+ // dynamicId: key for sigmaGraph
31
  dynamicId: string
32
  }
33
 
34
  export class RawGraph {
35
  nodes: RawNodeType[] = []
36
  edges: RawEdgeType[] = []
37
+ // nodeIDMap: map node id to index in nodes array (SigmaGraph has nodeId as key)
38
  nodeIdMap: Record<string, number> = {}
39
+ // edgeIDMap: map edge id to index in edges array (SigmaGraph not use id as key)
40
  edgeIdMap: Record<string, number> = {}
41
+ // edgeDynamicIdMap: map edge dynamic id to index in edges array (SigmaGraph has DynamicId as key)
42
  edgeDynamicIdMap: Record<string, number> = {}
43
 
44
  getNode = (nodeId: string) => {
lightrag_webui/src/utils/graphOperations.ts CHANGED
@@ -17,30 +17,32 @@ interface EdgeToUpdate {
17
  * @param propertyName - Name of the property being updated
18
  * @param newValue - New value for the property
19
  */
20
- export const updateGraphNode = async (nodeId: string, propertyName: string, newValue: string) => {
21
  // Get graph state from store
22
  const sigmaGraph = useGraphStore.getState().sigmaGraph
23
  const rawGraph = useGraphStore.getState().rawGraph
24
 
25
  // Validate graph state
26
- if (!sigmaGraph || !rawGraph || !sigmaGraph.hasNode(String(nodeId))) {
27
  return
28
  }
29
 
30
  try {
31
- const nodeAttributes = sigmaGraph.getNodeAttributes(String(nodeId))
32
 
33
- // Special handling for entity_id changes (node renaming)
34
- if (propertyName === 'entity_id') {
 
 
35
  // Create new node with updated ID but same attributes
36
  sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue })
37
 
38
  const edgesToUpdate: EdgeToUpdate[] = []
39
 
40
  // Process all edges connected to this node
41
- sigmaGraph.forEachEdge(String(nodeId), (edge, attributes, source, target) => {
42
- const otherNode = source === String(nodeId) ? target : source
43
- const isOutgoing = source === String(nodeId)
44
 
45
  // Get original edge dynamic ID for later reference
46
  const originalEdgeDynamicId = edge
@@ -67,14 +69,15 @@ export const updateGraphNode = async (nodeId: string, propertyName: string, newV
67
  })
68
 
69
  // Remove the old node after all edges are processed
70
- sigmaGraph.dropNode(String(nodeId))
71
 
72
  // Update node reference in raw graph data
73
- const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
74
  if (nodeIndex !== undefined) {
75
  rawGraph.nodes[nodeIndex].id = newValue
 
76
  rawGraph.nodes[nodeIndex].properties.entity_id = newValue
77
- delete rawGraph.nodeIdMap[String(nodeId)]
78
  rawGraph.nodeIdMap[newValue] = nodeIndex
79
  }
80
 
@@ -82,10 +85,10 @@ export const updateGraphNode = async (nodeId: string, propertyName: string, newV
82
  edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => {
83
  if (rawGraph.edges[edgeIndex]) {
84
  // Update source/target references
85
- if (rawGraph.edges[edgeIndex].source === String(nodeId)) {
86
  rawGraph.edges[edgeIndex].source = newValue
87
  }
88
- if (rawGraph.edges[edgeIndex].target === String(nodeId)) {
89
  rawGraph.edges[edgeIndex].target = newValue
90
  }
91
 
@@ -99,10 +102,14 @@ export const updateGraphNode = async (nodeId: string, propertyName: string, newV
99
  // Update selected node in store
100
  useGraphStore.getState().setSelectedNode(newValue)
101
  } else {
102
- // For other properties, just update the property in raw graph
103
  const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
104
  if (nodeIndex !== undefined) {
105
  rawGraph.nodes[nodeIndex].properties[propertyName] = newValue
 
 
 
 
106
  }
107
  }
108
  } catch (error) {
@@ -119,7 +126,7 @@ export const updateGraphNode = async (nodeId: string, propertyName: string, newV
119
  * @param propertyName - Name of the property being updated
120
  * @param newValue - New value for the property
121
  */
122
- export const updateGraphEdge = async (sourceId: string, targetId: string, propertyName: string, newValue: string) => {
123
  // Get graph state from store
124
  const sigmaGraph = useGraphStore.getState().sigmaGraph
125
  const rawGraph = useGraphStore.getState().rawGraph
@@ -130,44 +137,15 @@ export const updateGraphEdge = async (sourceId: string, targetId: string, proper
130
  }
131
 
132
  try {
133
- // Find the edge between source and target nodes
134
- const allEdges = sigmaGraph.edges()
135
- let keyToUse = null
136
-
137
- for (const edge of allEdges) {
138
- const edgeSource = sigmaGraph.source(edge)
139
- const edgeTarget = sigmaGraph.target(edge)
140
-
141
- // Match edge in either direction (undirected graph support)
142
- if ((edgeSource === sourceId && edgeTarget === targetId) ||
143
- (edgeSource === targetId && edgeTarget === sourceId)) {
144
- keyToUse = edge
145
- break
146
- }
147
- }
148
 
149
- if (keyToUse !== null) {
150
- // Special handling for keywords property (updates edge label)
151
- if(propertyName === 'keywords') {
152
- sigmaGraph.setEdgeAttribute(keyToUse, 'label', newValue)
153
- } else {
154
- sigmaGraph.setEdgeAttribute(keyToUse, propertyName, newValue)
155
- }
156
-
157
- // Update edge in raw graph data using dynamic ID mapping
158
- if (keyToUse && rawGraph.edgeDynamicIdMap[keyToUse] !== undefined) {
159
- const edgeIndex = rawGraph.edgeDynamicIdMap[keyToUse]
160
- if (rawGraph.edges[edgeIndex]) {
161
- rawGraph.edges[edgeIndex].properties[propertyName] = newValue
162
- }
163
- } else if (keyToUse !== null) {
164
- // Fallback: try to find edge by key in edge ID map
165
- const edgeIndexByKey = rawGraph.edgeIdMap[keyToUse]
166
- if (edgeIndexByKey !== undefined && rawGraph.edges[edgeIndexByKey]) {
167
- rawGraph.edges[edgeIndexByKey].properties[propertyName] = newValue
168
- }
169
  }
170
  }
 
171
  } catch (error) {
172
  console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error)
173
  throw new Error('Failed to update edge in graph')
 
17
  * @param propertyName - Name of the property being updated
18
  * @param newValue - New value for the property
19
  */
20
+ export const updateGraphNode = async (nodeId: string, entityId:string, propertyName: string, newValue: string) => {
21
  // Get graph state from store
22
  const sigmaGraph = useGraphStore.getState().sigmaGraph
23
  const rawGraph = useGraphStore.getState().rawGraph
24
 
25
  // Validate graph state
26
+ if (!sigmaGraph || !rawGraph || !sigmaGraph.hasNode(nodeId)) {
27
  return
28
  }
29
 
30
  try {
31
+ const nodeAttributes = sigmaGraph.getNodeAttributes(nodeId)
32
 
33
+ console.log('updateGraphNode', nodeId, entityId, propertyName, newValue)
34
+
35
+ // For entity_id changes (node renaming) with NetworkX graph storage
36
+ if ((nodeId === entityId) && (propertyName === 'entity_id')) {
37
  // Create new node with updated ID but same attributes
38
  sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue })
39
 
40
  const edgesToUpdate: EdgeToUpdate[] = []
41
 
42
  // Process all edges connected to this node
43
+ sigmaGraph.forEachEdge(nodeId, (edge, attributes, source, target) => {
44
+ const otherNode = source === nodeId ? target : source
45
+ const isOutgoing = source === nodeId
46
 
47
  // Get original edge dynamic ID for later reference
48
  const originalEdgeDynamicId = edge
 
69
  })
70
 
71
  // Remove the old node after all edges are processed
72
+ sigmaGraph.dropNode(nodeId)
73
 
74
  // Update node reference in raw graph data
75
+ const nodeIndex = rawGraph.nodeIdMap[nodeId]
76
  if (nodeIndex !== undefined) {
77
  rawGraph.nodes[nodeIndex].id = newValue
78
+ rawGraph.nodes[nodeIndex].labels = [newValue]
79
  rawGraph.nodes[nodeIndex].properties.entity_id = newValue
80
+ delete rawGraph.nodeIdMap[nodeId]
81
  rawGraph.nodeIdMap[newValue] = nodeIndex
82
  }
83
 
 
85
  edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => {
86
  if (rawGraph.edges[edgeIndex]) {
87
  // Update source/target references
88
+ if (rawGraph.edges[edgeIndex].source === nodeId) {
89
  rawGraph.edges[edgeIndex].source = newValue
90
  }
91
+ if (rawGraph.edges[edgeIndex].target === nodeId) {
92
  rawGraph.edges[edgeIndex].target = newValue
93
  }
94
 
 
102
  // Update selected node in store
103
  useGraphStore.getState().setSelectedNode(newValue)
104
  } else {
105
+ // for none NetworkX nodes or none entity_id changes
106
  const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
107
  if (nodeIndex !== undefined) {
108
  rawGraph.nodes[nodeIndex].properties[propertyName] = newValue
109
+ if (propertyName === 'entity_id') {
110
+ rawGraph.nodes[nodeIndex].labels = [newValue]
111
+ sigmaGraph.setNodeAttribute(String(nodeId), 'label', newValue)
112
+ }
113
  }
114
  }
115
  } catch (error) {
 
126
  * @param propertyName - Name of the property being updated
127
  * @param newValue - New value for the property
128
  */
129
+ export const updateGraphEdge = async (edgeId: string, dynamicId: string, sourceId: string, targetId: string, propertyName: string, newValue: string) => {
130
  // Get graph state from store
131
  const sigmaGraph = useGraphStore.getState().sigmaGraph
132
  const rawGraph = useGraphStore.getState().rawGraph
 
137
  }
138
 
139
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ const edgeIndex = rawGraph.edgeIdMap[String(edgeId)]
142
+ if (edgeIndex !== undefined && rawGraph.edges[edgeIndex]) {
143
+ rawGraph.edges[edgeIndex].properties[propertyName] = newValue
144
+ if(dynamicId !== undefined && propertyName === 'keywords') {
145
+ sigmaGraph.setEdgeAttribute(dynamicId, 'label', newValue)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  }
147
  }
148
+
149
  } catch (error) {
150
  console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error)
151
  throw new Error('Failed to update edge in graph')