fix: UI polish - tooltips, masthead, tabs, settings, and layout consistency (#1563)

* fix: increase masthead rail shimmer visibility

Bumped shimmer highlight opacity from 25% to 50% and reduced
animation duration from 11s to 5s, matching the login page
AuthCanvas shimmer precedent (via-white/60 at 4.5s).

* fix: reduce masthead rail opacity to match login page pattern

Changed all MastheadRail background classes from 100% to 70% opacity
(bg-brand → bg-brand/70, etc.) so the shimmer highlight reads as a surface
reflection rather than a detached floating bar. Matches the AuthCanvas
login card precedent.

* fix: improve masthead rail glow visibility

Replaced the invisible inset box-shadow glow (10px blur inside a 3px
element) with a white overlay that pulses opacity 0.15-0.55 over 4s.
Sped up glow animation from 5.5s to 4s for a more noticeable breath.

* fix: replace cursor-follow tooltip with standard Radix tooltip in sidebar rows

Replaced the Cursor/CursorFollow animate-ui primitives in StackRow
with the standard Radix Tooltip/TooltipTrigger/TooltipContent already
used throughout the app. The custom cursor dot that followed the mouse
is gone; tooltips now appear as static popovers on hover/touch.

* fix: wrap tooltip icon triggers in span for Radix compatibility

TooltipTrigger asChild requires a native element to forward props.
Lucide icon components did not reliably receive data-state and event
handlers, so tooltips on the check-failed and git-pending indicators
did not open. Wrapped each icon in a span.

* fix: rename Notification Routing to Routing in settings sidebar

* fix: replace Timeline/All tasks buttons with SegmentedControl

The two loose Button elements had indistinguishable selected/unselected
states (secondary vs ghost). Replaced with the SegmentedControl component
already used elsewhere on the same page, which has a clear active
highlight indicator.

* fix: standardize Blueprint empty state headings to font-heading

Replaced manual font-serif [font-style:var(--heading-style)] with the
font-heading class on the main heading and three step titles so the
Deployments empty state follows the same heading convention as the
rest of the app and respects the Calm/Signature theme toggle.

* fix: remove custom tooltip styling, use standard TooltipContent defaults

The RowTooltip wrapped its label in a font-mono text-stat-value span
instead of using the TooltipContent base styling (text-xs
text-popover-foreground) like every other tooltip in the app.

* Revert "fix: remove custom tooltip styling, use standard TooltipContent defaults"

This reverts commit 209fbd2742.

* fix: standardize all tooltips to match sidebar RowTooltip design

Wrapped every TooltipContent child in a font-mono text-xs
tabular-nums text-stat-value span so all tooltips across the app
share a consistent look. Previously some used plain text, others
used <p> elements with different fonts and sizes.

* Revert "fix: standardize all tooltips to match sidebar RowTooltip design"

This reverts commit 9e1d97d39e.

* fix: bake standard tooltip styling into TooltipContent component

Added font-mono tabular-nums text-stat-value directly to the
TooltipContent base styles so every tooltip in the app inherits
the same look without per-instance wrappers. Removed the now-
redundant span from StackRow's RowTooltip.

* fix: replace custom tooltip with shadcn/ui Radix tooltip

Replaced the glass/blur-styled tooltip with the standard shadcn/ui
pattern: bg-foreground/text-background, arrow pointer, proper
slide/fade animations, and w-fit max-w-xs sizing. This gives all
tooltips a consistent dark-solid look across the entire app.

* fix: apply glass/blur design to shadcn tooltip component

Replaced the solid bg-foreground/text-background style with the app's
glass design: bg-popover, border-glass-border, backdrop-blur, and
text-popover-foreground. Kept the shadcn structure (Portal, Arrow,
slide/fade animations).

* fix: replace native title tooltips with Radix tooltips on Fleet toolbar

Converted Refresh, Export Dossier, Search, Sort direction, Node Update,
and Manage Nodes buttons from native HTML title attributes to the shared
Radix Tooltip component so they match the app-wide glass tooltip design.

* fix: replace native title tooltip on Fleet Snapshots upload button

* fix: replace native title tooltips with Radix tooltips on Resources page

Converted all actionable button tooltips (Open stack, Inspect image,
Scan, Browse volume, and delete buttons) plus the SenchoBadge
informational tooltip. Wrapped disabled protected-resource delete
buttons in span triggers so tooltips fire on disabled elements.

* fix: replace native title tooltips with Radix tooltips on Security page

Converted scanner info icon, exploitability n/a badge, and scan
image button from native HTML title attributes to the shared Radix
Tooltip component.

* fix: replace native title tooltips with Radix tooltips on Schedules and Settings pages

Converted Run now, Execution history, Edit, Delete buttons on Schedules;
Download/Delete on Cloud Backup; Send test/Edit/Delete on Notification
Routing; Reset 2FA on Users; and Edit/Remove suppression on
Suppressions panel. Skipped SettingsSection/Modal title props which
are React component props, not native HTML attributes.

* feat: add copy button to execution history details column

Replaced the plain truncated Details cell with a flex layout containing
a copy button. Users can now copy the full error/output text instead
of relying on the native title tooltip to read long messages.

* fix: change tab highlight from neutral to brand color

Replaced bg-glass-highlight with bg-brand/20 on all TabsHighlight
instances and updated TabsTrigger active state to text-brand,
matching the SegmentedControl selected style. Works on both
dark and light themes.

* fix: restructure Resources page tabs to match Fleet/Security pattern

Moved tab band out of the outer card into a standalone full-width
element. Replaced custom FilterToggle with Fleet-style Button
toggles. Added search inputs per tab. Wrapped each resource table
in a card container matching the Security Images table design.

* fix: match Resources tab font size to Fleet and Security

Removed explicit text-xs from Resources TabsTrigger elements so they
use the default text-sm, matching Fleet and Security tab typography.

* fix: restore missing closing bracket on TabsTrigger tag

* fix: add aria-labels to schedule action buttons, update test selectors

Replaced native title attributes with aria-labels on Run now,
Execution history, Edit, and Delete buttons so the Radix tooltip
conversion does not break test selectors that relied on title.
Updated tests from findByTitle to findByRole.

* fix: add fingerprint-based dismiss to post-deploy scan banner

Created useScanBannerDismiss hook following the usePreflightDismiss
pattern. The banner now persists dismissal in localStorage keyed to a
fingerprint of scan status + attemptedAt. Dismissal survives page
reloads for the same scan outcome, and the banner automatically
reappears when a new scan runs or the status changes.

* fix: update Configuration Status card labels and add new rows

Notifications section: renamed Notification agents to Channels,
Notification routing to Routing, added Mute Rules row linking to
notification-suppression settings.

Security section: added Trivy installed Yes/No row, renamed
Vulnerability scanning to Scan policies.

Backend: added trivyInstalled and suppressionRules fields to
the /dashboard/configuration payload.

* fix: style Trivy row as badge and link to Security scanner setup

* fix: remove Mute button from stack anatomy panel header

* Revert "fix: remove Mute button from stack anatomy panel header"

This reverts commit 16843a81b5.

* fix: remove Mute button from stack anatomy panel header

* fix: remove unused stackMuteActions imports and props from StackAnatomyPanel

The removal of the ActivityMuteKebab rendering left orphaned imports
and props that cause TS6133 errors under strict mode. Clean up the
imports, prop type, destructuring, and caller prop passes.

* fix: refine anatomy tab sizing and add pulsing scroll chevron

Changed anatomy tabs from 12px fixed height to 11px with py-1
padding, matching the Fleet tab approach. Bumped Files/Edit buttons
from 10px to xs for better visibility. Added subtle animate-pulse
to the right scroll chevron to hint at overflow tabs, gated behind
the Reduced effects appearance setting.

* fix: link reclaim hero number to heading style

* fix: replace native title tooltips with Radix tooltips on stack detail page

Converted Expand/Collapse logs, Stats unavailable, Copy service URL,
Dismiss findings, Likely secret badge, Move up/down, and Remove
buttons from native HTML title attributes to the shared Radix
glass tooltip component.

* fix: replace native title tooltip on image source link button

* fix: add tooltips to container logs and bash session buttons

* fix: add tooltip to download logs button

* fix: add tooltip to copy digest button

* fix: replace cursor-follow tooltip with Radix tooltip on severity badge

* fix: add tooltips to density toggle buttons

* feat: add expand button to multi-container health panel

Added an expand/collapse toggle next to the density controls that
expands the containers panel to fill the column and hides the logs
section. The expand and logs-expand are mutually exclusive. Only
appears for stacks with more than one container.

* fix: reduce Structured/Raw terminal toggle font to 10px

* fix: set compact density as default for multi-container stacks

* fix: change Feedback to Open New Issue with GitHub issues link

* fix: replace ExternalLink with GitHub icon for Open New Issue menu item

* fix: restore ExternalLink import needed by other menu items

* fix: default host threshold alerts to off for new installations

* fix: resolve CI typecheck and test failures

Broaden MenuRowProps.icon type to accept custom SVG components alongside
lucide icons, fixing TS2741 on GitHubIcon import.

Update host_alerts_enabled seed test assertion to match the new opt-in
default of off (0).

* fix: update ContainersHealth tests for compact default density

Adjust three tests to reflect the new default of compact mode:
- Rename 'detailed mode is the default' to 'compact mode is the default'
- Update sparkline visibility checks to switch to detailed first
- Rename remount test and verify reset to compact (new default)
This commit is contained in:
Anso
2026-07-06 04:25:01 -04:00
committed by GitHub
parent 0f9925e04f
commit bbcc4b59e4
48 changed files with 1209 additions and 682 deletions
+235 -134
View File
@@ -13,7 +13,9 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
import { Trash2, HardDrive, Network, PackageMinus, MonitorX, MoreVertical, AlertTriangle, ShieldCheck, Plus, Eye, Loader2, History, FolderOpen } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Trash2, HardDrive, Network, PackageMinus, MonitorX, MoreVertical, AlertTriangle, ShieldCheck, Plus, Eye, Loader2, History, FolderOpen, Search } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { SeverityBadge } from '@/components/ui/SeverityBadge';
import { useTrivyStatus } from '@/hooks/useTrivyStatus';
import { VulnerabilityScanSheet } from './VulnerabilityScanSheet';
@@ -143,29 +145,19 @@ function FilterToggle({ value, onChange, counts }: FilterToggleProps) {
];
return (
<div className="flex items-center gap-1 px-3 py-2.5 border-b bg-muted/10">
<div className="flex items-center gap-0.5 bg-muted/50 rounded-lg p-0.5">
{options.map(({ key, label, count }) => (
<button
key={key}
onClick={() => onChange(key)}
className={cn(
'flex items-center gap-1.5 px-3 py-1 rounded-md text-xs font-medium transition-all duration-200',
value === key
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground',
)}
>
{label}
<span className={cn(
'inline-flex items-center justify-center h-4 min-w-4 px-1 rounded-sm text-[10px] font-mono transition-colors duration-200',
value === key ? 'bg-muted text-foreground' : 'text-stat-subtitle',
)}>
{count}
</span>
</button>
))}
</div>
<div className="flex items-center gap-1">
{options.map(({ key, label, count }) => (
<Button
key={key}
variant={value === key ? 'default' : 'outline'}
size="sm"
className="h-7 text-xs px-2.5 gap-1.5"
onClick={() => onChange(key)}
>
{label}
<span className="font-mono tabular-nums text-[10px] opacity-70">{count}</span>
</Button>
))}
</div>
);
}
@@ -183,9 +175,16 @@ function ManagedBadge({ status, managedBy, onOpenStack }: {
const inner = (<><span className="w-1.5 h-1.5 rounded-full bg-success shrink-0" />{managedBy}</>);
if (onOpenStack && managedBy) {
return (
<button type="button" className={`${cls} hover:bg-success/15 transition-colors`} title={`Open stack ${managedBy}`} onClick={() => onOpenStack(managedBy)}>
{inner}
</button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button type="button" className={`${cls} hover:bg-success/15 transition-colors`} onClick={() => onOpenStack(managedBy)}>
{inner}
</button>
</TooltipTrigger>
<TooltipContent>Open stack {managedBy}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return <span className={cls}>{inner}</span>;
@@ -213,13 +212,17 @@ function ManagedBadge({ status, managedBy, onOpenStack }: {
function SenchoBadge() {
return (
<span
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded border border-brand/25 bg-brand/8 text-brand text-[10px] font-medium"
title="Protected · running Sencho instance"
>
<span className="w-1.5 h-1.5 rounded-full bg-brand shrink-0" />
Sencho
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded border border-brand/25 bg-brand/8 text-brand text-[10px] font-medium">
<span className="w-1.5 h-1.5 rounded-full bg-brand shrink-0" />
Sencho
</span>
</TooltipTrigger>
<TooltipContent>Protected · running Sencho instance</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
@@ -340,6 +343,11 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
const [volumeFilter, setVolumeFilter] = useState<ResourceFilter>('all');
const [networkFilter, setNetworkFilter] = useState<ResourceFilter>('all');
// Search state
const [imageSearch, setImageSearch] = useState('');
const [volumeSearch, setVolumeSearch] = useState('');
const [networkSearch, setNetworkSearch] = useState('');
// Modal states
const [confirmPrune, setConfirmPrune] = useState<{ target: PruneTarget; scope: PruneScope } | null>(null);
const [confirmDelete, setConfirmDelete] = useState<{ type: 'images' | 'volumes' | 'networks'; id: string; name?: string } | null>(null);
@@ -617,16 +625,19 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
// Derived filtered lists
const filteredImages = images.filter(img =>
imageFilter === 'managed' ? img.managedStatus === 'managed' :
imageFilter === 'unmanaged' ? img.managedStatus !== 'managed' : true
(imageFilter === 'managed' ? img.managedStatus === 'managed' :
imageFilter === 'unmanaged' ? img.managedStatus !== 'managed' : true) &&
(imageSearch === '' || (img.RepoTags?.[0] || '').toLowerCase().includes(imageSearch.toLowerCase()))
);
const filteredVolumes = volumes.filter(vol =>
volumeFilter === 'managed' ? vol.managedStatus === 'managed' :
volumeFilter === 'unmanaged' ? vol.managedStatus !== 'managed' : true
(volumeFilter === 'managed' ? vol.managedStatus === 'managed' :
volumeFilter === 'unmanaged' ? vol.managedStatus !== 'managed' : true) &&
(volumeSearch === '' || vol.Name.toLowerCase().includes(volumeSearch.toLowerCase()))
);
const filteredNetworks = networks.filter(net =>
networkFilter === 'managed' ? net.managedStatus === 'managed' :
networkFilter === 'unmanaged' ? net.managedStatus !== 'managed' : true
(networkFilter === 'managed' ? net.managedStatus === 'managed' :
networkFilter === 'unmanaged' ? net.managedStatus !== 'managed' : true) &&
(networkSearch === '' || net.Name.toLowerCase().includes(networkSearch.toLowerCase()))
);
// Sortable tables (standard sort behavior across the resource tables).
@@ -805,7 +816,7 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
<Tabs
value={resourceTab}
onValueChange={(v) => setResourceTab(v as typeof resourceTab)}
className="flex-1 flex flex-col w-full rounded-lg border bg-card shadow-card-bevel overflow-hidden min-h-[400px] animate-in fade-in-0 slide-in-from-bottom-2 duration-300 delay-150"
className="flex-1 flex flex-col w-full min-h-[400px]"
>
{isMobile ? (
<MobileSubTabs
@@ -820,19 +831,23 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
]}
/>
) : (
<div className="px-4 pt-3 pb-2 flex items-center justify-between gap-3">
<TabsList className="grid grid-cols-4 w-full md:w-[680px] h-9 gap-1 p-0">
<TabsHighlight className="rounded-md bg-glass-highlight" transition={springs.snappy}>
<div className="flex items-center gap-3 mb-4 flex-wrap rounded-lg border border-card-border bg-card/40 px-2.5 py-1.5">
<TabsList className="border-transparent bg-transparent max-md:w-full max-md:overflow-x-auto max-md:[scrollbar-width:none]">
<TabsHighlight className="rounded-md bg-brand/20" transition={springs.snappy}>
{(['images', 'volumes', 'networks'] as const).map(tab => (
<TabsHighlightItem key={tab} value={tab}>
<TabsTrigger value={tab} className="capitalize text-xs">
{tab}
<TabsTrigger value={tab}>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
<span className="ml-1.5 text-[10px] text-stat-subtitle tabular-nums">
{tab === 'images' ? images.length : tab === 'volumes' ? volumes.length : networks.length}
</span>
</TabsTrigger>
</TabsHighlightItem>
))}
<TabsHighlightItem value="unmanaged">
<TabsTrigger value="unmanaged" className="relative text-xs">
<TabsTrigger value="unmanaged" className="relative">
Unmanaged
<span className="ml-1.5 text-[10px] text-stat-subtitle tabular-nums">{totalOrphansCount}</span>
{totalOrphansCount > 0 && (
<span className="absolute -top-1.5 -right-1 flex h-4 min-w-4 px-1 items-center justify-center rounded-full bg-warning text-[9px] text-warning-foreground font-medium animate-in zoom-in-75 duration-200">
{totalOrphansCount}
@@ -849,7 +864,16 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
{/* Images */}
<TabsContent value="images" className="m-0 border-0 p-0 animate-in fade-in-0 duration-200">
<div className="flex items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-2 mb-4">
<div className="relative flex-1 min-w-[200px] max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
<Input
placeholder="Search images..."
value={imageSearch}
onChange={(e) => setImageSearch(e.target.value)}
className="pl-9 h-9"
/>
</div>
<FilterToggle
value={imageFilter}
onChange={setImageFilter}
@@ -859,24 +883,25 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
unmanaged: images.filter(i => i.managedStatus !== 'managed').length,
}}
/>
<div className="flex-1" />
{trivy.available && (
<Button
variant="outline"
size="sm"
className="h-7 text-xs gap-1.5 mr-3 shrink-0"
className="h-9 gap-2 shrink-0"
onClick={() => {
window.dispatchEvent(new CustomEvent<SenchoNavigateDetail>(SENCHO_NAVIGATE_EVENT, {
detail: { view: 'security', tab: 'history' },
}));
}}
title="View completed vulnerability scans and compare them"
aria-label="Open scan history"
>
<History className="w-3.5 h-3.5" strokeWidth={1.5} />
<History className="w-4 h-4" strokeWidth={1.5} />
Scan history
</Button>
)}
</div>
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel overflow-hidden">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
@@ -917,33 +942,45 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setInspectImageId(img.Id)}
title="Inspect image"
aria-label={`Inspect ${img.RepoTags?.[0] || 'image'}`}
>
<Eye className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>
{trivy.available && isAdmin && img.RepoTags?.[0] && img.RepoTags[0] !== '<none>:<none>' && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-foreground transition-colors"
disabled={scanningImageRef === img.RepoTags[0]}
title="Scan for vulnerabilities"
onClick={() => setInspectImageId(img.Id)}
aria-label={`Inspect ${img.RepoTags?.[0] || 'image'}`}
>
{scanningImageRef === img.RepoTags[0] ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" strokeWidth={1.5} />
) : (
<ShieldCheck className="w-3.5 h-3.5" strokeWidth={1.5} />
)}
<Eye className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Inspect image</TooltipContent>
</Tooltip>
</TooltipProvider>
{trivy.available && isAdmin && img.RepoTags?.[0] && img.RepoTags[0] !== '<none>:<none>' && (
<DropdownMenu>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-foreground transition-colors"
disabled={scanningImageRef === img.RepoTags[0]}
>
{scanningImageRef === img.RepoTags[0] ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" strokeWidth={1.5} />
) : (
<ShieldCheck className="w-3.5 h-3.5" strokeWidth={1.5} />
)}
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Scan for vulnerabilities</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => handleScanImage(img.RepoTags![0], { scanners: ['vuln'] })}
@@ -958,16 +995,26 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
</DropdownMenuContent>
</DropdownMenu>
)}
{isAdmin && <Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive/60 hover:bg-destructive hover:text-destructive-foreground transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-destructive/60"
disabled={img.isSencho}
title={img.isSencho ? 'Protected · running Sencho instance' : undefined}
onClick={() => setConfirmDelete({ type: 'images', id: img.Id, name: img.RepoTags?.[0] })}
>
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>}
{isAdmin && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className={img.isSencho ? 'cursor-not-allowed' : undefined}>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive/60 hover:bg-destructive hover:text-destructive-foreground transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-destructive/60"
disabled={img.isSencho}
onClick={() => setConfirmDelete({ type: 'images', id: img.Id, name: img.RepoTags?.[0] })}
>
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>
</span>
</TooltipTrigger>
{img.isSencho && <TooltipContent>Protected · running Sencho instance</TooltipContent>}
</Tooltip>
</TooltipProvider>
)}
</div>
</TableCell>
</TableRow>
@@ -975,19 +1022,32 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
</TableBody>
)}
</Table>
</div>
</TabsContent>
{/* Volumes */}
<TabsContent value="volumes" className="m-0 border-0 p-0 animate-in fade-in-0 duration-200">
<FilterToggle
value={volumeFilter}
onChange={setVolumeFilter}
counts={{
all: volumes.length,
managed: volumes.filter(v => v.managedStatus === 'managed').length,
unmanaged: volumes.filter(v => v.managedStatus !== 'managed').length,
}}
/>
<div className="flex flex-wrap items-center gap-2 mb-4">
<div className="relative flex-1 min-w-[200px] max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
<Input
placeholder="Search volumes..."
value={volumeSearch}
onChange={(e) => setVolumeSearch(e.target.value)}
className="pl-9 h-9"
/>
</div>
<FilterToggle
value={volumeFilter}
onChange={setVolumeFilter}
counts={{
all: volumes.length,
managed: volumes.filter(v => v.managedStatus === 'managed').length,
unmanaged: volumes.filter(v => v.managedStatus !== 'managed').length,
}}
/>
</div>
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel overflow-hidden">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
@@ -1020,27 +1080,43 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
{isAdmin && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setBrowseVolume(vol.Name)}
title="Browse volume contents"
aria-label={`Browse ${vol.Name}`}
>
<FolderOpen className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setBrowseVolume(vol.Name)}
aria-label={`Browse ${vol.Name}`}
>
<FolderOpen className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>
</TooltipTrigger>
<TooltipContent>Browse volume contents</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{isAdmin && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className={vol.isSencho ? 'cursor-not-allowed' : undefined}>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive/60 hover:bg-destructive hover:text-destructive-foreground transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-destructive/60"
disabled={vol.isSencho}
onClick={() => setConfirmDelete({ type: 'volumes', id: vol.Name, name: vol.Name })}
>
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>
</span>
</TooltipTrigger>
{vol.isSencho && <TooltipContent>Protected · running Sencho instance</TooltipContent>}
</Tooltip>
</TooltipProvider>
)}
{isAdmin && <Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive/60 hover:bg-destructive hover:text-destructive-foreground transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-destructive/60"
disabled={vol.isSencho}
title={vol.isSencho ? 'Protected · running Sencho instance' : undefined}
onClick={() => setConfirmDelete({ type: 'volumes', id: vol.Name, name: vol.Name })}
>
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>}
</div>
</TableCell>
</TableRow>
@@ -1048,27 +1124,38 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
</TableBody>
)}
</Table>
</div>
</TabsContent>
{/* Networks */}
<TabsContent value="networks" className="m-0 border-0 p-0 animate-in fade-in-0 duration-200">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-center gap-2 mb-4">
{networkViewMode === 'list' ? (
<FilterToggle
value={networkFilter}
onChange={setNetworkFilter}
counts={{
all: networks.length,
managed: networks.filter(n => n.managedStatus === 'managed').length,
unmanaged: networks.filter(n => n.managedStatus !== 'managed').length,
}}
/>
<>
<div className="relative flex-1 min-w-[200px] max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
<Input
placeholder="Search networks..."
value={networkSearch}
onChange={(e) => setNetworkSearch(e.target.value)}
className="pl-9 h-9"
/>
</div>
<FilterToggle
value={networkFilter}
onChange={setNetworkFilter}
counts={{
all: networks.length,
managed: networks.filter(n => n.managedStatus === 'managed').length,
unmanaged: networks.filter(n => n.managedStatus !== 'managed').length,
}}
/>
</>
) : (
// Keep a left spacer so justify-between holds the toggle +
// Create Network group anchored on the right in topology mode.
<span aria-hidden="true" />
)}
<div className="flex items-center gap-2 pr-3">
<div className="flex-1" />
<div className="flex items-center gap-2">
<div className="flex items-center gap-0.5 bg-muted/50 rounded-lg p-0.5">
<button
onClick={() => setNetworkViewMode('list')}
@@ -1125,6 +1212,7 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
</CapabilityGate>
</div>
) : (
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel overflow-hidden">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
@@ -1173,16 +1261,26 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
>
{inspectLoadingId === net.Id ? <Loader2 className="w-3.5 h-3.5 animate-spin" strokeWidth={1.5} /> : <Eye className="w-3.5 h-3.5" strokeWidth={1.5} />}
</Button>
{isAdmin && <Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive/60 hover:bg-destructive hover:text-destructive-foreground transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-destructive/60"
disabled={net.managedStatus === 'system' || net.isSencho}
title={net.isSencho ? 'Protected · running Sencho instance' : undefined}
onClick={() => setConfirmDelete({ type: 'networks', id: net.Id, name: net.Name })}
>
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>}
{isAdmin && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className={net.isSencho ? 'cursor-not-allowed' : undefined}>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive/60 hover:bg-destructive hover:text-destructive-foreground transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-destructive/60"
disabled={net.managedStatus === 'system' || net.isSencho}
onClick={() => setConfirmDelete({ type: 'networks', id: net.Id, name: net.Name })}
>
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
</Button>
</span>
</TooltipTrigger>
{net.isSencho && <TooltipContent>Protected · running Sencho instance</TooltipContent>}
</Tooltip>
</TooltipProvider>
)}
</div>
</TableCell>
</TableRow>
@@ -1190,11 +1288,13 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
</TableBody>
)}
</Table>
</div>
)}
</TabsContent>
{/* Unmanaged Containers */}
<TabsContent value="unmanaged" className="m-0 border-0 p-0 h-full flex flex-col animate-in fade-in-0 duration-200">
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel overflow-hidden">
<div className="flex justify-between items-center px-4 py-2.5 border-b bg-muted/10 sticky top-0 z-10">
<div className="flex items-center gap-2.5">
<input
@@ -1275,6 +1375,7 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
))}
</div>
)}
</div>
</TabsContent>
</ScrollArea>
</Tabs>