import { useMemo } from 'react'
import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
import Text from '@/components/ui/Text'
import Button from '@/components/ui/Button'
import useLightragGraph from '@/hooks/useLightragGraph'
import { useTranslation } from 'react-i18next'
import { GitBranchPlus, Scissors } from 'lucide-react'
import EditablePropertyRow from './EditablePropertyRow'
/**
* Component that view properties of elements in graph.
*/
const PropertiesView = () => {
const { getNode, getEdge } = useLightragGraph()
const selectedNode = useGraphStore.use.selectedNode()
const focusedNode = useGraphStore.use.focusedNode()
const selectedEdge = useGraphStore.use.selectedEdge()
const focusedEdge = useGraphStore.use.focusedEdge()
const graphDataVersion = useGraphStore.use.graphDataVersion()
const { currentElement, currentType } = useMemo(() => {
let type: 'node' | 'edge' | null = null
let element: RawNodeType | RawEdgeType | null = null
if (focusedNode) {
type = 'node'
element = getNode(focusedNode)
} else if (selectedNode) {
type = 'node'
element = getNode(selectedNode)
} else if (focusedEdge) {
type = 'edge'
element = getEdge(focusedEdge, true)
} else if (selectedEdge) {
type = 'edge'
element = getEdge(selectedEdge, true)
}
if (element) {
return {
currentElement: type === 'node'
? refineNodeProperties(element as any)
: refineEdgeProperties(element as any),
currentType: type
}
}
return { currentElement: null, currentType: null }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [focusedNode, selectedNode, focusedEdge, selectedEdge, graphDataVersion, getNode, getEdge])
if (!currentElement) {
return <>>
}
return (
{currentType == 'node' ? (
) : (
)}
)
}
type NodeType = RawNodeType & {
relationships: {
type: string
id: string
label: string
}[]
}
type EdgeType = RawEdgeType & {
sourceNode?: RawNodeType
targetNode?: RawNodeType
}
const refineNodeProperties = (node: RawNodeType): NodeType => {
const state = useGraphStore.getState()
const relationships = []
if (state.sigmaGraph && state.rawGraph) {
try {
if (!state.sigmaGraph.hasNode(node.id)) {
console.warn('Node not found in sigmaGraph:', node.id)
return {
...node,
relationships: []
}
}
const edges = state.sigmaGraph.edges(node.id)
for (const edgeId of edges) {
if (!state.sigmaGraph.hasEdge(edgeId)) continue;
const edge = state.rawGraph.getEdge(edgeId, true)
if (edge) {
const isTarget = node.id === edge.source
const neighbourId = isTarget ? edge.target : edge.source
if (!state.sigmaGraph.hasNode(neighbourId)) continue;
const neighbour = state.rawGraph.getNode(neighbourId)
if (neighbour) {
relationships.push({
type: 'Neighbour',
id: neighbourId,
label: neighbour.properties['entity_id'] ? neighbour.properties['entity_id'] : neighbour.labels.join(', ')
})
}
}
}
} catch (error) {
console.error('Error refining node properties:', error)
}
}
return {
...node,
relationships
}
}
const refineEdgeProperties = (edge: RawEdgeType): EdgeType => {
const state = useGraphStore.getState()
let sourceNode: RawNodeType | undefined = undefined
let targetNode: RawNodeType | undefined = undefined
if (state.sigmaGraph && state.rawGraph) {
try {
if (!state.sigmaGraph.hasEdge(edge.dynamicId)) {
console.warn('Edge not found in sigmaGraph:', edge.id, 'dynamicId:', edge.dynamicId)
return {
...edge,
sourceNode: undefined,
targetNode: undefined
}
}
if (state.sigmaGraph.hasNode(edge.source)) {
sourceNode = state.rawGraph.getNode(edge.source)
}
if (state.sigmaGraph.hasNode(edge.target)) {
targetNode = state.rawGraph.getNode(edge.target)
}
} catch (error) {
console.error('Error refining edge properties:', error)
}
}
return {
...edge,
sourceNode,
targetNode
}
}
const PropertyRow = ({
name,
value,
onClick,
tooltip,
nodeId,
edgeId,
dynamicId,
entityId,
entityType,
sourceId,
targetId,
isEditable = false,
truncate
}: {
name: string
value: any
onClick?: () => void
tooltip?: string
nodeId?: string
entityId?: string
edgeId?: string
dynamicId?: string
entityType?: 'node' | 'edge'
sourceId?: string
targetId?: string
isEditable?: boolean
truncate?: string
}) => {
const { t } = useTranslation()
const getPropertyNameTranslation = (name: string) => {
const translationKey = `graphPanel.propertiesView.node.propertyNames.${name}`
const translation = t(translationKey)
return translation === translationKey ? name : translation
}
// Utility function to convert to newlines
const formatValueWithSeparators = (value: any): string => {
if (typeof value === 'string') {
return value.replace(//g, ';\n')
}
return typeof value === 'string' ? value : JSON.stringify(value, null, 2)
}
// Format the value to convert to newlines
const formattedValue = formatValueWithSeparators(value)
let formattedTooltip = tooltip || formatValueWithSeparators(value)
// If this is source_id field and truncate info exists, append it to the tooltip
if (name === 'source_id' && truncate) {
formattedTooltip += `\n(Truncated: ${truncate})`
}
// Use EditablePropertyRow for editable fields (description, entity_id and entity_type)
if (isEditable && (name === 'description' || name === 'entity_id' || name === 'entity_type' || name === 'keywords')) {
return (
)
}
// For non-editable fields, use the regular Text component
return (
{getPropertyNameTranslation(name)}
{name === 'source_id' && truncate && †}
:
)
}
const NodePropertiesView = ({ node }: { node: NodeType }) => {
const { t } = useTranslation()
const handleExpandNode = () => {
useGraphStore.getState().triggerNodeExpand(node.id)
}
const handlePruneNode = () => {
useGraphStore.getState().triggerNodePrune(node.id)
}
return (
{t('graphPanel.propertiesView.node.title')}
{
useGraphStore.getState().setSelectedNode(node.id, true)
}}
/>
{t('graphPanel.propertiesView.node.properties')}
{Object.keys(node.properties)
.sort()
.map((name) => {
if (name === 'created_at' || name === 'truncate') return null; // Hide created_at and truncate properties
return (
)
})}
{node.relationships.length > 0 && (
<>
{t('graphPanel.propertiesView.node.relationships')}
{node.relationships.map(({ type, id, label }) => {
return (
{
useGraphStore.getState().setSelectedNode(id, true)
}}
/>
)
})}
>
)}
)
}
const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
const { t } = useTranslation()
return (
{t('graphPanel.propertiesView.edge.title')}
{edge.type &&
}
{
useGraphStore.getState().setSelectedNode(edge.source, true)
}}
/>
{
useGraphStore.getState().setSelectedNode(edge.target, true)
}}
/>
{t('graphPanel.propertiesView.edge.properties')}
{Object.keys(edge.properties)
.sort()
.map((name) => {
if (name === 'created_at' || name === 'truncate') return null; // Hide created_at and truncate properties
return (
)
})}
)
}
export default PropertiesView