From 3fed67a3f1ef17f5b3639c90d3f579a05b014201 Mon Sep 17 00:00:00 2001 From: Anso Date: Sat, 11 Jul 2026 20:55:37 -0400 Subject: [PATCH] fix: assorted UI polish across Home, Security, and Resources pages (#1590) * fix: polish stack health table, security card heights, and scanner-gate copy - Remove state dot column from Stack Health rows; state still signaled via row tinting and sparkline color - Reposition Update available badge adjacent to stack name and track the theme accent (--brand) instead of static fuchsia - Equalize Top exploit-risk findings and Severity x exploitability card heights via h-full in a stretching grid - Add resolution prop to CapabilityGate so scanner-gated tabs show "Install a scanner from the Scanner setup tab." instead of the misleading "Upgrade the node" message * fix: scope Resources scroll area to table contents only - Remove bg-background from the outer wrapper so search/filter bars no longer sit on a dark strip - Replace outer ScrollArea with a plain div so search and filter controls stay fixed while only table rows scroll - Add ScrollArea (max-h-[62vh]) inside each table card for Images, Volumes, and Networks tabs * fix: remove static bg-background from Security table scroll areas Images and History tab tables used bg-background on their ScrollArea, forcing a near-black fill that didn't adapt to the theme. Removing it lets the parent card's bg-card show through, matching the Resources table pattern that correctly respects DIM, OLED, and Light modes. * fix: collapsible search icon and transparent Combobox across Resources and Security - Replace always-visible search inputs on Resources tabs (Images, Volumes, Networks) and Security Images (desktop + mobile) with Fleet-style collapsible search: an icon button that expands to a full input on click, collapses on blur when empty - Add [&>button]:!bg-background to Security Images severity Combobox, matching Fleet's transparent dropdown design * fix: remove redundant orange badge from Unmanaged tab label The count was shown twice: once as inline text in the tab and again as an orange bg-warning pill badge. The inline text remains; the duplicate badge is removed. * fix: outline buttons invisible in Light mode - Changed outline variant border from border-input to border so it uses --card-border, which maintains a visible 0.035 lightness gap from --background in Light mode (was 0.005, essentially invisible) - Boosted Light-mode --button-inner-glow from a 4% white inset to a 10% inset plus a subtle drop shadow for the physical-key feel * fix: bump table cell horizontal padding from 8px to density-aware token Changed TableHead and TableCell px-2 (8px) to px-[var(--density-row-x)] which resolves to 20px comfortable / 16px compact. Matches the density system used by StackHealthTable and gives every shadcn-based table proper breathing room on both edges. * fix: expand collapsible search before typing in ImagesTab test The test was written for the old always-visible search input. Now that the search bar collapses to an icon, click it first to expand before interacting with the input. * fix: reorder Settings Infrastructure to Nodes, Stacks, Fleet Move the Stacks section directly below Nodes so the Infrastructure sidebar reads Nodes, Stacks, Fleet. Pure array-element relocation in the settings registry; the sidebar renders in array order. * fix: simplify pilot enroll command and add Step 2 copy button Step 2 of the pilot-agent enrollment modal now shows "docker compose up -d" instead of the redundant "-f compose.yaml" form, since Step 1 already has the operator save the file as the default compose.yaml. Add an inline copy button next to the command so it can be copied without selecting the text by hand. * fix: stack Fleet/Security tab heading actions on mobile The shared FleetTabHeading kept its title and action buttons on one row at every width, so on phones the Security Suppressions heading and its Export VEX / Add suppression buttons were crammed together. Stack the heading and actions vertically below the md breakpoint and restore the original row layout at md and up, so desktop is unchanged. * feat: add node selector to mobile Home masthead The mobile Home page showed the active node as static text, so switching nodes was only possible from the Stacks page. Render the same compact NodeSwitcher in the Home masthead kicker slot, giving a second place to view and switch the active node with the identical popover and Manage nodes action. --- frontend/src/components/CapabilityGate.tsx | 9 +- frontend/src/components/EditorLayout.tsx | 1 + frontend/src/components/ResourcesView.tsx | 125 +++++++++++++----- frontend/src/components/SecurityView.tsx | 8 +- .../components/dashboard/StackHealthTable.tsx | 18 +-- .../src/components/fleet/FleetEmptyState.tsx | 2 +- .../src/components/mobile/MobileDashboard.tsx | 8 +- .../src/components/nodes/useNodeActions.tsx | 20 ++- .../src/components/security/HistoryTab.tsx | 2 +- .../src/components/security/ImagesTab.tsx | 76 ++++++++--- .../src/components/security/OverviewTab.tsx | 7 +- .../components/security/SecurityCharts.tsx | 8 +- .../security/__tests__/ImagesTab.test.tsx | 1 + frontend/src/components/settings/registry.ts | 18 +-- frontend/src/components/ui/button.tsx | 2 +- frontend/src/components/ui/table.tsx | 4 +- frontend/src/index.css | 2 +- 17 files changed, 208 insertions(+), 103 deletions(-) diff --git a/frontend/src/components/CapabilityGate.tsx b/frontend/src/components/CapabilityGate.tsx index 34f82a47..4386bc4a 100644 --- a/frontend/src/components/CapabilityGate.tsx +++ b/frontend/src/components/CapabilityGate.tsx @@ -8,6 +8,10 @@ import { LockCard } from './ui/LockCard'; interface CapabilityGateProps { capability: Capability; featureName?: string; + /** Overrides the default "Upgrade the node to use this feature." body. Use + * when the capability is gated on something other than node version (for + * example, a scanner binary that must be installed separately). */ + resolution?: string; children: ReactNode; } @@ -23,7 +27,7 @@ interface CapabilityGateProps { * dependency and adds no chunk weight, so a node that lacks the * capability never downloads the gated module. */ -export function CapabilityGate({ capability, featureName = 'This feature', children }: CapabilityGateProps) { +export function CapabilityGate({ capability, featureName = 'This feature', resolution, children }: CapabilityGateProps) { const { hasCapability, activeNode, activeNodeMeta } = useNodes(); if (hasCapability(capability)) return <>{children}; @@ -32,12 +36,13 @@ export function CapabilityGate({ capability, featureName = 'This feature', child const versionHint = isValidVersion(activeNodeMeta?.version) ? `${nodeName} is running v${activeNodeMeta.version}.` : `${nodeName} does not advertise this capability.`; + const body = resolution ?? `${versionHint} Upgrade the node to use this feature.`; return ( ); } diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 1c53aeb5..ea953611 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -949,6 +949,7 @@ export default function EditorLayout() { headerActions={mobileMastheadActions} onNavigateToStack={handleSelectStack} onViewAllStacks={goToMobileList} + onManageNodes={() => openSettings('nodes')} /> ); case 'fleet': diff --git a/frontend/src/components/ResourcesView.tsx b/frontend/src/components/ResourcesView.tsx index 266863ed..4ca00e88 100644 --- a/frontend/src/components/ResourcesView.tsx +++ b/frontend/src/components/ResourcesView.tsx @@ -422,6 +422,17 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} const [imageSearch, setImageSearch] = useState(''); const [volumeSearch, setVolumeSearch] = useState(''); const [networkSearch, setNetworkSearch] = useState(''); + // Collapsible search: icon-only until clicked, stays open while query is active. + const [imageSearchExpanded, setImageSearchExpanded] = useState(false); + const [volumeSearchExpanded, setVolumeSearchExpanded] = useState(false); + const [networkSearchExpanded, setNetworkSearchExpanded] = useState(false); + const imageSearchRef = useRef(null); + const volumeSearchRef = useRef(null); + const networkSearchRef = useRef(null); + + useEffect(() => { if (imageSearchExpanded) imageSearchRef.current?.focus(); }, [imageSearchExpanded]); + useEffect(() => { if (volumeSearchExpanded) volumeSearchRef.current?.focus(); }, [volumeSearchExpanded]); + useEffect(() => { if (networkSearchExpanded) networkSearchRef.current?.focus(); }, [networkSearchExpanded]); // Modal states const [confirmPrune, setConfirmPrune] = useState<{ target: PruneTarget; scope: PruneScope } | null>(null); @@ -1002,11 +1013,6 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} Unmanaged {totalOrphansCount} - {totalOrphansCount > 0 && ( - - {totalOrphansCount} - - )} @@ -1014,20 +1020,35 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} )} - +
{/* Images */}
-
- - setImageSearch(e.target.value)} - className="pl-9 h-9" - /> -
+ {imageSearch !== '' || imageSearchExpanded ? ( +
+ + setImageSearch(e.target.value)} + onBlur={() => { if (imageSearch === '') setImageSearchExpanded(false); }} + className="pl-9 h-9" + /> +
+ ) : ( + + + + + + Search images + + + )}
+ @@ -1186,21 +1208,37 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} )}
+
{/* Volumes */}
-
- - setVolumeSearch(e.target.value)} - className="pl-9 h-9" - /> -
+ {volumeSearch !== '' || volumeSearchExpanded ? ( +
+ + setVolumeSearch(e.target.value)} + onBlur={() => { if (volumeSearch === '') setVolumeSearchExpanded(false); }} + className="pl-9 h-9" + /> +
+ ) : ( + + + + + + Search volumes + + + )}
+ @@ -1288,6 +1327,7 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} )}
+
@@ -1296,15 +1336,30 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
{networkViewMode === 'list' ? ( <> -
- - setNetworkSearch(e.target.value)} - className="pl-9 h-9" - /> -
+ {networkSearch !== '' || networkSearchExpanded ? ( +
+ + setNetworkSearch(e.target.value)} + onBlur={() => { if (networkSearch === '') setNetworkSearchExpanded(false); }} + className="pl-9 h-9" + /> +
+ ) : ( + + + + + + Search networks + + + )} ) : (
+ @@ -1452,6 +1508,7 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} )}
+
)} @@ -1541,7 +1598,7 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} )}
- +
); diff --git a/frontend/src/components/SecurityView.tsx b/frontend/src/components/SecurityView.tsx index 828767f8..531dc35b 100644 --- a/frontend/src/components/SecurityView.tsx +++ b/frontend/src/components/SecurityView.tsx @@ -270,7 +270,7 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
- + - + - + @@ -320,7 +320,7 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security - + diff --git a/frontend/src/components/dashboard/StackHealthTable.tsx b/frontend/src/components/dashboard/StackHealthTable.tsx index ce8f40c7..89b7419f 100644 --- a/frontend/src/components/dashboard/StackHealthTable.tsx +++ b/frontend/src/components/dashboard/StackHealthTable.tsx @@ -22,8 +22,8 @@ const PAGE_SIZE = 8; // Shared by the header and data rows so their columns stay aligned. The // `max-md:min-w` keeps both at the same width below md, where the card scrolls // horizontally; desktop is unaffected by the `max-md:` prefix. Columns: -// dot · STACK · SOURCE · PORT · UP · CPU · MEM · CPU·10m · chevron. -const GRID_TEMPLATE = 'grid-cols-[14px_minmax(0,1fr)_64px_56px_52px_52px_72px_110px_16px] max-md:min-w-[640px]'; +// STACK · SOURCE · PORT · UP · CPU · MEM · CPU·10m · chevron. +const GRID_TEMPLATE = 'grid-cols-[minmax(0,1fr)_64px_56px_52px_52px_72px_110px_16px] max-md:min-w-[620px]'; const formatMemory = (mb: number): string => { if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`; @@ -69,12 +69,6 @@ function formatUptime(seconds: number): string { return `${Math.max(1, Math.floor(seconds))}s`; } -const stateDot: Record = { - healthy: 'bg-success', - warn: 'bg-warning', - error: 'bg-destructive', -}; - const rowTint: Record = { healthy: '', warn: 'bg-warning/[0.04]', @@ -224,7 +218,6 @@ export function StackHealthTable({ ) : null}
- SOURCE PORT @@ -249,11 +242,10 @@ export function StackHealthTable({ }} className={`grid ${GRID_TEMPLATE} cursor-pointer items-center gap-4 px-[var(--density-row-x)] py-[var(--density-row-y)] transition-colors hover:bg-accent/5 ${rowTint[row.state]}`} > -