fix(dashboard): render the snooze button, and cap the things-to-handle list (v0.23.15-beta)
Three faults in the same card, all layout. **Snooze rendered as an unreadable sliver.** The row is a three-column grid — severity bar, text, action — but carried four children once the snooze button was added in v0.23.10. The fourth wrapped onto an implicit second row and landed in column one, which is 3px wide, so the button was squeezed to nothing. The two buttons now share the third column in a flex wrapper, which is what the grid always assumed. Only the rendering was broken: the dismiss round-trip was verified end to end against the running instance — POST returns 201, the item leaves the list, the snoozed count rises, and DELETE restores it. **A long backlog pushed the rest of the dashboard off the page.** The list grew without bound, so a fleet with real problems displaced everything below it. It now caps at min(52vh, 460px) and scrolls, keeping the card a fixed part of the page. The snoozed-count footer stays outside the scroll area so it remains visible. **The card rendered at 57% width with dead space beside it.** It was the only child of a two-column grid left behind when quick actions moved to the header (#104) — the comment above it already claimed full width, which it had not been since. The empty column and its wrapper are gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7a548abfd8
commit
f011a40748
@@ -6,7 +6,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-0.23.14_Beta-blue" alt="Version" />
|
||||
<img src="https://img.shields.io/badge/version-0.23.15_Beta-blue" alt="Version" />
|
||||
<img src="https://img.shields.io/badge/license-AGPLv3-blue" alt="License" />
|
||||
<img src="https://img.shields.io/badge/docker-compose-2496ED?logo=docker&logoColor=white" alt="Docker" />
|
||||
<img src="https://img.shields.io/badge/TypeScript-5.3-3178C6?logo=typescript&logoColor=white" alt="TypeScript" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mikrotik-manager-backend",
|
||||
"version": "0.23.14-beta",
|
||||
"version": "0.23.15-beta",
|
||||
"description": "Mikrotik Network Management Platform - Backend API",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mikrotik-manager-frontend",
|
||||
"version": "0.23.14-beta",
|
||||
"version": "0.23.15-beta",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -804,10 +804,10 @@ function OperationsView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Things to handle — full width now that quick actions live in the header */}
|
||||
<div className="grid gap-4" style={{ gridTemplateColumns: '1.3fr 1fr' }}>
|
||||
{/* Things to handle */}
|
||||
<div className="card" style={{ overflow: 'hidden' }}>
|
||||
{/* Things to handle — genuinely full width. It previously sat as the only
|
||||
child of a two-column grid left behind when quick actions moved to the
|
||||
header (#104), so it rendered at 57% with an empty column beside it. */}
|
||||
<div className="card" style={{ overflow: 'hidden' }}>
|
||||
<div
|
||||
className="flex items-center gap-[10px] px-5 py-[14px]"
|
||||
style={{ borderBottom: '1px solid var(--line)' }}
|
||||
@@ -822,6 +822,10 @@ function OperationsView({
|
||||
{thingsToHandle.length} item{thingsToHandle.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
{/* A fleet with a real backlog can generate dozens of these. Capping the
|
||||
list and scrolling keeps the card a fixed part of the page instead of
|
||||
something that grows without bound and displaces everything below. */}
|
||||
<div style={{ maxHeight: 'min(52vh, 460px)', overflowY: 'auto' }}>
|
||||
{thingsToHandle.length > 0 ? thingsToHandle.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
@@ -836,33 +840,39 @@ function OperationsView({
|
||||
<div className="text-[13.5px] font-medium mb-[3px]" style={{ color: 'var(--ink)' }}>{item.title}</div>
|
||||
<div className="text-[12.5px] leading-relaxed" style={{ color: 'var(--ink-2)' }}>{item.body}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate(item.path)}
|
||||
className="text-[11.5px] rounded-[5px] px-3 py-[6px] flex-shrink-0 transition-colors"
|
||||
style={{ background: 'transparent', border: '1px solid var(--line)', color: 'var(--ink-2)' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--surface-3)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||
>
|
||||
{item.action} →
|
||||
</button>
|
||||
{/* "Not now" rather than "never": an item that is genuinely resolved
|
||||
stops being generated, so a dismissal is time-boxed (#102). */}
|
||||
{item.fingerprint && (
|
||||
{/* Both buttons share the grid's third column. Placed as siblings
|
||||
they made four children in a three-column grid, which wrapped
|
||||
Snooze onto an implicit second row inside the 3px severity
|
||||
column — where it rendered as an unreadable sliver. */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => dismissInsight.mutate({
|
||||
fingerprint: item.fingerprint!, hours: 24,
|
||||
category: item.category, title: item.title,
|
||||
})}
|
||||
disabled={dismissInsight.isPending}
|
||||
title="Hide this for 24 hours. It returns if it is still true."
|
||||
className="text-[11.5px] rounded-[5px] px-2 py-[6px] flex-shrink-0 transition-colors"
|
||||
style={{ background: 'transparent', border: '1px solid var(--line)', color: 'var(--ink-3)' }}
|
||||
onClick={() => navigate(item.path)}
|
||||
className="text-[11.5px] rounded-[5px] px-3 py-[6px] whitespace-nowrap transition-colors"
|
||||
style={{ background: 'transparent', border: '1px solid var(--line)', color: 'var(--ink-2)' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--surface-3)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||
>
|
||||
Snooze
|
||||
{item.action} →
|
||||
</button>
|
||||
)}
|
||||
{/* "Not now" rather than "never": an item that is genuinely resolved
|
||||
stops being generated, so a dismissal is time-boxed (#102). */}
|
||||
{item.fingerprint && (
|
||||
<button
|
||||
onClick={() => dismissInsight.mutate({
|
||||
fingerprint: item.fingerprint!, hours: 24,
|
||||
category: item.category, title: item.title,
|
||||
})}
|
||||
disabled={dismissInsight.isPending}
|
||||
title="Hide this for 24 hours. It returns if it is still true."
|
||||
className="text-[11.5px] rounded-[5px] px-2 py-[6px] whitespace-nowrap transition-colors disabled:opacity-50"
|
||||
style={{ background: 'transparent', border: '1px solid var(--line)', color: 'var(--ink-3)' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--surface-3)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||
>
|
||||
Snooze
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="flex items-center justify-center gap-2 py-8" style={{ color: 'var(--ink-3)' }}>
|
||||
@@ -870,13 +880,12 @@ function OperationsView({
|
||||
<span className="text-[13px]">Nothing needs attention</span>
|
||||
</div>
|
||||
)}
|
||||
{(insights?.dismissedCount ?? 0) > 0 && (
|
||||
<div className="px-5 py-2 text-[11.5px]" style={{ color: 'var(--ink-3)', borderTop: '1px solid var(--line-soft)' }}>
|
||||
{insights!.dismissedCount} snoozed — they return automatically if still true.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{(insights?.dismissedCount ?? 0) > 0 && (
|
||||
<div className="px-5 py-2 text-[11.5px]" style={{ color: 'var(--ink-3)', borderTop: '1px solid var(--line-soft)' }}>
|
||||
{insights!.dismissedCount} snoozed — they return automatically if still true.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Capacity + Security rollup */}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const APP_VERSION = 'v0.23.14 Beta';
|
||||
export const APP_VERSION = 'v0.23.15 Beta';
|
||||
|
||||
Reference in New Issue
Block a user