Add clickable relation navigation to inventory panel
CI / basic-check (push) Has been cancelled

This commit is contained in:
Perplexity Bot
2026-08-04 19:58:31 +00:00
parent 92b0136015
commit d2fd4d1078
5 changed files with 97 additions and 48 deletions
@@ -1,31 +1,14 @@
import type { NodeContext } from '../../types/inventory';
import { RelationNavigator } from './RelationNavigator';
import './inventory.css';
type Props = {
context: NodeContext | null;
loading?: boolean;
onNavigateNode: (id: string) => void;
};
function RelationList({ title, items }: { title: string; items: NodeContext['relations']['incoming'] }) {
return (
<div className="inventory-context-block">
<h3>{title}</h3>
{items.length === 0 ? (
<p className="inventory-muted">Nessuna relazione</p>
) : (
<ul className="inventory-list">
{items.map((item) => (
<li key={item.id}>
<strong>{item.type}</strong> · {item.layer} · {item.source_id} {item.target_id}
</li>
))}
</ul>
)}
</div>
);
}
export function NodeContextPanel({ context, loading }: Props) {
export function NodeContextPanel({ context, loading, onNavigateNode }: Props) {
if (loading) {
return (
<section className="inventory-panel inventory-context-panel">
@@ -56,12 +39,14 @@ export function NodeContextPanel({ context, loading }: Props) {
<div className="inventory-context-grid">
<div className="inventory-context-block">
<h3>Physical</h3>
<p><strong>Parent:</strong> {parent ? parent.name : 'Nessuno'}</p>
<p><strong>Parent:</strong> {parent ? <button className="inventory-link-button inline" onClick={() => onNavigateNode(parent.id)}>{parent.name}</button> : 'Nessuno'}</p>
<p><strong>Children:</strong> {children.length}</p>
{children.length > 0 && (
<ul className="inventory-list">
{children.map((child) => (
<li key={child.id}>{child.name} · {child.type}</li>
<li key={child.id}>
<button className="inventory-link-button inline" onClick={() => onNavigateNode(child.id)}>{child.name}</button> · {child.type}
</li>
))}
</ul>
)}
@@ -76,8 +61,15 @@ export function NodeContextPanel({ context, loading }: Props) {
</div>
</div>
<RelationList title="Incoming relations" items={relations.incoming} />
<RelationList title="Outgoing relations" items={relations.outgoing} />
<div className="inventory-context-block">
<h3>Outgoing relations</h3>
<RelationNavigator relations={relations.outgoing} onNavigate={onNavigateNode} />
</div>
<div className="inventory-context-block">
<h3>Incoming relations</h3>
<RelationNavigator relations={relations.incoming} onNavigate={onNavigateNode} />
</div>
</section>
);
}
@@ -0,0 +1,30 @@
import type { Relation } from '../../types/inventory';
import './inventory.css';
type Props = {
relations: Relation[];
onNavigate: (id: string) => void;
};
export function RelationNavigator({ relations, onNavigate }: Props) {
if (relations.length === 0) {
return <p className="inventory-muted">Nessuna relazione navigabile.</p>;
}
return (
<ul className="inventory-list inventory-relation-list">
{relations.map((relation) => {
const targetId = relation.target_id;
return (
<li key={relation.id} className="inventory-relation-item">
<button className="inventory-link-button" onClick={() => onNavigate(targetId)}>
<strong>{relation.type}</strong> <span className="inventory-muted">({relation.layer})</span>
<span className="inventory-relation-arrow"></span>
<span>{targetId}</span>
</button>
</li>
);
})}
</ul>
);
}
@@ -46,7 +46,8 @@
}
.inventory-tree-toggle,
.inventory-tree-label {
.inventory-tree-label,
.inventory-link-button {
background: transparent;
border: 0;
color: inherit;
@@ -96,6 +97,7 @@
background: rgba(255,255,255,0.03);
border-radius: 10px;
padding: 12px;
margin-bottom: 12px;
}
.inventory-list {
@@ -113,6 +115,32 @@
color: #cbd5e1;
}
.inventory-link-button.inline {
padding: 0;
color: #93c5fd;
text-decoration: underline;
text-underline-offset: 2px;
}
.inventory-link-button.inline:hover,
.inventory-link-button:hover {
color: #bfdbfe;
}
.inventory-relation-list {
padding-left: 0;
list-style: none;
}
.inventory-relation-item {
margin-top: 8px;
}
.inventory-relation-arrow {
margin: 0 6px;
color: #94a3b8;
}
@media (max-width: 960px) {
.inventory-layout {
grid-template-columns: 1fr;
+6 -1
View File
@@ -1,4 +1,4 @@
import type { EntityResponse, InventoryTreeNode, ListResponse, NodeContext } from '../types/inventory';
import type { EntityResponse, InventoryTreeNode, ListResponse, NodeContext, Relation } from '../types/inventory';
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
@@ -21,3 +21,8 @@ export async function fetchNodeContext(id: string): Promise<NodeContext> {
const data = await fetchJson<EntityResponse<NodeContext>>(`/inventory/nodes/${id}/context`);
return data.item;
}
export async function fetchNodeRelations(id: string): Promise<Relation[]> {
const data = await fetchJson<ListResponse<Relation>>(`/inventory/nodes/${id}/relations`);
return data.items;
}
+16 -22
View File
@@ -20,6 +20,19 @@ export function InventoryPage() {
const [loadingContext, setLoadingContext] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadContext = async (id: string) => {
setSelectedId(id);
setLoadingContext(true);
try {
const item = await fetchNodeContext(id);
setContext(item);
} catch (err) {
setError(err instanceof Error ? err.message : 'Errore sconosciuto');
} finally {
setLoadingContext(false);
}
};
useEffect(() => {
let active = true;
setLoadingTree(true);
@@ -28,7 +41,7 @@ export function InventoryPage() {
if (!active) return;
setNodes(items);
const first = findFirstNode(items);
setSelectedId(first);
if (first) loadContext(first);
})
.catch((err: Error) => {
if (!active) return;
@@ -42,25 +55,6 @@ export function InventoryPage() {
};
}, []);
useEffect(() => {
if (!selectedId) return;
let active = true;
setLoadingContext(true);
fetchNodeContext(selectedId)
.then((item) => {
if (active) setContext(item);
})
.catch((err: Error) => {
if (active) setError(err.message);
})
.finally(() => {
if (active) setLoadingContext(false);
});
return () => {
active = false;
};
}, [selectedId]);
if (loadingTree) {
return <div className="inventory-empty">Caricamento inventory...</div>;
}
@@ -71,8 +65,8 @@ export function InventoryPage() {
return (
<div className="inventory-layout">
<InventoryTree nodes={nodes} selectedId={selectedId} onSelect={setSelectedId} />
<NodeContextPanel context={context} loading={loadingContext} />
<InventoryTree nodes={nodes} selectedId={selectedId} onSelect={loadContext} />
<NodeContextPanel context={context} loading={loadingContext} onNavigateNode={loadContext} />
</div>
);
}