This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user