This commit is contained in:
@@ -1,31 +1,14 @@
|
|||||||
import type { NodeContext } from '../../types/inventory';
|
import type { NodeContext } from '../../types/inventory';
|
||||||
|
import { RelationNavigator } from './RelationNavigator';
|
||||||
import './inventory.css';
|
import './inventory.css';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
context: NodeContext | null;
|
context: NodeContext | null;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
onNavigateNode: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function RelationList({ title, items }: { title: string; items: NodeContext['relations']['incoming'] }) {
|
export function NodeContextPanel({ context, loading, onNavigateNode }: Props) {
|
||||||
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) {
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<section className="inventory-panel inventory-context-panel">
|
<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-grid">
|
||||||
<div className="inventory-context-block">
|
<div className="inventory-context-block">
|
||||||
<h3>Physical</h3>
|
<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>
|
<p><strong>Children:</strong> {children.length}</p>
|
||||||
{children.length > 0 && (
|
{children.length > 0 && (
|
||||||
<ul className="inventory-list">
|
<ul className="inventory-list">
|
||||||
{children.map((child) => (
|
{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>
|
</ul>
|
||||||
)}
|
)}
|
||||||
@@ -76,8 +61,15 @@ export function NodeContextPanel({ context, loading }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RelationList title="Incoming relations" items={relations.incoming} />
|
<div className="inventory-context-block">
|
||||||
<RelationList title="Outgoing relations" items={relations.outgoing} />
|
<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>
|
</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-toggle,
|
||||||
.inventory-tree-label {
|
.inventory-tree-label,
|
||||||
|
.inventory-link-button {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -96,6 +97,7 @@
|
|||||||
background: rgba(255,255,255,0.03);
|
background: rgba(255,255,255,0.03);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-list {
|
.inventory-list {
|
||||||
@@ -113,6 +115,32 @@
|
|||||||
color: #cbd5e1;
|
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) {
|
@media (max-width: 960px) {
|
||||||
.inventory-layout {
|
.inventory-layout {
|
||||||
grid-template-columns: 1fr;
|
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> {
|
async function handleResponse<T>(response: Response): Promise<T> {
|
||||||
if (!response.ok) {
|
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`);
|
const data = await fetchJson<EntityResponse<NodeContext>>(`/inventory/nodes/${id}/context`);
|
||||||
return data.item;
|
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 [loadingContext, setLoadingContext] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
setLoadingTree(true);
|
setLoadingTree(true);
|
||||||
@@ -28,7 +41,7 @@ export function InventoryPage() {
|
|||||||
if (!active) return;
|
if (!active) return;
|
||||||
setNodes(items);
|
setNodes(items);
|
||||||
const first = findFirstNode(items);
|
const first = findFirstNode(items);
|
||||||
setSelectedId(first);
|
if (first) loadContext(first);
|
||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
if (!active) return;
|
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) {
|
if (loadingTree) {
|
||||||
return <div className="inventory-empty">Caricamento inventory...</div>;
|
return <div className="inventory-empty">Caricamento inventory...</div>;
|
||||||
}
|
}
|
||||||
@@ -71,8 +65,8 @@ export function InventoryPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inventory-layout">
|
<div className="inventory-layout">
|
||||||
<InventoryTree nodes={nodes} selectedId={selectedId} onSelect={setSelectedId} />
|
<InventoryTree nodes={nodes} selectedId={selectedId} onSelect={loadContext} />
|
||||||
<NodeContextPanel context={context} loading={loadingContext} />
|
<NodeContextPanel context={context} loading={loadingContext} onNavigateNode={loadContext} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user