216 lines
9.8 KiB
TypeScript
216 lines
9.8 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import { NavLink, Outlet } from 'react-router-dom';
|
||
import {
|
||
LayoutDashboard,
|
||
Building2,
|
||
CreditCard,
|
||
Package,
|
||
Smartphone,
|
||
Server,
|
||
BellRing,
|
||
AlertTriangle,
|
||
FileCheck2,
|
||
RotateCcw,
|
||
CheckCircle2,
|
||
Radio,
|
||
ExternalLink,
|
||
} from 'lucide-react';
|
||
import { useKccStore } from '../store/kccStore';
|
||
import { OperatorSessionSwitcher } from './OperatorSessionSwitcher';
|
||
import { InspectorDrawer } from './InspectorDrawer';
|
||
import { ConfirmOperatorActionModal } from './ConfirmOperatorAction';
|
||
import { ServerMonitorOutageBanner } from './ServerMonitorOutageBanner';
|
||
|
||
export const ControlCenterShell: React.FC = () => {
|
||
const isInitialized = useKccStore((state) => state.isInitialized);
|
||
const initialize = useKccStore((state) => state.initialize);
|
||
const resetToDefaults = useKccStore((state) => state.resetToDefaults);
|
||
const serverMonitorOutage = useKccStore((state) => state.serverMonitorOutage);
|
||
const toggleServerMonitorOutage = useKccStore((state) => state.toggleServerMonitorOutage);
|
||
|
||
const [utcTime, setUtcTime] = useState('');
|
||
const [isResetting, setIsResetting] = useState(false);
|
||
const [showSavedFeedback, setShowSavedFeedback] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!isInitialized) {
|
||
initialize();
|
||
}
|
||
}, [isInitialized, initialize]);
|
||
|
||
useEffect(() => {
|
||
const updateTime = () => {
|
||
const now = new Date();
|
||
setUtcTime(now.toISOString().replace('T', ' ').slice(0, 19) + ' UTC');
|
||
};
|
||
updateTime();
|
||
const interval = setInterval(updateTime, 1000);
|
||
return () => clearInterval(interval);
|
||
}, []);
|
||
|
||
const handleReset = async () => {
|
||
if (confirm('Сбросить состояние демонстрационной базы данных к исходному состоянию?')) {
|
||
setIsResetting(true);
|
||
await resetToDefaults();
|
||
setIsResetting(false);
|
||
setShowSavedFeedback(true);
|
||
setTimeout(() => setShowSavedFeedback(false), 3000);
|
||
}
|
||
};
|
||
|
||
const navItems = [
|
||
{ to: '/control', label: 'Обзор', icon: LayoutDashboard, end: true },
|
||
{ to: '/control/organizations', label: 'Организации', icon: Building2 },
|
||
{ to: '/control/billing', label: 'Подписки и биллинг', icon: CreditCard },
|
||
{ to: '/control/modules', label: 'Каталог модулей', icon: Package },
|
||
{ to: '/control/installations', label: 'Установки и клиенты', icon: Smartphone },
|
||
{ to: '/control/infrastructure', label: 'Инфраструктура', icon: Server },
|
||
{ to: '/control/announcements', label: 'Сообщения', icon: BellRing },
|
||
{ to: '/control/incidents', label: 'Инциденты', icon: AlertTriangle },
|
||
{ to: '/control/audit', label: 'Неизменяемый аудит', icon: FileCheck2 },
|
||
];
|
||
|
||
return (
|
||
<div className="min-h-screen max-w-full overflow-x-hidden bg-[#07130e] text-zinc-100 flex flex-col font-sans selection:bg-amber-500 selection:text-zinc-950">
|
||
{/* Top Console Bar */}
|
||
<header className="h-14 border-b border-zinc-800 bg-[#0a1b14] px-2.5 sm:px-4 flex items-center justify-between z-30 shrink-0 max-w-full overflow-hidden">
|
||
<div className="flex items-center gap-2 sm:gap-4 min-w-0">
|
||
{/* Logo & Title */}
|
||
<div className="flex items-center gap-2 sm:gap-2.5 min-w-0">
|
||
<div className="w-7 h-7 sm:w-8 sm:h-8 rounded-lg bg-emerald-950 border border-amber-500/40 flex items-center justify-center text-amber-400 font-extrabold shadow-sm shrink-0">
|
||
K
|
||
</div>
|
||
<div className="min-w-0">
|
||
<div className="text-xs font-black tracking-widest text-amber-400 uppercase flex items-center gap-1.5">
|
||
<span className="shrink-0">Krona Control Center</span>
|
||
<span className="text-[9px] px-1.5 py-0.2 rounded-xs bg-emerald-900/80 text-emerald-300 font-mono hidden sm:inline">
|
||
DEMO v1.0
|
||
</span>
|
||
</div>
|
||
<div className="text-[10px] text-zinc-400 font-mono tracking-wider hidden sm:block">
|
||
Кабинет владельца платформы
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2 text-[11px] text-zinc-500 font-mono pl-2 sm:pl-4 border-l border-zinc-800 shrink-0">
|
||
<span className="hidden xl:inline">{utcTime}</span>
|
||
<span className="hidden xl:inline">•</span>
|
||
<span className="flex items-center gap-1 text-emerald-400">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse shrink-0" />
|
||
<span className="text-[10px] sm:text-xs">IndexedDB Synced</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Header Actions */}
|
||
<div className="flex items-center gap-1.5 sm:gap-3 shrink-0">
|
||
{/* Server Monitor Simulation Toggle */}
|
||
<button
|
||
type="button"
|
||
onClick={toggleServerMonitorOutage}
|
||
className={`flex items-center gap-1.5 px-2 sm:px-2.5 py-1 rounded-md text-xs font-medium border transition-colors ${
|
||
serverMonitorOutage
|
||
? 'bg-rose-950 text-rose-300 border-rose-700/60'
|
||
: 'bg-zinc-900 text-zinc-400 border-zinc-800 hover:text-zinc-200'
|
||
}`}
|
||
title="Переключить симуляцию недоступности Server Monitor"
|
||
>
|
||
<Radio className={`w-3.5 h-3.5 ${serverMonitorOutage ? 'text-rose-400 animate-pulse' : 'text-zinc-500'} shrink-0`} />
|
||
<span className="hidden md:inline">{serverMonitorOutage ? 'SM Outage Sim (Активен)' : 'Simulate SM Outage'}</span>
|
||
</button>
|
||
|
||
{/* Reset Demo DB Button */}
|
||
<button
|
||
type="button"
|
||
onClick={handleReset}
|
||
disabled={isResetting}
|
||
className="hidden sm:flex items-center gap-1.5 px-2 sm:px-2.5 py-1 rounded-md text-xs font-medium bg-zinc-900 hover:bg-zinc-800 text-zinc-300 border border-zinc-800 transition-colors disabled:opacity-50 shrink-0"
|
||
title="Сбросить демонстрационные данные к эталону"
|
||
>
|
||
<RotateCcw className={`w-3.5 h-3.5 ${isResetting ? 'animate-spin' : ''} shrink-0`} />
|
||
<span className="hidden md:inline">Сброс Demo</span>
|
||
</button>
|
||
|
||
{/* Operator RBAC Switcher */}
|
||
<OperatorSessionSwitcher />
|
||
|
||
{/* Return to Client App Link */}
|
||
<NavLink
|
||
to="/"
|
||
className="hidden xl:flex items-center gap-1 text-xs text-zinc-500 hover:text-zinc-300 transition-colors pl-2"
|
||
title="Перейти в клиентское приложение платформы"
|
||
>
|
||
<span>В приложение</span>
|
||
<ExternalLink className="w-3 h-3" />
|
||
</NavLink>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Main Console Layout */}
|
||
<div className="flex-1 flex flex-col md:flex-row overflow-hidden max-w-full">
|
||
{/* Compact Operator Sidebar */}
|
||
<aside
|
||
className="w-full md:w-56 border-b md:border-b-0 md:border-r border-zinc-800 bg-[#091712] flex flex-row md:flex-col shrink-0 overflow-x-auto md:overflow-y-auto"
|
||
aria-label="Навигация Control Center"
|
||
>
|
||
<div className="p-2 md:p-3 flex flex-row md:flex-col gap-1 shrink-0">
|
||
<div className="hidden md:block px-3 py-1.5 text-[10px] font-bold text-zinc-500 uppercase tracking-widest">
|
||
Управление
|
||
</div>
|
||
{navItems.map((item) => {
|
||
const Icon = item.icon;
|
||
return (
|
||
<NavLink
|
||
key={item.to}
|
||
to={item.to}
|
||
end={item.end}
|
||
className={({ isActive }) =>
|
||
`flex items-center gap-2 px-2.5 py-1.5 md:px-3 md:py-2 rounded-lg text-xs font-medium whitespace-nowrap transition-colors ${
|
||
isActive
|
||
? 'bg-amber-500/15 text-amber-300 border border-amber-500/30 font-semibold'
|
||
: 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/60'
|
||
}`
|
||
}
|
||
>
|
||
<Icon className="w-4 h-4 shrink-0" />
|
||
<span>{item.label}</span>
|
||
</NavLink>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Sidebar Footer Info */}
|
||
<div className="hidden md:block mt-auto p-3 border-t border-zinc-800/80 text-[11px] text-zinc-500 space-y-1 bg-zinc-950/40">
|
||
<div className="flex justify-between items-center">
|
||
<span>Security Gate:</span>
|
||
<span className="text-emerald-400 font-semibold">Active</span>
|
||
</div>
|
||
<div className="flex justify-between items-center">
|
||
<span>Node Identity:</span>
|
||
<span className="text-zinc-400 font-mono">178.212.13.102</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* Content Workspace */}
|
||
<main className="flex-1 flex flex-col overflow-y-auto bg-[#07130e] p-3 sm:p-4 md:p-6 min-w-0 max-w-full">
|
||
<ServerMonitorOutageBanner />
|
||
{showSavedFeedback && (
|
||
<div className="mb-4 p-3 rounded-lg bg-emerald-950/80 border border-emerald-700/60 text-emerald-200 text-xs flex items-center gap-2 animate-in fade-in">
|
||
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
|
||
<span>Демонстрационные данные успешно сброшены и сохранены в IndexedDB.</span>
|
||
</div>
|
||
)}
|
||
<Outlet />
|
||
</main>
|
||
</div>
|
||
|
||
{/* Contextual Inspector Drawer */}
|
||
<InspectorDrawer />
|
||
|
||
{/* Modal Dialog for Dangerous Operations */}
|
||
<ConfirmOperatorActionModal />
|
||
</div>
|
||
);
|
||
};
|