mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +00:00
feat: improve connection display with reusable component and instant actor type updates
Creates reusable ConnectionDisplay component and enhances connection visualization in both Actor and Relation properties panels. Changes: - Add ConnectionDisplay component for consistent connection visualization - Shows source and target actors with icons, labels, and type names - Includes direction indicators (→, ↔, —) based on directionality - Provides tooltips with node IDs on hover - Enhance Actor Properties connections section - Display full actor information instead of just node IDs - Show edge type badges with color indicators - Include custom labels when different from type labels - Use ConnectionDisplay component for rich connection details - Refactor Relation Properties to use ConnectionDisplay - Eliminates duplicate connection rendering code - Maintains consistent UI across panels - Change actor type updates to apply instantly - Remove debounce delay for actor type dropdown changes - Provides immediate visual feedback when changing types - Consistent with relation type and directionality behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1646cfb0ce
commit
fab5c035a5
2 changed files with 124 additions and 70 deletions
79
src/components/Common/ConnectionDisplay.tsx
Normal file
79
src/components/Common/ConnectionDisplay.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
|
import { getIconComponent } from '../../utils/iconUtils';
|
||||||
|
import type { Actor, NodeTypeConfig, EdgeDirectionality } from '../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConnectionDisplay - Reusable component for displaying actor connections
|
||||||
|
*
|
||||||
|
* Shows source and target actors with:
|
||||||
|
* - Node type icon
|
||||||
|
* - Node label
|
||||||
|
* - Node type label
|
||||||
|
* - Direction indicator based on directionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ConnectionDisplayProps {
|
||||||
|
sourceNode: Actor | undefined;
|
||||||
|
targetNode: Actor | undefined;
|
||||||
|
nodeTypes: NodeTypeConfig[];
|
||||||
|
directionality: EdgeDirectionality;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConnectionDisplay = ({
|
||||||
|
sourceNode,
|
||||||
|
targetNode,
|
||||||
|
nodeTypes,
|
||||||
|
directionality
|
||||||
|
}: ConnectionDisplayProps) => {
|
||||||
|
const sourceType = nodeTypes.find(nt => nt.id === sourceNode?.data?.type);
|
||||||
|
const targetType = nodeTypes.find(nt => nt.id === targetNode?.data?.type);
|
||||||
|
const SourceIcon = sourceType ? getIconComponent(sourceType.icon) : null;
|
||||||
|
const TargetIcon = targetType ? getIconComponent(targetType.icon) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-600 py-2 bg-gray-50 rounded px-2 space-x-2">
|
||||||
|
{/* Source Actor */}
|
||||||
|
<Tooltip title={`ID: ${sourceNode?.id || 'Unknown'}`} placement="top">
|
||||||
|
<div className="flex items-center space-x-1 flex-1">
|
||||||
|
{SourceIcon && (
|
||||||
|
<div className="flex-shrink-0" style={{ color: sourceType?.color, fontSize: '14px' }}>
|
||||||
|
<SourceIcon fontSize="small" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="font-medium truncate">
|
||||||
|
{sourceNode?.data?.label || sourceNode?.id || 'Unknown'}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-400 text-[10px] flex-shrink-0">
|
||||||
|
({sourceType?.label || 'Unknown'})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Direction Indicator */}
|
||||||
|
<span className="flex-shrink-0 text-gray-500">
|
||||||
|
{directionality === 'directed' && '→'}
|
||||||
|
{directionality === 'bidirectional' && '↔'}
|
||||||
|
{directionality === 'undirected' && '—'}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Target Actor */}
|
||||||
|
<Tooltip title={`ID: ${targetNode?.id || 'Unknown'}`} placement="top">
|
||||||
|
<div className="flex items-center space-x-1 flex-1 justify-end">
|
||||||
|
<span className="text-gray-400 text-[10px] flex-shrink-0">
|
||||||
|
({targetType?.label || 'Unknown'})
|
||||||
|
</span>
|
||||||
|
<span className="font-medium truncate">
|
||||||
|
{targetNode?.data?.label || targetNode?.id || 'Unknown'}
|
||||||
|
</span>
|
||||||
|
{TargetIcon && (
|
||||||
|
<div className="flex-shrink-0" style={{ color: targetType?.color, fontSize: '14px' }}>
|
||||||
|
<TargetIcon fontSize="small" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectionDisplay;
|
||||||
|
|
@ -12,7 +12,7 @@ import { useGraphWithHistory } from '../../hooks/useGraphWithHistory';
|
||||||
import { useDocumentHistory } from '../../hooks/useDocumentHistory';
|
import { useDocumentHistory } from '../../hooks/useDocumentHistory';
|
||||||
import { useConfirm } from '../../hooks/useConfirm';
|
import { useConfirm } from '../../hooks/useConfirm';
|
||||||
import GraphMetrics from '../Common/GraphMetrics';
|
import GraphMetrics from '../Common/GraphMetrics';
|
||||||
import { getIconComponent } from '../../utils/iconUtils';
|
import ConnectionDisplay from '../Common/ConnectionDisplay';
|
||||||
import type { Actor, Relation, EdgeDirectionality } from '../../types';
|
import type { Actor, Relation, EdgeDirectionality } from '../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -264,8 +264,18 @@ const RightPanel = ({ selectedNode, selectedEdge, onClose }: Props) => {
|
||||||
<select
|
<select
|
||||||
value={actorType}
|
value={actorType}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setActorType(e.target.value);
|
const newType = e.target.value;
|
||||||
setHasNodeChanges(true);
|
setActorType(newType);
|
||||||
|
// Apply actor type change instantly (no debounce)
|
||||||
|
if (selectedNode) {
|
||||||
|
updateNode(selectedNode.id, {
|
||||||
|
data: {
|
||||||
|
type: newType,
|
||||||
|
label: actorLabel,
|
||||||
|
description: actorDescription || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
|
|
@ -331,24 +341,37 @@ const RightPanel = ({ selectedNode, selectedEdge, onClose }: Props) => {
|
||||||
{connections.length === 0 ? (
|
{connections.length === 0 ? (
|
||||||
<p className="text-xs text-gray-500 italic">No connections</p>
|
<p className="text-xs text-gray-500 italic">No connections</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-3">
|
||||||
{connections.map((edge) => {
|
{connections.map((edge) => {
|
||||||
const edgeConfig = edgeTypes.find((et) => et.id === edge.data?.type);
|
const edgeConfig = edgeTypes.find((et) => et.id === edge.data?.type);
|
||||||
const isOutgoing = edge.source === selectedNode.id;
|
const sourceNode = nodes.find(n => n.id === edge.source);
|
||||||
const otherId = isOutgoing ? edge.target : edge.source;
|
const targetNode = nodes.find(n => n.id === edge.target);
|
||||||
|
const edgeDirectionality = edge.data?.directionality || edgeConfig?.defaultDirectionality || 'directed';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={edge.id} className="space-y-1">
|
||||||
key={edge.id}
|
{/* Edge Type Badge */}
|
||||||
className="text-xs text-gray-600 flex items-center space-x-1"
|
<div className="flex items-center space-x-1">
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
className="inline-block w-2 h-2 rounded-full"
|
className="inline-block w-2 h-2 rounded-full"
|
||||||
style={{ backgroundColor: edgeConfig?.color || '#6b7280' }}
|
style={{ backgroundColor: edgeConfig?.color || '#6b7280' }}
|
||||||
/>
|
/>
|
||||||
<span className="font-medium">{edgeConfig?.label || 'Unknown'}</span>
|
<span className="text-xs font-medium text-gray-700">
|
||||||
<span>{isOutgoing ? '→' : '←'}</span>
|
{edgeConfig?.label || 'Unknown'}
|
||||||
<span className="text-gray-500">{otherId}</span>
|
</span>
|
||||||
|
{edge.data?.label && edge.data.label !== edgeConfig?.label && (
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
({edge.data.label})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Connection Display */}
|
||||||
|
<ConnectionDisplay
|
||||||
|
sourceNode={sourceNode}
|
||||||
|
targetNode={targetNode}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
directionality={edgeDirectionality}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -539,60 +562,12 @@ const RightPanel = ({ selectedNode, selectedEdge, onClose }: Props) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-xs text-gray-600 py-2 bg-gray-50 rounded px-2 space-x-2">
|
<ConnectionDisplay
|
||||||
{/* Source Actor */}
|
sourceNode={nodes.find(n => n.id === currentEdge.source)}
|
||||||
<Tooltip title={`ID: ${currentEdge.source}`} placement="top">
|
targetNode={nodes.find(n => n.id === currentEdge.target)}
|
||||||
<div className="flex items-center space-x-1 flex-1">
|
nodeTypes={nodeTypes}
|
||||||
{(() => {
|
directionality={relationDirectionality}
|
||||||
const sourceNode = nodes.find(n => n.id === currentEdge.source);
|
/>
|
||||||
const sourceType = nodeTypes.find(nt => nt.id === sourceNode?.data?.type);
|
|
||||||
const IconComponent = sourceType ? getIconComponent(sourceType.icon) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{IconComponent && (
|
|
||||||
<div className="flex-shrink-0" style={{ color: sourceType?.color, fontSize: '14px' }}>
|
|
||||||
<IconComponent fontSize="small" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<span className="font-medium truncate">{sourceNode?.data?.label || currentEdge.source}</span>
|
|
||||||
<span className="text-gray-400 text-[10px] flex-shrink-0">({sourceType?.label || 'Unknown'})</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{/* Direction Indicator */}
|
|
||||||
<span className="flex-shrink-0 text-gray-500">
|
|
||||||
{relationDirectionality === 'directed' && '→'}
|
|
||||||
{relationDirectionality === 'bidirectional' && '↔'}
|
|
||||||
{relationDirectionality === 'undirected' && '—'}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Target Actor */}
|
|
||||||
<Tooltip title={`ID: ${currentEdge.target}`} placement="top">
|
|
||||||
<div className="flex items-center space-x-1 flex-1 justify-end">
|
|
||||||
{(() => {
|
|
||||||
const targetNode = nodes.find(n => n.id === currentEdge.target);
|
|
||||||
const targetType = nodeTypes.find(nt => nt.id === targetNode?.data?.type);
|
|
||||||
const IconComponent = targetType ? getIconComponent(targetType.icon) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className="text-gray-400 text-[10px] flex-shrink-0">({targetType?.label || 'Unknown'})</span>
|
|
||||||
<span className="font-medium truncate">{targetNode?.data?.label || currentEdge.target}</span>
|
|
||||||
{IconComponent && (
|
|
||||||
<div className="flex-shrink-0" style={{ color: targetType?.color, fontSize: '14px' }}>
|
|
||||||
<IconComponent fontSize="small" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue