diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..8172974 --- /dev/null +++ b/frontend/src/App.css @@ -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; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..da1a37a --- /dev/null +++ b/frontend/src/App.tsx @@ -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('inventory'); + + const renderView = () => { + switch (current) { + case 'topology': + return ; + case 'inventory': + return ; + case 'ips': + return ; + case 'discovery': + return ; + default: + return ; + } + }; + + return ( + <> + {renderView()} + + ); +} diff --git a/frontend/src/components/AppLayout.tsx b/frontend/src/components/AppLayout.tsx new file mode 100644 index 0000000..f5ebf20 --- /dev/null +++ b/frontend/src/components/AppLayout.tsx @@ -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 ( +
+ +
+ {children} +
+
+ ); +} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..1bccee5 --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -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 ( + + ); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..0ac00b7 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,14 @@ +import type { NetObject } from '../types'; + +const API_BASE = '/api/v1'; + +export async function fetchObjects(): Promise { + // 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' }, + ]; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 6cc4900..ee3f745 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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( diff --git a/frontend/src/pages/DiscoveryJobsView.tsx b/frontend/src/pages/DiscoveryJobsView.tsx new file mode 100644 index 0000000..f702a81 --- /dev/null +++ b/frontend/src/pages/DiscoveryJobsView.tsx @@ -0,0 +1,14 @@ +import { AppLayout } from '../components/AppLayout'; + +export function DiscoveryJobsView() { + return ( + {}}> +
+

Discovery Jobs

+
+
+

Job di discovery: prossima implementazione.

+
+
+ ); +} diff --git a/frontend/src/pages/IPsView.tsx b/frontend/src/pages/IPsView.tsx new file mode 100644 index 0000000..0fbc985 --- /dev/null +++ b/frontend/src/pages/IPsView.tsx @@ -0,0 +1,14 @@ +import { AppLayout } from '../components/AppLayout'; + +export function IPsView() { + return ( + {}}> +
+

IPs

+
+
+

Registro IP: prossima implementazione.

+
+
+ ); +} diff --git a/frontend/src/pages/InventoryView.tsx b/frontend/src/pages/InventoryView.tsx new file mode 100644 index 0000000..c6036d7 --- /dev/null +++ b/frontend/src/pages/InventoryView.tsx @@ -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>>([]); + + useEffect(() => { + fetchObjects().then(setObjects); + }, []); + + return ( + {}}> +
+

Inventory

+
+
+ + + + + + + + + + + + {objects.length === 0 && ( + + + + )} + {objects.map(o => ( + + + + + + + + ))} + +
NameTypeVendorModelStatus
Nessun oggetto
{o.name}{o.type}{o.vendor ?? '-'}{o.model ?? '-'}{o.status ?? '-'}
+
+
+ ); +} diff --git a/frontend/src/pages/TopologyView.tsx b/frontend/src/pages/TopologyView.tsx new file mode 100644 index 0000000..7ac0385 --- /dev/null +++ b/frontend/src/pages/TopologyView.tsx @@ -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>>([]); + + useState(() => { + fetchObjects().then(setObjects); + }); + + return ( + {}}> +
+

Topology

+
+
+

Vista topologia: prossima implementazione (grafo/annidamento).

+
+ Oggetti mockati: +
+            {JSON.stringify(objects, null, 2)}
+          
+
+
+
+ ); +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..efc6ad8 --- /dev/null +++ b/frontend/src/types/index.ts @@ -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';