feat(control-center): adapt AnnouncementsPage and IncidentsPage mobile-first styling
This commit is contained in:
parent
b81206a2bb
commit
b8739a5096
2 changed files with 231 additions and 137 deletions
|
|
@ -3,9 +3,13 @@ import {
|
|||
BellRing,
|
||||
Send,
|
||||
Plus,
|
||||
X,
|
||||
Users,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import { useKccStore, useOperatorRole } from '../store/kccStore';
|
||||
import { StatusBadge } from '../components/StatusBadge';
|
||||
import { RU_ANNOUNCEMENT_PRIORITY } from '../locales/ru';
|
||||
|
||||
export const AnnouncementsPage: React.FC = () => {
|
||||
const announcements = useKccStore((state) => state.announcements);
|
||||
|
|
@ -74,15 +78,15 @@ export const AnnouncementsPage: 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">
|
||||
<BellRing 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">
|
||||
<BellRing className="w-5 h-5 text-amber-500 shrink-0" />
|
||||
<span>Оповещения и системные сообщения</span>
|
||||
</h1>
|
||||
<p className="text-xs text-zinc-400 mt-0.5">
|
||||
<p className="text-xs text-krona-secondary mt-0.5">
|
||||
Таргетированные сервисные рассылки, уведомления об обновлениях модулей и плановом ТО
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -91,102 +95,105 @@ export const AnnouncementsPage: React.FC = () => {
|
|||
type="button"
|
||||
disabled={!permissions.canCreateAnnouncements}
|
||||
onClick={() => setIsCreating(!isCreating)}
|
||||
className="px-3 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs flex items-center gap-1.5 transition-colors 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 flex items-center justify-center gap-2 transition-colors disabled:opacity-40 shadow-xs shrink-0"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>{isCreating ? 'Закрыть редактор' : 'Создать объявление'}</span>
|
||||
{isCreating ? <X className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
|
||||
<span>{isCreating ? 'Закрыть редактор' : 'Создать оповещение'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* New Announcement Form (Сценарий 7) */}
|
||||
{/* New Announcement Form (Modal or Expandable) */}
|
||||
{isCreating && (
|
||||
<form
|
||||
onSubmit={handleSendAnnouncement}
|
||||
className="rounded-xl border border-amber-500/40 bg-[#0a1c15] p-5 space-y-4 shadow-xl animate-in fade-in slide-in-from-top-2"
|
||||
className="rounded-xl border border-amber-500/40 bg-krona-surface p-4 md:p-5 space-y-4 shadow-lg animate-in fade-in slide-in-from-top-2"
|
||||
>
|
||||
<div className="flex justify-between items-center border-b border-zinc-800 pb-3">
|
||||
<div className="flex items-center gap-2 text-sm font-bold text-zinc-100">
|
||||
<Send className="w-4 h-4 text-amber-400" />
|
||||
<div className="flex justify-between items-center border-b border-krona-border pb-3">
|
||||
<div className="flex items-center gap-2 text-sm font-bold text-krona-primary">
|
||||
<Send className="w-4 h-4 text-amber-500" />
|
||||
<span>Конструктор операторского оповещения</span>
|
||||
</div>
|
||||
<span className="text-xs text-amber-300/80 font-mono">DEMO: без внешней отправки SMS/Email</span>
|
||||
<span className="text-[11px] text-amber-600 dark:text-amber-300 font-mono">
|
||||
ДЕМО: In-App доставка
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
|
||||
{/* Title */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-semibold text-zinc-300">Заголовок оповещения:</label>
|
||||
<label className="font-semibold text-krona-primary">Заголовок оповещения:</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Например: Доступен модуль Omnichannel 3.0"
|
||||
className="w-full bg-zinc-950 border border-zinc-700 rounded-lg p-2.5 text-xs text-zinc-100 placeholder-zinc-500 focus:outline-hidden focus:border-amber-500"
|
||||
placeholder="Например: Доступно обновление платформы v3.0"
|
||||
className="w-full bg-krona-workspace border border-krona-border rounded-lg px-3 py-2.5 text-xs text-krona-primary placeholder:text-krona-secondary/60 focus:outline-hidden focus:border-amber-500 min-h-[44px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-semibold text-zinc-300">Приоритет доставки:</label>
|
||||
<label className="font-semibold text-krona-primary">Приоритет доставки:</label>
|
||||
<select
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(e.target.value as typeof priority)}
|
||||
className="w-full bg-zinc-950 border border-zinc-700 rounded-lg p-2.5 text-xs text-zinc-100 focus:outline-hidden"
|
||||
className="w-full bg-krona-workspace border border-krona-border rounded-lg px-3 py-2.5 text-xs text-krona-primary focus:outline-hidden min-h-[44px]"
|
||||
>
|
||||
<option value="normal">Обычный (Информационный баннер)</option>
|
||||
<option value="important">Важный (Уведомление в шапке)</option>
|
||||
<option value="critical">Критический (Блокирующий модальный диалог)</option>
|
||||
<option value="critical">Критический (Блокирующий диалог)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Audience selection */}
|
||||
<div className="space-y-2 text-xs">
|
||||
<label className="font-semibold text-zinc-300">Целевая аудитория тенантов:</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<label className="font-semibold text-krona-primary">Целевая аудитория организаций:</label>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAudienceType('all')}
|
||||
className={`p-2 rounded-lg border text-xs font-medium transition-colors ${
|
||||
className={`min-h-[44px] p-2.5 rounded-lg border text-xs font-medium transition-colors flex items-center justify-center gap-1.5 ${
|
||||
audienceType === 'all'
|
||||
? 'bg-amber-500/20 text-amber-300 border-amber-500/50 font-bold'
|
||||
: 'bg-zinc-950 text-zinc-400 border-zinc-800'
|
||||
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/50 font-bold'
|
||||
: 'bg-krona-workspace text-krona-secondary border-krona-border hover:text-krona-primary'
|
||||
}`}
|
||||
>
|
||||
Все организации ({organizations.length})
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
<span>Все ({organizations.length})</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAudienceType('module')}
|
||||
className={`p-2 rounded-lg border text-xs font-medium transition-colors ${
|
||||
className={`min-h-[44px] p-2.5 rounded-lg border text-xs font-medium transition-colors flex items-center justify-center gap-1.5 ${
|
||||
audienceType === 'module'
|
||||
? 'bg-amber-500/20 text-amber-300 border-amber-500/50 font-bold'
|
||||
: 'bg-zinc-950 text-zinc-400 border-zinc-800'
|
||||
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/50 font-bold'
|
||||
: 'bg-krona-workspace text-krona-secondary border-krona-border hover:text-krona-primary'
|
||||
}`}
|
||||
>
|
||||
По установленному модулю
|
||||
<span>По модулю</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAudienceType('specific')}
|
||||
className={`p-2 rounded-lg border text-xs font-medium transition-colors ${
|
||||
className={`min-h-[44px] p-2.5 rounded-lg border text-xs font-medium transition-colors flex items-center justify-center gap-1.5 ${
|
||||
audienceType === 'specific'
|
||||
? 'bg-amber-500/20 text-amber-300 border-amber-500/50 font-bold'
|
||||
: 'bg-zinc-950 text-zinc-400 border-zinc-800'
|
||||
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/50 font-bold'
|
||||
: 'bg-krona-workspace text-krona-secondary border-krona-border hover:text-krona-primary'
|
||||
}`}
|
||||
>
|
||||
Выборочные организации ({selectedTenants.length})
|
||||
<span>Выборочные ({selectedTenants.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{audienceType === 'module' && (
|
||||
<div className="pt-2 flex items-center gap-2">
|
||||
<span className="text-zinc-400 text-xs">Выберите модуль:</span>
|
||||
<div className="pt-2 flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<span className="text-krona-secondary text-xs">Выберите модуль:</span>
|
||||
<select
|
||||
value={selectedModuleId}
|
||||
onChange={(e) => setSelectedModuleId(e.target.value)}
|
||||
className="bg-zinc-900 border border-zinc-700 rounded-md px-2.5 py-1 text-xs text-zinc-200"
|
||||
className="bg-krona-workspace border border-krona-border rounded-lg px-3 py-2 text-xs text-krona-primary min-h-[44px]"
|
||||
>
|
||||
{modules.map((m) => (
|
||||
<option key={m.moduleId} value={m.moduleId}>
|
||||
|
|
@ -200,73 +207,94 @@ export const AnnouncementsPage: React.FC = () => {
|
|||
|
||||
{/* Body */}
|
||||
<div className="space-y-1.5 text-xs">
|
||||
<label className="font-semibold text-zinc-300">Текст сообщения:</label>
|
||||
<label className="font-semibold text-krona-primary">Текст сообщения:</label>
|
||||
<textarea
|
||||
required
|
||||
rows={3}
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
placeholder="Введите текст объявления для операторов и пользователей организаций..."
|
||||
className="w-full bg-zinc-950 border border-zinc-700 rounded-lg p-2.5 text-xs text-zinc-100 placeholder-zinc-500 focus:outline-hidden focus:border-amber-500 resize-none"
|
||||
className="w-full bg-krona-workspace border border-krona-border rounded-lg p-3 text-xs text-krona-primary placeholder:text-krona-secondary/60 focus:outline-hidden focus:border-amber-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-zinc-800">
|
||||
<div className="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-2 border-t border-krona-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreating(false)}
|
||||
className="px-3 py-1.5 rounded-lg border border-zinc-700 text-zinc-400 hover:text-zinc-200 text-xs font-medium"
|
||||
className="min-h-[44px] px-4 py-2.5 rounded-lg border border-krona-border text-krona-secondary hover:text-krona-primary text-xs font-medium transition-colors"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs flex items-center gap-1.5 shadow-md shadow-amber-900/30"
|
||||
className="min-h-[44px] px-5 py-2.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs flex items-center justify-center gap-2 shadow-xs"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
<span>Опубликовать (Reason + Audit)</span>
|
||||
<Send className="w-4 h-4" />
|
||||
<span>Опубликовать с подтверждением</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Announcements List */}
|
||||
{/* Announcements List - Mobile-First Cards */}
|
||||
<div className="space-y-3">
|
||||
{announcements.map((anc) => (
|
||||
<div key={anc.announcementId} className="rounded-xl border border-zinc-800 bg-[#091a13] p-5 space-y-3 shadow-lg">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-zinc-850 pb-3">
|
||||
{announcements.map((anc) => {
|
||||
const priorityLabel = RU_ANNOUNCEMENT_PRIORITY[anc.priority] || anc.priority;
|
||||
const isCritical = anc.priority === 'critical';
|
||||
const isImportant = anc.priority === 'important';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={anc.announcementId}
|
||||
className="rounded-xl border border-krona-border bg-krona-surface p-4 md:p-5 space-y-3 shadow-xs"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-krona-border pb-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-bold text-sm text-zinc-100">{anc.title}</span>
|
||||
<StatusBadge status={anc.status === 'completed' ? 'healthy' : 'warning'} size="sm" customLabel={anc.status.toUpperCase()} />
|
||||
<span className="text-[10px] uppercase font-bold text-amber-400 px-1.5 py-0.5 rounded-xs bg-amber-500/10 border border-amber-500/30">
|
||||
{anc.priority}
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||
<span className="font-bold text-sm text-krona-primary">{anc.title}</span>
|
||||
<StatusBadge
|
||||
status={anc.status === 'completed' ? 'healthy' : 'warning'}
|
||||
size="sm"
|
||||
customLabel={anc.status === 'completed' ? 'Доставлено' : 'Отправка'}
|
||||
/>
|
||||
<span
|
||||
className={`text-[10px] font-bold px-2 py-0.5 rounded-md border ${
|
||||
isCritical
|
||||
? 'bg-rose-500/15 text-rose-600 dark:text-rose-300 border-rose-500/40'
|
||||
: isImportant
|
||||
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/40'
|
||||
: 'bg-krona-workspace text-krona-secondary border-krona-border'
|
||||
}`}
|
||||
>
|
||||
{priorityLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-zinc-400">
|
||||
Автор: <span className="text-zinc-200">{anc.author}</span> • Опубликовано:{' '}
|
||||
{new Date(anc.createdAt).toLocaleString('ru-RU')}
|
||||
<div className="text-[11px] text-krona-secondary">
|
||||
Автор: <span className="text-krona-primary font-medium">{anc.author}</span> • Опубликовано:{' '}
|
||||
{new Date(anc.createdAt).toLocaleDateString('ru-RU')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delivery statistics */}
|
||||
<div className="flex items-center gap-3 text-xs shrink-0 font-mono">
|
||||
<div className="text-right">
|
||||
<div className="text-[10px] text-zinc-500">Доставлено</div>
|
||||
<div className="font-bold text-emerald-400">{anc.stats.delivered} / {anc.stats.totalTargeted}</div>
|
||||
<div className="text-left sm:text-right">
|
||||
<div className="text-[10px] text-krona-secondary">Доставлено</div>
|
||||
<div className="font-bold text-emerald-600 dark:text-emerald-400 flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3 h-3 inline" />
|
||||
<span>{anc.stats.delivered} / {anc.stats.totalTargeted}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[10px] text-zinc-500">Каналы</div>
|
||||
<div className="text-zinc-300 font-semibold">{anc.channels.join(', ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-zinc-300 leading-relaxed">{anc.body}</p>
|
||||
<p className="text-xs text-krona-secondary leading-relaxed">{anc.body}</p>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@ import {
|
|||
AlertTriangle,
|
||||
Search,
|
||||
ChevronRight,
|
||||
ShieldAlert,
|
||||
Server,
|
||||
Building2,
|
||||
Package,
|
||||
} from 'lucide-react';
|
||||
import { useKccStore } from '../store/kccStore';
|
||||
import { StatusBadge } from '../components/StatusBadge';
|
||||
import { Incident } from '../contracts';
|
||||
import { RU_INCIDENT_SEVERITY, RU_INCIDENT_STATUS } from '../locales/ru';
|
||||
|
||||
export const IncidentsPage: React.FC = () => {
|
||||
const incidents = useKccStore((state) => state.incidents);
|
||||
const openInspector = useKccStore((state) => state.openInspector);
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | Incident['status']>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'open' | 'mitigating' | 'resolved'>('all');
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filtered = incidents.filter((inc) => {
|
||||
|
|
@ -21,57 +25,81 @@ export const IncidentsPage: React.FC = () => {
|
|||
inc.incidentNumber.toLowerCase().includes(search.toLowerCase()) ||
|
||||
inc.summary.toLowerCase().includes(search.toLowerCase()) ||
|
||||
inc.description.toLowerCase().includes(search.toLowerCase());
|
||||
const matchStatus = statusFilter === 'all' || inc.status === statusFilter;
|
||||
|
||||
let matchStatus = true;
|
||||
if (statusFilter === 'open') {
|
||||
matchStatus = inc.status === 'investigating';
|
||||
} else if (statusFilter === 'mitigating') {
|
||||
matchStatus = inc.status === 'identified' || inc.status === 'monitoring';
|
||||
} else if (statusFilter === 'resolved') {
|
||||
matchStatus = inc.status === 'resolved';
|
||||
}
|
||||
|
||||
return matchSearch && matchStatus;
|
||||
});
|
||||
|
||||
const unresolvedCount = incidents.filter((i) => i.status !== 'resolved').length;
|
||||
const criticalCount = incidents.filter(
|
||||
(i) => i.status !== 'resolved' && (i.severity === 'P0_CRITICAL' || i.severity === 'P1_MAJOR')
|
||||
).length;
|
||||
|
||||
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">
|
||||
<AlertTriangle className="w-5 h-5 text-rose-400" />
|
||||
<span>Инциденты платформы (Incidents Desk)</span>
|
||||
<h1 className="text-lg md:text-xl font-bold text-krona-primary flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-rose-500 shrink-0" />
|
||||
<span>Журнал инцидентов платформы</span>
|
||||
</h1>
|
||||
<p className="text-xs text-zinc-400 mt-0.5">
|
||||
<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">{incidents.length}</span> (открыто:{' '}
|
||||
<span className="font-bold text-rose-400">{incidents.filter((i) => i.status !== 'resolved').length}</span>)
|
||||
<div className="flex flex-wrap 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">{incidents.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>{unresolvedCount}</strong>
|
||||
</span>
|
||||
{criticalCount > 0 && (
|
||||
<span className="px-2.5 py-1 rounded-md bg-rose-500/10 border border-rose-500/30 text-rose-500 font-bold">
|
||||
Критических: {criticalCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter and Search Bar */}
|
||||
<div className="flex flex-col md:flex-row gap-3 items-stretch md:items-center justify-between">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="w-4 h-4 text-zinc-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<Search className="w-4 h-4 text-krona-secondary absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Поиск по номеру, описанию или теме..."
|
||||
className="w-full bg-[#0b1d16] border border-zinc-800 rounded-lg pl-9 pr-3 py-2 text-xs text-zinc-100 placeholder-zinc-500 focus:outline-hidden focus:border-amber-500 transition-colors"
|
||||
className="w-full bg-krona-workspace border border-krona-border rounded-lg pl-9 pr-3 py-2.5 text-xs text-krona-primary placeholder:text-krona-secondary/60 focus:outline-hidden focus:border-amber-500 transition-colors min-h-[44px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 overflow-x-auto pb-1 text-xs">
|
||||
{/* Status Filter Tabs with min 44px touch targets */}
|
||||
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 text-xs -mx-1 px-1">
|
||||
{[
|
||||
{ id: 'all', label: 'Все инциденты' },
|
||||
{ id: 'open', label: 'Открытые' },
|
||||
{ id: 'mitigating', label: 'В процессе митигации' },
|
||||
{ id: 'resolved', label: 'Разрешенные' },
|
||||
{ id: 'mitigating', label: 'В процессе' },
|
||||
{ id: 'resolved', label: 'Устранённые' },
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setStatusFilter(tab.id as typeof statusFilter)}
|
||||
className={`px-3 py-1.5 rounded-lg font-medium transition-colors ${
|
||||
className={`min-h-[44px] px-3.5 py-2 rounded-lg font-medium transition-colors whitespace-nowrap ${
|
||||
statusFilter === tab.id
|
||||
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40 font-bold'
|
||||
: 'bg-zinc-900 text-zinc-400 border border-zinc-800 hover:text-zinc-200'
|
||||
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border border-amber-500/40 font-bold shadow-xs'
|
||||
: 'bg-krona-workspace text-krona-secondary border border-krona-border hover:text-krona-primary'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
|
|
@ -80,58 +108,96 @@ export const IncidentsPage: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Incidents List */}
|
||||
{/* Incidents List - Mobile-First Cards */}
|
||||
<div className="space-y-3">
|
||||
{filtered.map((inc) => (
|
||||
{filtered.map((inc) => {
|
||||
const isCritical = inc.severity === 'P0_CRITICAL';
|
||||
const isMajor = inc.severity === 'P1_MAJOR';
|
||||
const severityInfo = RU_INCIDENT_SEVERITY[inc.severity] || {
|
||||
label: inc.severity,
|
||||
short: inc.severity,
|
||||
};
|
||||
const statusText = RU_INCIDENT_STATUS[inc.status] || inc.status;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={inc.incidentId}
|
||||
onClick={() => openInspector('incident', inc.incidentId)}
|
||||
className="p-5 rounded-xl border border-zinc-800 bg-[#091a13] hover:border-zinc-700 cursor-pointer transition-colors space-y-3 shadow-lg group"
|
||||
className={`p-4 md:p-5 rounded-xl border bg-krona-surface transition-all cursor-pointer space-y-3 shadow-xs hover:border-amber-500/50 group ${
|
||||
isCritical
|
||||
? 'border-rose-500/40 bg-rose-500/5'
|
||||
: isMajor
|
||||
? 'border-amber-500/30'
|
||||
: 'border-krona-border'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-zinc-850 pb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="font-mono font-bold text-sm text-zinc-100 group-hover:text-amber-300 transition-colors">
|
||||
{/* Card Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-krona-border pb-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono font-bold text-sm text-krona-primary group-hover:text-amber-500 transition-colors">
|
||||
{inc.incidentNumber}
|
||||
</span>
|
||||
<StatusBadge status={inc.status} size="sm" />
|
||||
<span className="text-[10px] uppercase font-extrabold px-2 py-0.5 rounded-xs bg-rose-950/80 text-rose-300 border border-rose-800/60 font-mono">
|
||||
{inc.severity}
|
||||
<StatusBadge status={inc.status} size="sm" customLabel={statusText} />
|
||||
<span
|
||||
className={`text-[11px] font-bold px-2 py-0.5 rounded-md border font-mono ${
|
||||
isCritical
|
||||
? 'bg-rose-500/15 text-rose-600 dark:text-rose-300 border-rose-500/40'
|
||||
: isMajor
|
||||
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/40'
|
||||
: 'bg-sky-500/15 text-sky-600 dark:text-sky-300 border-sky-500/40'
|
||||
}`}
|
||||
>
|
||||
{severityInfo.short}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-zinc-400">
|
||||
<span>Ответственный: <strong className="text-zinc-200">{inc.ownerName}</strong></span>
|
||||
<div className="flex items-center gap-2 text-xs text-krona-secondary">
|
||||
<span>Ответственный: <strong className="text-krona-primary font-medium">{inc.ownerName}</strong></span>
|
||||
<span>•</span>
|
||||
<span>{new Date(inc.openedAt).toLocaleString('ru-RU')}</span>
|
||||
<span>{new Date(inc.openedAt).toLocaleDateString('ru-RU')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary and Description */}
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-bold text-sm text-zinc-100">{inc.summary}</h3>
|
||||
<p className="text-xs text-zinc-300 leading-relaxed">{inc.description}</p>
|
||||
<h3 className="font-bold text-sm text-krona-primary">{inc.summary}</h3>
|
||||
<p className="text-xs text-krona-secondary leading-relaxed line-clamp-2 md:line-clamp-none">
|
||||
{inc.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-zinc-850 flex items-center justify-between text-xs text-zinc-400">
|
||||
<div className="flex gap-4">
|
||||
<span>Затронуто тенантов: <strong className="text-zinc-200">{inc.affectedTenantIds.length}</strong></span>
|
||||
<span>Узлов: <strong className="text-zinc-200">{inc.affectedServerIds.length}</strong></span>
|
||||
<span>Модулей: <strong className="text-zinc-200">{inc.affectedModuleIds.length}</strong></span>
|
||||
{/* Card Footer: Impact Metrics & Inspector Trigger */}
|
||||
<div className="pt-2 border-t border-krona-border flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs text-krona-secondary">
|
||||
<div className="flex flex-wrap items-center gap-3 md:gap-5">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Building2 className="w-3.5 h-3.5 text-krona-secondary" />
|
||||
Организаций: <strong className="text-krona-primary font-medium">{inc.affectedTenantIds.length}</strong>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Server className="w-3.5 h-3.5 text-krona-secondary" />
|
||||
Узлов: <strong className="text-krona-primary font-medium">{inc.affectedServerIds.length}</strong>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Package className="w-3.5 h-3.5 text-krona-secondary" />
|
||||
Модулей: <strong className="text-krona-primary font-medium">{inc.affectedModuleIds.length}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 text-xs text-amber-400 hover:text-amber-300 font-medium"
|
||||
>
|
||||
<div className="inline-flex items-center gap-1 text-xs font-semibold text-amber-500 hover:text-amber-600 transition-colors min-h-[44px]">
|
||||
<span>Детали расследования</span>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<ChevronRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<div className="p-8 rounded-xl border border-zinc-800 bg-[#091a13] text-center text-xs text-zinc-500">
|
||||
Инциденты не найдены
|
||||
<div className="p-8 rounded-xl border border-krona-border bg-krona-surface text-center space-y-2">
|
||||
<ShieldAlert className="w-8 h-8 text-krona-secondary mx-auto opacity-50" />
|
||||
<p className="text-xs text-krona-secondary">
|
||||
Инциденты не найдены по выбранным критериям фильтрации
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue