krona-front/src/features/control-center/components/ControlCenterShell.tsx

216 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};