diff --git a/frontend/src/App.css b/frontend/src/App.css index 8172974..2f2860b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -141,3 +141,48 @@ html, body, #root { padding: var(--space-8); text-align: center; } + + +.actions { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; +} + +.status { + display: inline-flex; + align-items: center; + padding: 0.15rem 0.5rem; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 600; +} + +.status-active { + background: #d9efe2; + color: #216c43; +} + +.status-failed { + background: #f8d7da; + color: #8a1f2d; +} + +.status-inactive { + background: #ececec; + color: #555; +} + +.status-pending { + background: #fff3cd; + color: #8a6d1f; +} + +.alert { + margin-bottom: var(--space-4); + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-sm); + background: #f8d7da; + color: #8a1f2d; + border: 1px solid #f1b8c0; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index da1a37a..e32bb4d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,29 +4,30 @@ import { TopologyView } from './pages/TopologyView'; import { InventoryView } from './pages/InventoryView'; import { IPsView } from './pages/IPsView'; import { DiscoveryJobsView } from './pages/DiscoveryJobsView'; +import { ServicesView } from './pages/ServicesView'; import type { ViewKey } from './types'; export function App() { const [current, setCurrent] = useState('inventory'); + const commonProps = { onNavigate: setCurrent }; + const renderView = () => { switch (current) { case 'topology': - return ; + return ; case 'inventory': - return ; + return ; case 'ips': - return ; + return ; case 'discovery': - return ; + return ; + case 'services': + return ; default: - return ; + return ; } }; - return ( - <> - {renderView()} - - ); + return renderView(); } diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 1bccee5..992e893 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -10,6 +10,7 @@ const views: { key: ViewKey; label: string }[] = [ { key: 'inventory', label: 'Inventory' }, { key: 'ips', label: 'IPs' }, { key: 'discovery', label: 'Discovery Jobs' }, + { key: 'services', label: 'Services' }, ]; export function Sidebar({ current, onChange }: Props) { diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0ac00b7..c550667 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,9 +1,8 @@ -import type { NetObject } from '../types'; +import type { NetObject, ServiceInfo } from '../types'; -const API_BASE = '/api/v1'; +const AGENT_BASE = 'http://127.0.0.1:8001'; 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' }, @@ -12,3 +11,17 @@ export async function fetchObjects(): Promise { { id: '5', name: 'Firewall Edge', slug: 'firewall-edge', type: 'firewall', status: 'active' }, ]; } + +export async function fetchServices(): Promise { + const res = await fetch(`${AGENT_BASE}/services`); + if (!res.ok) throw new Error('Impossibile leggere lo stato dei servizi'); + return res.json(); +} + +export async function serviceAction(name: string, action: 'start' | 'stop' | 'restart') { + const res = await fetch(`${AGENT_BASE}/services/${encodeURIComponent(name)}/${action}`, { + method: 'POST', + }); + if (!res.ok) throw new Error(`Azione ${action} fallita su ${name}`); + return res.json(); +} diff --git a/frontend/src/pages/DiscoveryJobsView.tsx b/frontend/src/pages/DiscoveryJobsView.tsx index f702a81..a2dd5dc 100644 --- a/frontend/src/pages/DiscoveryJobsView.tsx +++ b/frontend/src/pages/DiscoveryJobsView.tsx @@ -1,8 +1,9 @@ import { AppLayout } from '../components/AppLayout'; +import type { ViewKey } from '../types'; -export function DiscoveryJobsView() { +export function DiscoveryJobsView({ onNavigate }: { onNavigate: (v: ViewKey) => void }) { return ( - {}}> +

Discovery Jobs

