Add services status view with polling and control actions
CI / basic-check (push) Has been cancelled
CI / basic-check (push) Has been cancelled
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+11
-10
@@ -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<ViewKey>('inventory');
|
||||
|
||||
const commonProps = { onNavigate: setCurrent };
|
||||
|
||||
const renderView = () => {
|
||||
switch (current) {
|
||||
case 'topology':
|
||||
return <TopologyView />;
|
||||
return <TopologyView {...commonProps} />;
|
||||
case 'inventory':
|
||||
return <InventoryView />;
|
||||
return <InventoryView {...commonProps} />;
|
||||
case 'ips':
|
||||
return <IPsView />;
|
||||
return <IPsView {...commonProps} />;
|
||||
case 'discovery':
|
||||
return <DiscoveryJobsView />;
|
||||
return <DiscoveryJobsView {...commonProps} />;
|
||||
case 'services':
|
||||
return <ServicesView {...commonProps} />;
|
||||
default:
|
||||
return <InventoryView />;
|
||||
return <InventoryView {...commonProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderView()}
|
||||
</>
|
||||
);
|
||||
return renderView();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+16
-3
@@ -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<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' },
|
||||
@@ -12,3 +11,17 @@ export async function fetchObjects(): Promise<NetObject[]> {
|
||||
{ id: '5', name: 'Firewall Edge', slug: 'firewall-edge', type: 'firewall', status: 'active' },
|
||||
];
|
||||
}
|
||||
|
||||
export async function fetchServices(): Promise<ServiceInfo[]> {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<AppLayout current="discovery" onNavigate={() => {}}>
|
||||
<AppLayout current="discovery" onNavigate={onNavigate}>
|
||||
<header className="header">
|
||||
<h1 className="page-title">Discovery Jobs</h1>
|
||||
</header>
|
||||
|
||||
@@ -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 (
|
||||
<AppLayout current="ips" onNavigate={() => {}}>
|
||||
<AppLayout current="ips" onNavigate={onNavigate}>
|
||||
<header className="header">
|
||||
<h1 className="page-title">IPs</h1>
|
||||
</header>
|
||||
|
||||
@@ -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<Awaited<ReturnType<typeof fetchObjects>>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -10,7 +11,7 @@ export function InventoryView() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppLayout current="inventory" onNavigate={() => {}}>
|
||||
<AppLayout current="inventory" onNavigate={onNavigate}>
|
||||
<header className="header">
|
||||
<h1 className="page-title">Inventory</h1>
|
||||
</header>
|
||||
|
||||
@@ -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<ServiceInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState<string | null>(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 (
|
||||
<AppLayout current="services" onNavigate={onNavigate}>
|
||||
<header className="header">
|
||||
<h1 className="page-title">Services</h1>
|
||||
<button className="btn" onClick={load}>Refresh</button>
|
||||
</header>
|
||||
|
||||
{error && <div className="alert">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
{loading ? (
|
||||
<p className="empty">Caricamento stato servizi...</p>
|
||||
) : (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>State</th>
|
||||
<th>Substate</th>
|
||||
<th>Loaded</th>
|
||||
<th>PID</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{services.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="empty">Nessun servizio disponibile</td>
|
||||
</tr>
|
||||
)}
|
||||
{services.map(service => (
|
||||
<tr key={service.name}>
|
||||
<td>{service.name}</td>
|
||||
<td><span className={stateClass(service.active_state)}>{service.active_state}</span></td>
|
||||
<td>{service.sub_state}</td>
|
||||
<td>{service.loaded}</td>
|
||||
<td>{service.pid ?? '-'}</td>
|
||||
<td>
|
||||
<div className="actions">
|
||||
<button className="btn" disabled={!!busy} onClick={() => runAction(service.name, 'start')}>Start</button>
|
||||
<button className="btn" disabled={!!busy} onClick={() => runAction(service.name, 'stop')}>Stop</button>
|
||||
<button className="btn btn-primary" disabled={!!busy} onClick={() => runAction(service.name, 'restart')}>Restart</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
@@ -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<Awaited<ReturnType<typeof fetchObjects>>>([]);
|
||||
|
||||
useState(() => {
|
||||
useEffect(() => {
|
||||
fetchObjects().then(setObjects);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppLayout current="topology" onNavigate={() => {}}>
|
||||
<AppLayout current="topology" onNavigate={onNavigate}>
|
||||
<header className="header">
|
||||
<h1 className="page-title">Topology</h1>
|
||||
</header>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user