fix(ui): повысить приоритет правил и исправить мобильные строки

This commit is contained in:
Denozordec committed 2026-10-03 21:49:05 +07:00
1 parent c7e1f75e2e
commit d5edfe5ef6
5 files changed
+137 -113

No files matched your search

+102 -111
View File
@@ -722,7 +722,7 @@ function AlertRuleRow({ rule, onToggle, onDelete, onEdit, interactionsDisabled,i
return (
<div className={cn(
"flex items-center gap-3 px-4 py-3 hover:bg-muted/20 transition-colors group",
"flex flex-wrap items-center gap-3 px-4 py-3 hover:bg-muted/20 transition-colors group",
!rule.enabled && "opacity-55",
)}>
{/* toggle */}
@@ -737,10 +737,10 @@ function AlertRuleRow({ rule, onToggle, onDelete, onEdit, interactionsDisabled,i
</div>
{/* name + target */}
<div className="flex-1 min-w-0">
<div className="order-first min-w-0 basis-full sm:order-none sm:basis-auto sm:flex-1">
{isLive&&rule.type==="bgp-prefix"?<p role="status" className="text-xs text-warning">Отслеживание появления и отзыва префиксов пока недоступно. Это правило не проверяется.</p>:null}
<p className="text-sm font-medium truncate">{rule.name}</p>
<p className="text-[11px] text-muted-foreground truncate">
<p className="text-sm font-medium break-words sm:truncate">{rule.name}</p>
<p className="text-[11px] text-muted-foreground break-words sm:truncate">
{TYPE_META[rule.type].label} ·{" "}
<span className="font-mono">
{rule.targets.length > 1 ? `${rule.targets.length} объектов` : rule.target}
@@ -774,7 +774,7 @@ function AlertRuleRow({ rule, onToggle, onDelete, onEdit, interactionsDisabled,i
{/* last fired */}
<span className={cn(
"text-[11px] shrink-0 w-[80px] text-right",
"text-[11px] shrink-0 sm:w-[80px] text-right",
rule.lastFired ? "text-muted-foreground" : "text-muted-foreground/40",
)}>
{rule.lastFired ?? "—"}
@@ -786,7 +786,7 @@ function AlertRuleRow({ rule, onToggle, onDelete, onEdit, interactionsDisabled,i
</div>
{/* actions (visible on hover) */}
<div className="flex gap-1 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 focus-within:opacity-100 transition-opacity shrink-0">
<div className="ml-auto flex gap-1 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 focus-within:opacity-100 transition-opacity shrink-0">
{onEdit && (
<button
type="button"
@@ -2530,42 +2530,7 @@ function AlertsContent() {
{config.outcome?<p role="status" className="text-xs">{config.outcome}</p>:null}
{config.busy?<p role="status" className="text-xs">Ожидаем подтверждение правил и групп…</p>:null}
{config.unknown&&!config.busy?<div className="space-y-2"><p role="status" className="text-xs">Результат сохранения правил неизвестен. Запрос мог выполниться; проверьте правила и группы в БД перед повтором. Обновление списка не разрешает повтор автоматически.</p><Button type="button" size="sm" variant="outline" className="h-auto whitespace-normal" disabled={!config.enabled||operationBusy} onClick={config.review}>Я проверил правила — разрешить новый запрос</Button></div>:null}
{isLive?<AlertsTargetsStatus catalogs={catalogs}/>:null}
{isLive?<AlertsOutboxPanel base={backendUrl} available={backendStatus===true} operationsBusy={operationBusy} beginOperation={beginOperation} endOperation={endOperation}/>:null}
{known?<>
<Collapsible open={hintsExpanded} onOpenChange={setHintsExpanded} className="rounded-lg border border-dashed border-border bg-muted/15">
<CollapsibleTrigger className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs font-medium text-foreground hover:bg-muted/40 rounded-lg">
<ChevronDownIcon className={cn("size-4 shrink-0 transition-transform", hintsExpanded && "rotate-180")} />
Подсказки: формат целей и «восстановился»
</CollapsibleTrigger>
<CollapsibleContent className="px-3 pb-3 text-[11px] text-muted-foreground leading-relaxed space-y-2 border-t border-border/60 pt-2">
<p>
Движок читает последние сэмплы из PostgreSQL (uptime ресурсы, REST-пинг, пробы, GRE/BGP). Условие
срабатывает только если цепочка состояний совпадает с текстом правила.
</p>
<ul className="list-disc pl-4 space-y-1.5">
<li>
<span className="font-medium text-foreground">RTT / потери</span> — в объектах укажите ключ пробы
как в мониторинге: <code className="rounded bg-muted px-1 font-mono">имя пробы → target</code>.
</li>
<li>
<span className="font-medium text-foreground">Восстановился</span> — нужна история: после падения
в последних сэмплах виден возврат в online (REST и метрики ресурса объединяются). Один текущий
online без прошлого offline часто не даёт hit.
</li>
<li>
<span className="font-medium text-foreground">Перешёл в offline</span> — только на ребре (предыдущий
сэмпл был online). Пока сервер остаётся offline, повторные уведомления не шлются.
</li>
<li>
<span className="font-medium text-foreground">Стабильность / cooldown</span> — задержка перед
Telegram и пауза между повторными отправками. В разделе «Сбор данных» в деталях прогона «Оповещения»
есть таблица по каждому правилу (hit, стабильность, cooldown, Telegram, причина блока).
</li>
</ul>
</CollapsibleContent>
</Collapsible>
{/* ── summary chip bar ── */}
<div className="flex items-center gap-2 flex-wrap">
@@ -2576,7 +2541,7 @@ function AlertsContent() {
{ key: "warning", label: `Предупреждений: ${warning}`, cls: SEVERITY_META.warning.chip },
{ key: "info", label: `Информационных: ${info}`, cls: SEVERITY_META.info.chip },
] as { key: SeverityFilter; label: string; cls: string }[]).map(({ key, label, cls }) => (
<button key={key} onClick={() => handleChip(key)}
<button type="button" aria-pressed={chipActive(key)} key={key} onClick={() => handleChip(key)}
className={cn(
"text-xs px-3 py-1.5 rounded-full border font-medium transition-colors",
chipActive(key)
@@ -2590,7 +2555,7 @@ function AlertsContent() {
<div className="w-px h-4 bg-border mx-1" />
{/* active count chip */}
<button onClick={() => setOnlyActive(v => !v)}
<button type="button" aria-pressed={onlyActive} onClick={() => setOnlyActive(v => !v)}
className={cn(
"text-xs px-3 py-1.5 rounded-full border font-medium transition-colors",
onlyActive
@@ -2612,6 +2577,66 @@ function AlertsContent() {
</span>
</div>
{/* ── main layout ── */}
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_300px] gap-5 items-start">
{/* ── rules card ── */}
<OpsPanel
title={
<span className="flex items-center gap-2">
Правила оповещения
{rulesSaveBusy && <LoaderCircleIcon className="size-3.5 animate-spin text-muted-foreground" aria-hidden />}
</span>
}
contentClassName="px-0 pb-0 pt-2"
>
<div className="px-4 pb-3"><Input aria-label="Поиск правил" placeholder="Поиск по имени или объекту…" value={search} onChange={e => setSearch(e.target.value)} autoComplete="off" name="alert-rules-search" /></div>
{filteredRules.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground/40">
<BellOffIcon className="size-8 mb-2 opacity-40" />
<p className="text-sm">Правила не найдены</p>
</div>
) : (
<div className="divide-y divide-border/60">
{filteredRules.map(r => (
<AlertRuleRow
key={r.id}
rule={r}
onToggle={handleToggle}
onDelete={id=>setPendingDelete({kind:"rule",id})}
onEdit={openRuleSheetEdit}
interactionsDisabled={writesDisabled}
isLive={isLive}
/>
))}
</div>
)}
{/* footer */}
<div className="px-4 py-2.5 border-t flex flex-wrap items-center justify-between gap-2 text-[11px] text-muted-foreground">
<span>
{filteredRules.length} из {total} правил
{onlyActive && " · только активные"}
{sevFilter !== "all" && ` · ${SEVERITY_META[sevFilter].label.toLowerCase()}`}
</span>
<button
type="button"
disabled={writesDisabled}
onClick={openRuleSheetAdd}
className="flex items-center gap-1 hover:text-foreground transition-colors">
<PlusIcon className="size-3" />Добавить правило
</button>
</div>
</OpsPanel>
{/* ── right sidebar ── */}
<div className="flex flex-col gap-5">
<AlertsTelegramForm initial={tg} tokenConfigured={tokenConfigured} base={backendUrl} live={isLive} available={backendStatus===true&&!operationBusy}
onBegin={beginOperation} onEnd={endOperation}
onSaved={(value,configured)=>{setTg(value);setTokenConfigured(configured)}}/>
<HistoryCard entries={history} />
</div>
</div>
{/* ── presets + groups (свёрнуто по умолчанию) ── */}
<Collapsible open={extrasExpanded} onOpenChange={setExtrasExpanded} className="rounded-lg border border-border bg-card shadow-sm">
<CollapsibleTrigger
@@ -2706,76 +2731,42 @@ function AlertsContent() {
</CollapsibleContent>
</Collapsible>
{/* ── main layout ── */}
<div className="grid grid-cols-1 xl:grid-cols-[1fr_300px] gap-5 items-start">
<Collapsible open={hintsExpanded} onOpenChange={setHintsExpanded} className="rounded-lg border border-dashed border-border bg-muted/15">
<CollapsibleTrigger className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs font-medium text-foreground hover:bg-muted/40 rounded-lg">
<ChevronDownIcon className={cn("size-4 shrink-0 transition-transform", hintsExpanded && "rotate-180")} />
Подсказки: формат целей и «восстановился»
</CollapsibleTrigger>
<CollapsibleContent className="px-3 pb-3 text-[11px] text-muted-foreground leading-relaxed space-y-2 border-t border-border/60 pt-2">
<p>
Движок читает последние сэмплы из PostgreSQL (uptime ресурсы, REST-пинг, пробы, GRE/BGP). Условие
срабатывает только если цепочка состояний совпадает с текстом правила.
</p>
<ul className="list-disc pl-4 space-y-1.5">
<li>
<span className="font-medium text-foreground">RTT / потери</span> — в объектах укажите ключ пробы
как в мониторинге: <code className="rounded bg-muted px-1 font-mono">имя пробы → target</code>.
</li>
<li>
<span className="font-medium text-foreground">Восстановился</span> — нужна история: после падения
в последних сэмплах виден возврат в online (REST и метрики ресурса объединяются). Один текущий
online без прошлого offline часто не даёт hit.
</li>
<li>
<span className="font-medium text-foreground">Перешёл в offline</span> — только на ребре (предыдущий
сэмпл был online). Пока сервер остаётся offline, повторные уведомления не шлются.
</li>
<li>
<span className="font-medium text-foreground">Стабильность / cooldown</span> — задержка перед
Telegram и пауза между повторными отправками. В разделе «Сбор данных» в деталях прогона «Оповещения»
есть таблица по каждому правилу (hit, стабильность, cooldown, Telegram, причина блока).
</li>
</ul>
</CollapsibleContent>
</Collapsible>
{/* ── rules card ── */}
<OpsPanel
title={
<span className="flex items-center gap-2">
Правила оповещения
{rulesSaveBusy && <LoaderCircleIcon className="size-3.5 animate-spin text-muted-foreground" aria-hidden />}
</span>
}
headerRight={
<Input
placeholder="Поиск…"
value={search}
onChange={e => setSearch(e.target.value)}
autoComplete="off"
name="alert-rules-search"
className="h-7 text-xs max-w-[200px]"
/>
}
contentClassName="px-0 pb-0 pt-2"
>
{filteredRules.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground/40">
<BellOffIcon className="size-8 mb-2 opacity-40" />
<p className="text-sm">Правила не найдены</p>
</div>
) : (
<div className="divide-y divide-border/60">
{filteredRules.map(r => (
<AlertRuleRow
key={r.id}
rule={r}
onToggle={handleToggle}
onDelete={id=>setPendingDelete({kind:"rule",id})}
onEdit={openRuleSheetEdit}
interactionsDisabled={writesDisabled}
isLive={isLive}
/>
))}
</div>
)}
{/* footer */}
<div className="px-4 py-2.5 border-t flex items-center justify-between text-[11px] text-muted-foreground">
<span>
{filteredRules.length} из {total} правил
{onlyActive && " · только активные"}
{sevFilter !== "all" && ` · ${SEVERITY_META[sevFilter].label.toLowerCase()}`}
</span>
<button
type="button"
disabled={writesDisabled}
onClick={openRuleSheetAdd}
className="flex items-center gap-1 hover:text-foreground transition-colors">
<PlusIcon className="size-3" />Добавить правило
</button>
</div>
</OpsPanel>
{/* ── right sidebar ── */}
<div className="flex flex-col gap-5">
<AlertsTelegramForm initial={tg} tokenConfigured={tokenConfigured} base={backendUrl} live={isLive} available={backendStatus===true&&!operationBusy}
onBegin={beginOperation} onEnd={endOperation}
onSaved={(value,configured)=>{setTg(value);setTokenConfigured(configured)}}/>
<HistoryCard entries={history} />
</div>
</div>
</>:null}
{isLive?<AlertsTargetsStatus catalogs={catalogs}/>:null}
{isLive?<AlertsOutboxPanel base={backendUrl} available={backendStatus===true} operationsBusy={operationBusy} beginOperation={beginOperation} endOperation={endOperation}/>:null}
</div>
</div>