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 ReactDOM from 'react-dom/client'
|
||||
import { App } from './pages/App'
|
||||
import { App } from './App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<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