Add basic frontend: sidebar, views and inventory table
CI / basic-check (push) Has been cancelled

This commit is contained in:
Perplexity Bot
2026-08-03 22:06:00 +00:00
parent 00ecb1862d
commit 871f4d0547
11 changed files with 373 additions and 1 deletions
+143
View File
@@ -0,0 +1,143 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--color-bg: #f7f6f2;
--color-surface: #ffffff;
--color-surface-offset: #f3f0ec;
--color-divider: #dcd9d5;
--color-text: #28251d;
--color-text-muted: #7a7974;
--color-primary: #01696f;
--color-primary-hover: #0c4e54;
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--font-body: 'Inter', 'Segoe UI', sans-serif;
}
html, body, #root {
height: 100%;
font-family: var(--font-body);
color: var(--color-text);
background: var(--color-bg);
}
.app {
display: grid;
grid-template-columns: 220px 1fr;
height: 100%;
}
.sidebar {
background: var(--color-surface);
border-right: 1px solid var(--color-divider);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.sidebar-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: var(--space-4);
}
.nav-item {
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.15s;
font-size: 0.9rem;
}
.nav-item:hover {
background: var(--color-surface-offset);
}
.nav-item.active {
background: var(--color-primary);
color: #fff;
}
.main {
padding: var(--space-6);
overflow: auto;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-6);
}
.page-title {
font-size: 1.25rem;
font-weight: 600;
}
.card {
background: var(--color-surface);
border: 1px solid var(--color-divider);
border-radius: var(--radius-md);
padding: var(--space-4);
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.table th, .table td {
text-align: left;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-divider);
}
.table th {
font-weight: 600;
color: var(--color-text-muted);
}
.btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
border: 1px solid var(--color-divider);
background: var(--color-surface);
cursor: pointer;
font-size: 0.9rem;
transition: background 0.15s, border-color 0.15s;
}
.btn:hover {
background: var(--color-surface-offset);
}
.btn-primary {
background: var(--color-primary);
color: #fff;
border-color: var(--color-primary);
}
.btn-primary:hover {
background: var(--color-primary-hover);
}
.empty {
color: var(--color-text-muted);
padding: var(--space-8);
text-align: center;
}
+32
View File
@@ -0,0 +1,32 @@
import { useState } from 'react';
import './App.css';
import { TopologyView } from './pages/TopologyView';
import { InventoryView } from './pages/InventoryView';
import { IPsView } from './pages/IPsView';
import { DiscoveryJobsView } from './pages/DiscoveryJobsView';
import type { ViewKey } from './types';
export function App() {
const [current, setCurrent] = useState<ViewKey>('inventory');
const renderView = () => {
switch (current) {
case 'topology':
return <TopologyView />;
case 'inventory':
return <InventoryView />;
case 'ips':
return <IPsView />;
case 'discovery':
return <DiscoveryJobsView />;
default:
return <InventoryView />;
}
};
return (
<>
{renderView()}
</>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { Sidebar } from './Sidebar';
import type { ViewKey } from '../types';
interface Props {
current: ViewKey;
onNavigate: (v: ViewKey) => void;
children: React.ReactNode;
}
export function AppLayout({ current, onNavigate, children }: Props) {
return (
<div className="app">
<Sidebar current={current} onChange={onNavigate} />
<main className="main">
{children}
</main>
</div>
);
}
+30
View File
@@ -0,0 +1,30 @@
import type { ViewKey } from '../types';
interface Props {
current: ViewKey;
onChange: (v: ViewKey) => void;
}
const views: { key: ViewKey; label: string }[] = [
{ key: 'topology', label: 'Topology' },
{ key: 'inventory', label: 'Inventory' },
{ key: 'ips', label: 'IPs' },
{ key: 'discovery', label: 'Discovery Jobs' },
];
export function Sidebar({ current, onChange }: Props) {
return (
<aside className="sidebar">
<div className="sidebar-title">NetMapper</div>
{views.map(v => (
<div
key={v.key}
className={`nav-item ${current === v.key ? 'active' : ''}`}
onClick={() => onChange(v.key)}
>
{v.label}
</div>
))}
</aside>
);
}
+14
View File
@@ -0,0 +1,14 @@
import type { NetObject } from '../types';
const API_BASE = '/api/v1';
export async function fetchObjects(): Promise<NetObject[]> {
// Mock data for now
return [
{ id: '1', name: 'Site HQ', slug: 'site-hq', type: 'site', status: 'active' },
{ id: '2', name: 'Rack A', slug: 'rack-a', type: 'rack', status: 'active' },
{ id: '3', name: 'Server Proxmox', slug: 'server-proxmox', type: 'physical_host', vendor: 'Dell', model: 'R730', status: 'active' },
{ id: '4', name: 'Switch Core', slug: 'switch-core', type: 'switch', vendor: 'Mikrotik', status: 'active' },
{ id: '5', name: 'Firewall Edge', slug: 'firewall-edge', type: 'firewall', status: 'active' },
];
}
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import { App } from './pages/App' import { App } from './App'
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
+14
View File
@@ -0,0 +1,14 @@
import { AppLayout } from '../components/AppLayout';
export function DiscoveryJobsView() {
return (
<AppLayout current="discovery" onNavigate={() => {}}>
<header className="header">
<h1 className="page-title">Discovery Jobs</h1>
</header>
<div className="card">
<p className="empty">Job di discovery: prossima implementazione.</p>
</div>
</AppLayout>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { AppLayout } from '../components/AppLayout';
export function IPsView() {
return (
<AppLayout current="ips" onNavigate={() => {}}>
<header className="header">
<h1 className="page-title">IPs</h1>
</header>
<div className="card">
<p className="empty">Registro IP: prossima implementazione.</p>
</div>
</AppLayout>
);
}
+48
View File
@@ -0,0 +1,48 @@
import { useState, useEffect } from 'react';
import { AppLayout } from '../components/AppLayout';
import { fetchObjects } from '../lib/api';
export function InventoryView() {
const [objects, setObjects] = useState<Awaited<ReturnType<typeof fetchObjects>>>([]);
useEffect(() => {
fetchObjects().then(setObjects);
}, []);
return (
<AppLayout current="inventory" onNavigate={() => {}}>
<header className="header">
<h1 className="page-title">Inventory</h1>
</header>
<div className="card">
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Vendor</th>
<th>Model</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{objects.length === 0 && (
<tr>
<td colSpan={5} className="empty">Nessun oggetto</td>
</tr>
)}
{objects.map(o => (
<tr key={o.id}>
<td>{o.name}</td>
<td>{o.type}</td>
<td>{o.vendor ?? '-'}</td>
<td>{o.model ?? '-'}</td>
<td>{o.status ?? '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
</AppLayout>
);
}
+29
View File
@@ -0,0 +1,29 @@
import { useState } from 'react';
import { AppLayout } from '../components/AppLayout';
import type { ViewKey } from '../types';
import { fetchObjects } from '../lib/api';
export function TopologyView() {
const [objects, setObjects] = useState<Awaited<ReturnType<typeof fetchObjects>>>([]);
useState(() => {
fetchObjects().then(setObjects);
});
return (
<AppLayout current="topology" onNavigate={() => {}}>
<header className="header">
<h1 className="page-title">Topology</h1>
</header>
<div className="card">
<p className="empty">Vista topologia: prossima implementazione (grafo/annidamento).</p>
<div style={{ marginTop: '1rem' }}>
<strong>Oggetti mockati:</strong>
<pre style={{ marginTop: '0.5rem', fontSize: '0.85rem' }}>
{JSON.stringify(objects, null, 2)}
</pre>
</div>
</div>
</AppLayout>
);
}
+29
View File
@@ -0,0 +1,29 @@
export type ObjectType =
| 'site'
| 'area'
| 'rack'
| 'physical_host'
| 'vm'
| 'lxc'
| 'app'
| 'switch'
| 'firewall'
| 'router'
| 'ap'
| 'camera'
| 'storage'
| 'other';
export interface NetObject {
id: string;
name: string;
slug: string;
type: ObjectType;
status?: string;
vendor?: string;
model?: string;
serial?: string;
notes?: string;
}
export type ViewKey = 'topology' | 'inventory' | 'ips' | 'discovery';