feat(control-center): adapt ModulesPage styling, rollout stages, and RU strings

This commit is contained in:
ochenstarik 2026-09-11 09:40:38 +07:00
parent 41aa560d38
commit 5115517403

View file

@ -5,10 +5,12 @@ import {
Pause,
RotateCcw,
Sliders,
Sparkles,
} from 'lucide-react';
import { useKccStore, useOperatorRole } from '../store/kccStore';
import { StatusBadge } from '../components/StatusBadge';
import { RolloutStage } from '../contracts';
import { RU_ROLLOUT_STAGE, RU_MODULE_CATEGORY } from '../locales/ru';
export const ModulesPage: React.FC = () => {
const modules = useKccStore((state) => state.modules);
@ -31,10 +33,11 @@ export const ModulesPage: React.FC = () => {
const handleStageChange = (stage: RolloutStage, percentage: number) => {
if (!activeRollout) return;
const stageLabel = RU_ROLLOUT_STAGE[stage] || stage;
openConfirmModal({
title: `Смена фазы раскатки: ${activeRollout.moduleName}`,
actionName: `rollout.stage_${stage}`,
impactSummary: `Конвейер раскатки будет переведен в состояние «${stage}» (${percentage}% трафика).`,
impactSummary: `Конвейер раскатки будет переведен в состояние «${stageLabel}» (${percentage}% трафика).`,
targetType: 'rollout',
targetId: activeRollout.rolloutId,
targetName: activeRollout.moduleName,
@ -68,34 +71,39 @@ export const ModulesPage: React.FC = () => {
};
return (
<div className="space-y-6">
<div className="space-y-4 md:space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b border-zinc-800 pb-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b border-krona-border pb-4">
<div>
<h1 className="text-xl font-bold text-zinc-100 flex items-center gap-2">
<Package className="w-5 h-5 text-amber-400" />
<h1 className="text-lg md:text-xl font-bold text-krona-primary flex items-center gap-2">
<Package className="w-5 h-5 text-amber-500 shrink-0" />
<span>Каталог модулей и Rollout конвейер</span>
</h1>
<p className="text-xs text-zinc-400 mt-0.5">
Управление манифестами, compatibility core/schema, пилотными организациями и фазовыми релизами
<p className="text-xs text-krona-secondary mt-0.5">
Управление версиями модулей, совместимостью с ядром, пилотными группами и канареечным развертыванием
</p>
</div>
<div className="text-xs text-zinc-400">
Всего модулей: <span className="font-bold text-zinc-100">{modules.length}</span> • Активных раскаток:{' '}
<span className="font-bold text-amber-400">{rollouts.length}</span>
<div className="flex items-center gap-2 text-xs text-krona-secondary">
<span className="px-2.5 py-1 rounded-md bg-krona-workspace border border-krona-border">
Всего модулей: <strong className="text-krona-primary">{modules.length}</strong>
</span>
<span className="px-2.5 py-1 rounded-md bg-amber-500/10 border border-amber-500/30 text-amber-500 font-semibold">
Активных раскаток: <strong>{rollouts.length}</strong>
</span>
</div>
</div>
{/* Grid: Module Selector + Module Detail & Rollout Manager */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-6">
{/* Left Column: Modules Catalog List */}
<div className="rounded-xl border border-zinc-800 bg-[#091a13] p-4 space-y-3">
<div className="text-xs font-semibold text-zinc-400 uppercase tracking-wider mb-1">
<div className="rounded-xl border border-krona-border bg-krona-surface p-3 md:p-4 space-y-3 shadow-xs">
<div className="text-xs font-semibold text-krona-secondary uppercase tracking-wider px-1">
Каталог системных модулей
</div>
<div className="space-y-2">
{/* Module list with min 44px items */}
<div className="space-y-2 max-h-[480px] lg:max-h-none overflow-y-auto">
{modules.map((m) => {
const isSelected = m.moduleId === selectedModuleId;
const hasRollout = rollouts.some((r) => r.moduleId === m.moduleId);
@ -104,24 +112,24 @@ export const ModulesPage: React.FC = () => {
<div
key={m.moduleId}
onClick={() => setSelectedModuleId(m.moduleId)}
className={`p-3 rounded-lg border text-xs cursor-pointer transition-colors space-y-1.5 ${
className={`min-h-[44px] p-3 rounded-lg border text-xs cursor-pointer transition-colors space-y-1.5 ${
isSelected
? 'bg-amber-500/15 border-amber-500/40 text-zinc-100'
: 'bg-zinc-950/60 border-zinc-800 text-zinc-400 hover:bg-zinc-800/50 hover:text-zinc-200'
? 'bg-amber-500/15 border-amber-500/40 text-krona-primary shadow-xs'
: 'bg-krona-workspace border-krona-border text-krona-secondary hover:bg-krona-surface hover:text-krona-primary'
}`}
>
<div className="flex justify-between items-center">
<span className="font-bold text-zinc-100">{m.name}</span>
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded-xs bg-zinc-900 border border-zinc-700 text-amber-300">
<span className="font-bold text-krona-primary">{m.name}</span>
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded-md bg-krona-surface border border-krona-border text-amber-500 font-semibold">
v{m.latestVersion}
</span>
</div>
<p className="text-[11px] text-zinc-400 line-clamp-1">{m.description}</p>
<div className="flex justify-between items-center text-[10px] text-zinc-500 pt-1">
<span>Тенантов: {m.activeTenantsCount} • Инстансов: {m.instancesCount}</span>
<p className="text-[11px] text-krona-secondary line-clamp-1">{m.description}</p>
<div className="flex justify-between items-center text-[10px] text-krona-secondary/80 pt-1">
<span>Организаций: {m.activeTenantsCount} • Установок: {m.instancesCount}</span>
{hasRollout && (
<span className="px-1.5 py-0.5 rounded-xs bg-indigo-950 text-indigo-300 border border-indigo-700 font-bold uppercase">
Rollout
<span className="px-1.5 py-0.5 rounded-md bg-amber-500/15 text-amber-600 dark:text-amber-300 border border-amber-500/30 font-bold uppercase text-[9px]">
Раскатка
</span>
)}
</div>
@ -132,97 +140,115 @@ export const ModulesPage: React.FC = () => {
</div>
{/* Right 2 Columns: Module Details & Rollout Controller */}
<div className="xl:col-span-2 space-y-6">
<div className="lg:col-span-2 space-y-4 md:space-y-6">
{selectedModule && (
<>
{/* Module Header Card */}
<div className="rounded-xl border border-zinc-800 bg-[#0a1c15] p-5 space-y-4">
<div className="flex justify-between items-start">
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 md:p-5 space-y-4 shadow-xs">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="font-mono text-xs text-amber-400 font-bold">{selectedModule.code}</span>
<span className="text-zinc-600">•</span>
<span className="text-xs text-zinc-400">Категория: {selectedModule.category}</span>
<div className="flex flex-wrap items-center gap-2 mb-1">
<span className="font-mono text-xs text-amber-500 font-bold">{selectedModule.code}</span>
<span className="text-krona-secondary">•</span>
<span className="text-xs text-krona-secondary">
Категория: {RU_MODULE_CATEGORY[selectedModule.category] || selectedModule.category}
</span>
</div>
<h2 className="text-lg font-bold text-zinc-100">{selectedModule.name}</h2>
<p className="text-xs text-zinc-300 mt-1">{selectedModule.description}</p>
<h2 className="text-base md:text-lg font-bold text-krona-primary">{selectedModule.name}</h2>
<p className="text-xs text-krona-secondary mt-1">{selectedModule.description}</p>
</div>
<div className="text-right">
<span className="text-xs text-zinc-400">Текущая стабильная версия:</span>
<div className="font-mono font-bold text-lg text-emerald-400">v{selectedModule.latestVersion}</div>
<div className="text-left sm:text-right shrink-0">
<span className="text-xs text-krona-secondary">Текущая версия:</span>
<div className="font-mono font-bold text-lg text-emerald-600 dark:text-emerald-400">
v{selectedModule.latestVersion}
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-3 text-xs pt-3 border-t border-zinc-800">
<div className="p-2.5 rounded-lg bg-zinc-950/70 border border-zinc-800">
<div className="text-zinc-400 mb-0.5">Канал релиза:</div>
<div className="font-semibold text-zinc-200 uppercase">{selectedModule.rolloutChannel}</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 text-xs pt-3 border-t border-krona-border">
<div className="p-2.5 rounded-lg bg-krona-workspace border border-krona-border">
<div className="text-krona-secondary mb-0.5">Канал релиза:</div>
<div className="font-semibold text-krona-primary uppercase">{selectedModule.rolloutChannel}</div>
</div>
<div className="p-2.5 rounded-lg bg-zinc-950/70 border border-zinc-800">
<div className="text-zinc-400 mb-0.5">Манифест:</div>
<div className="font-semibold text-zinc-200 font-mono">v{selectedModule.manifestVersion}</div>
<div className="p-2.5 rounded-lg bg-krona-workspace border border-krona-border">
<div className="text-krona-secondary mb-0.5">Манифест:</div>
<div className="font-semibold text-krona-primary font-mono">v{selectedModule.manifestVersion}</div>
</div>
<div className="p-2.5 rounded-lg bg-zinc-950/70 border border-zinc-800">
<div className="text-zinc-400 mb-0.5">Зависимости:</div>
<div className="font-semibold text-zinc-200">
{selectedModule.dependencies.length > 0 ? selectedModule.dependencies.join(', ') : 'Нет (Базовый)'}
<div className="p-2.5 rounded-lg bg-krona-workspace border border-krona-border">
<div className="text-krona-secondary mb-0.5">Зависимости:</div>
<div className="font-semibold text-krona-primary truncate">
{selectedModule.dependencies.length > 0 ? selectedModule.dependencies.join(', ') : 'Базовый'}
</div>
</div>
</div>
</div>
{/* Rollout Control Panel (Сценарий 8) */}
<div className="rounded-xl border border-zinc-800 bg-[#091a13] p-5 space-y-4">
<div className="flex justify-between items-center">
{/* Rollout Control Panel */}
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 md:p-5 space-y-4 shadow-xs">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div className="flex items-center gap-2">
<Sliders className="w-4 h-4 text-amber-400" />
<h3 className="text-sm font-bold text-zinc-200">Управление раскаткой (Rollout Pipeline)</h3>
<Sliders className="w-4 h-4 text-amber-500" />
<h3 className="text-sm font-bold text-krona-primary">
Управление конвейером раскатки (Rollout)
</h3>
</div>
{activeRollout && <StatusBadge status={activeRollout.stage} size="sm" />}
{activeRollout && (
<StatusBadge
status={activeRollout.stage}
size="sm"
customLabel={RU_ROLLOUT_STAGE[activeRollout.stage] || activeRollout.stage}
/>
)}
</div>
{activeRollout ? (
<div className="space-y-4 text-xs">
{/* Progress visualizer */}
<div className="p-4 rounded-xl bg-zinc-950 border border-zinc-800 space-y-3">
<div className="p-4 rounded-xl bg-krona-workspace border border-krona-border space-y-3">
<div className="flex justify-between items-center">
<span className="font-semibold text-zinc-200">
Целевая версия: <span className="font-mono text-amber-300">v{activeRollout.targetVersion}</span>
<span className="font-semibold text-krona-primary">
Целевая версия:{' '}
<span className="font-mono text-amber-500 font-bold">v{activeRollout.targetVersion}</span>
</span>
<span className="font-bold text-amber-400 font-mono text-sm">{activeRollout.percentage}%</span>
<span className="font-bold text-amber-500 font-mono text-sm">{activeRollout.percentage}%</span>
</div>
<div className="w-full bg-zinc-800 h-2 rounded-full overflow-hidden">
<div className="w-full bg-krona-border h-2 rounded-full overflow-hidden">
<div
className="h-full bg-amber-400 transition-all duration-300"
className="h-full bg-amber-500 transition-all duration-300 rounded-full"
style={{ width: `${activeRollout.percentage}%` }}
/>
</div>
<div className="flex justify-between text-[11px] text-zinc-400">
<span>Стадия: <strong className="text-zinc-200 uppercase">{activeRollout.stage}</strong></span>
<div className="flex flex-wrap justify-between gap-2 text-[11px] text-krona-secondary">
<span>
Стадия:{' '}
<strong className="text-krona-primary">
{RU_ROLLOUT_STAGE[activeRollout.stage] || activeRollout.stage}
</strong>
</span>
<span>Пилотных организаций: {activeRollout.pilotTenantIds.length}</span>
<span>Старт: {new Date(activeRollout.startedAt).toLocaleDateString('ru-RU')}</span>
</div>
</div>
{/* Rollout Action Buttons */}
{/* Rollout Action Buttons with min 44px touch targets */}
<div className="flex flex-wrap gap-2 pt-2">
<button
type="button"
disabled={!permissions.canTriggerRollouts || activeRollout.stage === 'pilot'}
onClick={() => handleStageChange('pilot', 15)}
className="px-3 py-1.5 rounded-lg bg-zinc-900 border border-zinc-700 text-zinc-300 hover:bg-zinc-800 text-xs font-medium disabled:opacity-40 transition-colors"
className="min-h-[44px] px-3.5 py-2 rounded-lg bg-krona-workspace border border-krona-border text-krona-primary hover:bg-krona-surface text-xs font-medium disabled:opacity-40 transition-colors shadow-xs"
>
Фаза 1: Pilot (15%)
Фаза 1: Пилот (15%)
</button>
<button
type="button"
disabled={!permissions.canTriggerRollouts || activeRollout.stage === 'percentage'}
onClick={() => handleStageChange('percentage', 50)}
className="px-3 py-1.5 rounded-lg bg-zinc-900 border border-zinc-700 text-zinc-300 hover:bg-zinc-800 text-xs font-medium disabled:opacity-40 transition-colors"
className="min-h-[44px] px-3.5 py-2 rounded-lg bg-krona-workspace border border-krona-border text-krona-primary hover:bg-krona-surface text-xs font-medium disabled:opacity-40 transition-colors shadow-xs"
>
Фаза 2: 50% организаций
</button>
@ -231,7 +257,7 @@ export const ModulesPage: React.FC = () => {
type="button"
disabled={!permissions.canTriggerRollouts || activeRollout.stage === 'general_availability'}
onClick={() => handleStageChange('general_availability', 100)}
className="px-3 py-1.5 rounded-lg bg-emerald-950 text-emerald-300 border border-emerald-700 hover:bg-emerald-900 text-xs font-bold disabled:opacity-40 transition-colors"
className="min-h-[44px] px-4 py-2 rounded-lg bg-emerald-500/15 text-emerald-600 dark:text-emerald-300 border border-emerald-500/40 hover:bg-emerald-500/25 text-xs font-bold disabled:opacity-40 transition-colors shadow-xs"
>
Фаза 3: Полный релиз (100% GA)
</button>
@ -241,19 +267,19 @@ export const ModulesPage: React.FC = () => {
type="button"
disabled={!permissions.canPauseRollbackRollouts}
onClick={() => handleStageChange('paused', activeRollout.percentage)}
className="px-3 py-1.5 rounded-lg bg-amber-950/70 text-amber-300 border border-amber-700 hover:bg-amber-900/60 text-xs font-bold disabled:opacity-40 transition-colors flex items-center gap-1.5"
className="min-h-[44px] px-3.5 py-2 rounded-lg bg-amber-500/15 text-amber-600 dark:text-amber-300 border border-amber-500/40 hover:bg-amber-500/25 text-xs font-bold disabled:opacity-40 transition-colors flex items-center gap-1.5 shadow-xs"
>
<Pause className="w-3.5 h-3.5" />
<span>Поставить на паузу</span>
<Pause className="w-4 h-4" />
<span>На паузу</span>
</button>
) : (
<button
type="button"
disabled={!permissions.canTriggerRollouts}
onClick={() => handleStageChange('pilot', activeRollout.percentage)}
className="px-3 py-1.5 rounded-lg bg-amber-500 text-zinc-950 font-bold text-xs hover:bg-amber-400 transition-colors flex items-center gap-1.5"
className="min-h-[44px] px-3.5 py-2 rounded-lg bg-amber-500 text-zinc-950 font-bold text-xs hover:bg-amber-400 transition-colors flex items-center gap-1.5 shadow-xs"
>
<Play className="w-3.5 h-3.5" />
<Play className="w-4 h-4" />
<span>Снять с паузы</span>
</button>
)}
@ -262,47 +288,50 @@ export const ModulesPage: React.FC = () => {
type="button"
disabled={!permissions.canPauseRollbackRollouts || activeRollout.stage === 'rolled_back'}
onClick={() => handleStageChange('rolled_back', 0)}
className="px-3 py-1.5 rounded-lg bg-rose-950 text-rose-300 border border-rose-700 hover:bg-rose-900 text-xs font-bold disabled:opacity-40 transition-colors flex items-center gap-1.5"
className="min-h-[44px] px-3.5 py-2 rounded-lg bg-rose-500/15 text-rose-600 dark:text-rose-300 border border-rose-500/40 hover:bg-rose-500/25 text-xs font-bold disabled:opacity-40 transition-colors flex items-center gap-1.5 shadow-xs"
>
<RotateCcw className="w-3.5 h-3.5" />
<span>Rollback (Откат)</span>
<RotateCcw className="w-4 h-4" />
<span>Откат (Rollback)</span>
</button>
</div>
</div>
) : (
<div className="p-4 rounded-xl bg-zinc-950 border border-zinc-800 text-xs space-y-3">
<p className="text-zinc-400">
Для модуля <strong className="text-zinc-200">{selectedModule.name}</strong> нет активных раскаток.
Вы можете запустить пилотный релиз новой версии.
<div className="p-4 rounded-xl bg-krona-workspace border border-krona-border text-xs space-y-3">
<p className="text-krona-secondary">
Для модуля <strong className="text-krona-primary">{selectedModule.name}</strong> нет активных раскаток.
Вы можете запустить пилотный релиз новой версии для проверки совместимости.
</p>
<button
type="button"
disabled={!permissions.canTriggerRollouts}
onClick={handleCreateRollout}
className="px-4 py-2 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs transition-colors flex items-center gap-1.5 disabled:opacity-40"
className="min-h-[44px] px-4 py-2.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs transition-colors flex items-center gap-2 disabled:opacity-40 shadow-xs"
>
<Play className="w-3.5 h-3.5" />
<span>Запустить пилотный Rollout</span>
<Play className="w-4 h-4" />
<span>Запустить пилотную раскатку</span>
</button>
</div>
)}
</div>
{/* Release History & Deployments */}
<div className="rounded-xl border border-zinc-800 bg-[#0a1c15] p-5 space-y-3 text-xs">
<h3 className="text-sm font-bold text-zinc-200">История релизов и совместимость (Compatibility)</h3>
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 md:p-5 space-y-3 text-xs shadow-xs">
<h3 className="text-sm font-bold text-krona-primary flex items-center gap-1.5">
<Sparkles className="w-4 h-4 text-amber-500" />
<span>История релизов и совместимость</span>
</h3>
<div className="space-y-2">
{selectedModule.releaseHistory.map((rel) => (
<div key={rel.version} className="p-3 rounded-lg bg-zinc-950 border border-zinc-800 space-y-1">
<div key={rel.version} className="p-3 rounded-lg bg-krona-workspace border border-krona-border space-y-1">
<div className="flex justify-between items-center">
<span className="font-bold text-amber-300 font-mono">v{rel.version}</span>
<span className="text-[11px] text-zinc-500">
<span className="font-bold text-amber-500 font-mono">v{rel.version}</span>
<span className="text-[11px] text-krona-secondary">
{new Date(rel.releasedAt).toLocaleDateString('ru-RU')}
</span>
</div>
<p className="text-zinc-300 text-[11px]">{rel.changelog}</p>
<div className="text-[10px] text-zinc-400 font-mono pt-1">
Совместимость с Core: {rel.compatibleCore}
<p className="text-krona-primary text-[11px]">{rel.changelog}</p>
<div className="text-[10px] text-krona-secondary font-mono pt-1">
Совместимость с ядром: {rel.compatibleCore}
</div>
</div>
))}
@ -315,3 +344,4 @@ export const ModulesPage: React.FC = () => {
</div>
);
};