diff --git a/frontend/src/pages/IPsView.tsx b/frontend/src/pages/IPsView.tsx index 0fbc985..ac8fb68 100644 --- a/frontend/src/pages/IPsView.tsx +++ b/frontend/src/pages/IPsView.tsx @@ -1,8 +1,9 @@ import { AppLayout } from '../components/AppLayout'; +import type { ViewKey } from '../types'; -export function IPsView() { +export function IPsView({ onNavigate }: { onNavigate: (v: ViewKey) => void }) { return ( - {}}> +

IPs

diff --git a/frontend/src/pages/InventoryView.tsx b/frontend/src/pages/InventoryView.tsx index c6036d7..861a249 100644 --- a/frontend/src/pages/InventoryView.tsx +++ b/frontend/src/pages/InventoryView.tsx @@ -1,8 +1,9 @@ import { useState, useEffect } from 'react'; import { AppLayout } from '../components/AppLayout'; import { fetchObjects } from '../lib/api'; +import type { ViewKey } from '../types'; -export function InventoryView() { +export function InventoryView({ onNavigate }: { onNavigate: (v: ViewKey) => void }) { const [objects, setObjects] = useState>>([]); useEffect(() => { @@ -10,7 +11,7 @@ export function InventoryView() { }, []); return ( - {}}> +

Inventory

diff --git a/frontend/src/pages/ServicesView.tsx b/frontend/src/pages/ServicesView.tsx new file mode 100644 index 0000000..0b374f5 --- /dev/null +++ b/frontend/src/pages/ServicesView.tsx @@ -0,0 +1,108 @@ +import { useCallback, useEffect, useState } from 'react'; +import { AppLayout } from '../components/AppLayout'; +import { fetchServices, serviceAction } from '../lib/api'; +import type { ServiceInfo, ViewKey } from '../types'; + +function stateClass(state: string) { + switch (state) { + case 'active': + return 'status status-active'; + case 'failed': + return 'status status-failed'; + case 'activating': + case 'deactivating': + return 'status status-pending'; + default: + return 'status status-inactive'; + } +} + +export function ServicesView({ onNavigate }: { onNavigate: (v: ViewKey) => void }) { + const [services, setServices] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(null); + + const load = useCallback(async () => { + try { + setError(null); + const data = await fetchServices(); + setServices(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Errore sconosciuto'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + load(); + const timer = setInterval(load, 5000); + return () => clearInterval(timer); + }, [load]); + + const runAction = async (name: string, action: 'start' | 'stop' | 'restart') => { + try { + setBusy(`${name}:${action}`); + await serviceAction(name, action); + await load(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Errore sconosciuto'); + } finally { + setBusy(null); + } + }; + + return ( + +
+

Services

+ +
+ + {error &&
{error}
} + +
+ {loading ? ( +

Caricamento stato servizi...

+ ) : ( + + + + + + + + + + + + + {services.length === 0 && ( + + + + )} + {services.map(service => ( + + + + + + + + + ))} + +
ServiceStateSubstateLoadedPIDActions
Nessun servizio disponibile
{service.name}{service.active_state}{service.sub_state}{service.loaded}{service.pid ?? '-'} +
+ + + +
+
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/TopologyView.tsx b/frontend/src/pages/TopologyView.tsx index 7ac0385..642dcac 100644 --- a/frontend/src/pages/TopologyView.tsx +++ b/frontend/src/pages/TopologyView.tsx @@ -1,17 +1,17 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { AppLayout } from '../components/AppLayout'; import type { ViewKey } from '../types'; import { fetchObjects } from '../lib/api'; -export function TopologyView() { +export function TopologyView({ onNavigate }: { onNavigate: (v: ViewKey) => void }) { const [objects, setObjects] = useState>>([]); - useState(() => { + useEffect(() => { fetchObjects().then(setObjects); - }); + }, []); return ( - {}}> +

Topology

diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index efc6ad8..9f35bcb 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -26,4 +26,12 @@ export interface NetObject { notes?: string; } -export type ViewKey = 'topology' | 'inventory' | 'ips' | 'discovery'; +export type ViewKey = 'topology' | 'inventory' | 'ips' | 'discovery' | 'services'; + +export interface ServiceInfo { + name: string; + active_state: string; + sub_state: string; + loaded: string; + pid?: number | null; +}