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