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