mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-27 04:11:01 +00:00
chore(resources): remove Largest 5 and Recently changed cards from Volumes tab (#981)
The two landing cards above the volumes table did not earn their space and made the Volumes view feel cluttered. Drop the cards, the unused TabLanding component, and the helpers that only fed it.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger, TabsHighlight, TabsHighlightItem } from "@/components/ui/tabs";
|
||||
@@ -37,22 +37,9 @@ import { FootprintTreemap } from './resources/FootprintTreemap';
|
||||
import { ImageDetailsSheet } from './resources/ImageDetailsSheet';
|
||||
import { VolumeBrowserSheet } from './resources/VolumeBrowserSheet';
|
||||
import { NetworkDetailSheet, type NetworkInspectData } from './resources/NetworkDetailSheet';
|
||||
import { TabLanding, type TabLandingEntry } from './resources/TabLanding';
|
||||
|
||||
const NetworkTopologyView = lazy(() => import('./NetworkTopologyView'));
|
||||
|
||||
const RECENT_WINDOW_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function ageLabel(ms: number): string {
|
||||
const minutes = Math.max(0, Math.round((Date.now() - ms) / 60000));
|
||||
if (minutes < 1) return 'just now';
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
// ── Interfaces ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface UsageData {
|
||||
@@ -627,28 +614,6 @@ export default function ResourcesView() {
|
||||
setConfirmPrune({ target: 'images', scope: 'all' });
|
||||
};
|
||||
|
||||
const volumeLandings = useMemo(() => {
|
||||
const largest: TabLandingEntry[] = [...volumes]
|
||||
.sort((a, b) => b.Size - a.Size)
|
||||
.slice(0, 5)
|
||||
.map(vol => ({
|
||||
key: vol.Name,
|
||||
primary: vol.Name,
|
||||
secondary: vol.Size > 0 ? formatBytes(vol.Size) : '-',
|
||||
}));
|
||||
const now = Date.now();
|
||||
const recent: TabLandingEntry[] = volumes
|
||||
.filter(v => !!v.CreatedAt && now - new Date(v.CreatedAt).getTime() <= RECENT_WINDOW_MS)
|
||||
.sort((a, b) => new Date(b.CreatedAt ?? 0).getTime() - new Date(a.CreatedAt ?? 0).getTime())
|
||||
.slice(0, 5)
|
||||
.map(vol => ({
|
||||
key: vol.Name,
|
||||
primary: vol.Name,
|
||||
secondary: vol.CreatedAt ? ageLabel(new Date(vol.CreatedAt).getTime()) : '-',
|
||||
}));
|
||||
return { largest, recent };
|
||||
}, [volumes]);
|
||||
|
||||
return (
|
||||
<div className="p-6 h-full overflow-auto text-foreground flex flex-col gap-6 animate-in fade-in-0 duration-300">
|
||||
|
||||
@@ -896,16 +861,6 @@ export default function ResourcesView() {
|
||||
|
||||
{/* Volumes */}
|
||||
<TabsContent value="volumes" className="m-0 border-0 p-0 animate-in fade-in-0 duration-200">
|
||||
{!isLoading && volumes.length > 0 && (
|
||||
<TabLanding
|
||||
largestSubtitle={`by size · ${volumes.length} total`}
|
||||
largestEntries={volumeLandings.largest}
|
||||
largestEmpty="No volumes on disk."
|
||||
recentSubtitle="last 24h"
|
||||
recentEntries={volumeLandings.recent}
|
||||
recentEmpty="No volumes created in the last 24h."
|
||||
/>
|
||||
)}
|
||||
<FilterToggle
|
||||
value={volumeFilter}
|
||||
onChange={setVolumeFilter}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface TabLandingEntry {
|
||||
key: string;
|
||||
primary: string;
|
||||
secondary: string;
|
||||
}
|
||||
|
||||
interface LandingCardProps {
|
||||
label: string;
|
||||
subtitle?: string;
|
||||
entries: TabLandingEntry[];
|
||||
emptyLabel: string;
|
||||
onEntryClick?: (entry: TabLandingEntry) => void;
|
||||
accent?: 'brand' | 'warning';
|
||||
}
|
||||
|
||||
function LandingCard({ label, subtitle, entries, emptyLabel, onEntryClick, accent = 'brand' }: LandingCardProps) {
|
||||
const accentClass = accent === 'warning' ? 'text-warning' : 'text-brand';
|
||||
return (
|
||||
<div className="flex flex-col rounded-md border border-card-border border-t-card-border-top bg-card shadow-card-bevel">
|
||||
<div className="flex items-baseline gap-2 px-3 pt-2.5 pb-1.5">
|
||||
<span className={cn('font-mono text-[10px] uppercase tracking-[0.22em]', accentClass)}>
|
||||
{label}
|
||||
</span>
|
||||
{subtitle ? (
|
||||
<span className="font-mono text-[10px] text-stat-subtitle">{subtitle}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{entries.length === 0 ? (
|
||||
<div className="px-3 pb-3 text-[11px] font-mono text-stat-subtitle/80">
|
||||
{emptyLabel}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="flex flex-col divide-y divide-border/40 px-1 pb-1">
|
||||
{entries.map(entry => (
|
||||
<li key={entry.key}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEntryClick?.(entry)}
|
||||
disabled={!onEntryClick}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between gap-3 rounded-sm px-2 py-1 text-left transition-colors',
|
||||
onEntryClick ? 'hover:bg-muted/40 cursor-pointer' : 'cursor-default',
|
||||
)}
|
||||
>
|
||||
<span className="truncate font-mono text-[11px] text-stat-value">{entry.primary}</span>
|
||||
<span className="shrink-0 font-mono text-[10px] tabular-nums text-stat-subtitle">
|
||||
{entry.secondary}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TabLandingProps {
|
||||
largestLabel?: string;
|
||||
largestSubtitle?: string;
|
||||
largestEntries: TabLandingEntry[];
|
||||
largestEmpty: string;
|
||||
recentLabel?: string;
|
||||
recentSubtitle?: string;
|
||||
recentEntries: TabLandingEntry[];
|
||||
recentEmpty: string;
|
||||
onLargestClick?: (entry: TabLandingEntry) => void;
|
||||
onRecentClick?: (entry: TabLandingEntry) => void;
|
||||
}
|
||||
|
||||
export function TabLanding({
|
||||
largestLabel = 'Largest 5',
|
||||
largestSubtitle,
|
||||
largestEntries,
|
||||
largestEmpty,
|
||||
recentLabel = 'Recently changed',
|
||||
recentSubtitle,
|
||||
recentEntries,
|
||||
recentEmpty,
|
||||
onLargestClick,
|
||||
onRecentClick,
|
||||
}: TabLandingProps) {
|
||||
const largestTop = useMemo(() => largestEntries.slice(0, 5), [largestEntries]);
|
||||
const recentTop = useMemo(() => recentEntries.slice(0, 5), [recentEntries]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-3 border-b">
|
||||
<LandingCard
|
||||
label={largestLabel}
|
||||
subtitle={largestSubtitle}
|
||||
entries={largestTop}
|
||||
emptyLabel={largestEmpty}
|
||||
onEntryClick={onLargestClick}
|
||||
accent="brand"
|
||||
/>
|
||||
<LandingCard
|
||||
label={recentLabel}
|
||||
subtitle={recentSubtitle}
|
||||
entries={recentTop}
|
||||
emptyLabel={recentEmpty}
|
||||
onEntryClick={onRecentClick}
|
||||
accent="warning"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user