Initial commit: enterprise digital platform with portal SSO, DigiServer, IT Assets, NetworkView, Server Monitor

This commit is contained in:
ske087
2026-05-10 21:07:50 +03:00
commit 8d9df56b0b
364 changed files with 73655 additions and 0 deletions
@@ -0,0 +1,235 @@
import { useState, useEffect, useCallback } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import * as api from '../api';
import type { Site, Room, Rack } from '../types';
interface TreeRoom extends Room {
racks: Rack[];
expanded: boolean;
}
interface TreeSite extends Site {
rooms: TreeRoom[];
expanded: boolean;
}
export default function Sidebar() {
const [tree, setTree] = useState<TreeSite[]>([]);
const [search, setSearch] = useState('');
const [searchResults, setSearchResults] = useState<Awaited<ReturnType<typeof api.search>> | null>(null);
const [loading, setLoading] = useState(true);
const [addSiteName, setAddSiteName] = useState('');
const [showAddSite, setShowAddSite] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const loadTree = useCallback(async () => {
const sites = await api.getSites();
setTree(sites.map(s => ({ ...s, rooms: [], expanded: false })));
setLoading(false);
}, []);
useEffect(() => { loadTree(); }, [loadTree]);
const expandSite = async (siteId: string) => {
setTree(prev => prev.map(s => {
if (s.id !== siteId) return s;
if (s.expanded) return { ...s, expanded: false };
return { ...s, expanded: true };
}));
// Load rooms if not loaded
setTree(prev => {
const site = prev.find(s => s.id === siteId);
if (site && site.rooms.length === 0) {
api.getSite(siteId).then(full => {
setTree(p => p.map(s => s.id === siteId
? { ...s, rooms: (full.rooms ?? []).map(r => ({ ...r, racks: [], expanded: false })) }
: s
));
});
}
return prev;
});
};
const expandRoom = async (siteId: string, roomId: string) => {
setTree(prev => prev.map(s => s.id !== siteId ? s : {
...s,
rooms: s.rooms.map(r => {
if (r.id !== roomId) return r;
if (r.expanded) return { ...r, expanded: false };
// Load racks
if (r.racks.length === 0) {
api.getRacks(roomId).then(racks => {
setTree(p => p.map(ss => ss.id !== siteId ? ss : {
...ss,
rooms: ss.rooms.map(rr => rr.id === roomId ? { ...rr, racks } : rr),
}));
});
}
return { ...r, expanded: true };
}),
}));
};
const handleAddSite = async (e: React.FormEvent) => {
e.preventDefault();
if (!addSiteName.trim()) return;
const site = await api.createSite({ name: addSiteName.trim() });
setAddSiteName('');
setShowAddSite(false);
await loadTree();
navigate(`/sites/${site.id}`);
};
useEffect(() => {
if (!search.trim()) { setSearchResults(null); return; }
const t = setTimeout(async () => {
const r = await api.search(search);
setSearchResults(r);
}, 300);
return () => clearTimeout(t);
}, [search]);
const isActive = (path: string) => location.pathname === path;
return (
<aside className="sidebar">
<div className="sidebar-header">
<Link to="/" className="sidebar-logo">
<span className="logo-icon"></span>
<span className="logo-text">NetworkView</span>
</Link>
</div>
<div className="sidebar-search">
<input
type="text"
placeholder="Search..."
value={search}
onChange={e => setSearch(e.target.value)}
className="search-input"
/>
{searchResults && (
<div className="search-results">
{searchResults.sites.map(s => (
<Link key={s.id} to={`/sites/${s.id}`} className="search-result-item" onClick={() => setSearch('')}>
<span className="search-result-icon">🏢</span> {s.name}
</Link>
))}
{searchResults.rooms.map(r => (
<Link key={r.id} to={`/rooms/${r.id}`} className="search-result-item" onClick={() => setSearch('')}>
<span className="search-result-icon">🚪</span> {r.name}
</Link>
))}
{searchResults.racks.map(r => (
<Link key={r.id} to={`/racks/${r.id}`} className="search-result-item" onClick={() => setSearch('')}>
<span className="search-result-icon">🗄</span> {r.name}
</Link>
))}
{searchResults.components.map(c => (
<Link key={c.id} to={`/components/${c.id}`} className="search-result-item" onClick={() => setSearch('')}>
<span className="search-result-icon">🔧</span> {c.name}
{c.ip_address && <span className="search-result-sub"> {c.ip_address}</span>}
</Link>
))}
{!searchResults.sites.length && !searchResults.rooms.length &&
!searchResults.racks.length && !searchResults.components.length && (
<div className="search-no-results">No results found</div>
)}
</div>
)}
</div>
<nav className="sidebar-nav">
<div className="sidebar-section-header">
<span>Sites</span>
<button className="icon-btn" title="Add site" onClick={() => setShowAddSite(v => !v)}>+</button>
</div>
{showAddSite && (
<form onSubmit={handleAddSite} className="inline-add-form">
<input
autoFocus
type="text"
placeholder="Site name..."
value={addSiteName}
onChange={e => setAddSiteName(e.target.value)}
className="inline-input"
/>
<button type="submit" className="inline-btn">Add</button>
<button type="button" className="inline-btn secondary" onClick={() => setShowAddSite(false)}></button>
</form>
)}
{loading && <div className="sidebar-loading">Loading...</div>}
{tree.map(site => (
<div key={site.id} className="tree-node">
<div className={`tree-item tree-site ${isActive(`/sites/${site.id}`) ? 'active' : ''}`}>
<button className="tree-expand" onClick={() => expandSite(site.id)}>
{site.expanded ? '▾' : '▸'}
</button>
<Link to={`/sites/${site.id}`} className="tree-label">
<span className="tree-icon">🏢</span>
<span className="tree-name">{site.name}</span>
{site.room_count != null && (
<span className="tree-count">{site.room_count}</span>
)}
</Link>
</div>
{site.expanded && (
<div className="tree-children">
{site.rooms.map(room => (
<div key={room.id} className="tree-node">
<div className={`tree-item tree-room ${isActive(`/rooms/${room.id}`) ? 'active' : ''}`}>
<button className="tree-expand" onClick={() => expandRoom(site.id, room.id)}>
{room.expanded ? '▾' : '▸'}
</button>
<Link to={`/rooms/${room.id}`} className="tree-label">
<span className="tree-icon">🚪</span>
<span className="tree-name">{room.name}</span>
</Link>
</div>
{room.expanded && (
<div className="tree-children">
{room.racks.map(rack => (
<div key={rack.id} className={`tree-item tree-rack ${isActive(`/racks/${rack.id}`) ? 'active' : ''}`}>
<Link to={`/racks/${rack.id}`} className="tree-label">
<span className="tree-icon">🗄</span>
<span className="tree-name">{rack.name}</span>
<span className="tree-count">{rack.total_units}U</span>
</Link>
</div>
))}
</div>
)}
</div>
))}
{site.rooms.length === 0 && (
<div className="tree-empty">No rooms yet</div>
)}
</div>
)}
</div>
))}
{!loading && tree.length === 0 && (
<div className="sidebar-empty">
<p>No sites yet.</p>
<button className="btn-primary btn-sm" onClick={() => setShowAddSite(true)}>
+ Add your first site
</button>
</div>
)}
</nav>
<div className="sidebar-footer">
<Link to="/settings" className={`sidebar-footer-link ${isActive('/settings') ? 'active' : ''}`}>
Settings
</Link>
</div>
</aside>
);
}