mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 11:47:11 +00:00
ecf4dd5d52
Realign tier guards to the user-stated philosophy: Community covers
deploy/monitor at scale plus security basics, Skipper adds automation
and advanced fleet management, Admiral keeps enterprise control.
Community now includes:
- Trivy install / uninstall / update from the Settings Hub (admin role)
- CVE suppressions CRUD (admin role; replicates fleet-wide)
- Manual image scan with vuln, secret, and misconfig results
- Stack-config scan, scan comparison
- Manual fleet snapshots: create, list, view, restore, delete
- Per-node Sencho self-update (Check Updates + per-node Update)
- Fleet Overview search, sort, filters, node-card expand, auto-refresh
Stays paid:
- Scan policies with block_on_deploy enforcement (Skipper+)
- SBOM (SPDX, CycloneDX), SARIF export (Skipper+)
- Bulk Update All across the fleet (Skipper+)
- Scheduled snapshot create (now Skipper, was Admiral)
- Trivy auto-update toggle, fleet-wide policy push (Admiral)
The Settings -> Security tab is unhidden by setting the registry tier to
null. The SecuritySection no longer early-returns a PaidGate; the policy
list, Add Policy button, and policy dialogs are wrapped in {isPaid && }.
The Fleet view drops isPaid gates on the Snapshots tab, Check Updates
button, per-node update handlers, OverviewToolbar grid controls, the
NodeCard expand affordance, and the auto-refresh notice. The
NodeUpdatesSheet receives a canBulkUpdate prop and gates the Update All
button on it. useFleetUpdateStatus and useFleetPolling drop their isPaid
guards so polling runs for Community; useFleetOverview drops the isPaid
wrap on the filter and sort path.
Backend route guards are flipped per the matrix above. The scheduler
tick and requireScheduledTaskTier add 'snapshot' to the Skipper+ branch.
Backend test assertions are inverted for the now-Community endpoints
and a positive Skipper-snapshot-task test is added.
Documentation across features/, api-reference/, and operations/ is
updated to reflect the new tier mapping.
152 lines
6.4 KiB
TypeScript
152 lines
6.4 KiB
TypeScript
import { Server, Search, RotateCcw } from 'lucide-react';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { Button } from '@/components/ui/button';
|
|
import { FleetTopology } from '../fleet/FleetTopology';
|
|
import { NodeCard } from './NodeCard';
|
|
import { OverviewToolbar } from './OverviewToolbar';
|
|
import type { FleetTopologyNode } from '@/lib/fleet-topology-layout';
|
|
import type { Label as StackLabel } from '../label-types';
|
|
import type { FleetNode, NodeUpdateStatus, ViewMode, FleetPreferences, FleetPaletteEntry } from './types';
|
|
|
|
interface OverviewTabProps {
|
|
loading: boolean;
|
|
nodes: FleetNode[];
|
|
processedNodes: FleetNode[];
|
|
allNodes: FleetNode[];
|
|
topologyNodes: FleetTopologyNode[];
|
|
viewMode: ViewMode;
|
|
onViewModeChange: (mode: ViewMode) => void;
|
|
searchQuery: string;
|
|
onSearchQueryChange: (q: string) => void;
|
|
prefs: FleetPreferences;
|
|
onPrefsChange: (update: Partial<FleetPreferences>) => void;
|
|
fleetPalette: FleetPaletteEntry[];
|
|
labelFilters: Set<string>;
|
|
onLabelFiltersChange: (filters: Set<string>) => void;
|
|
onClearFilters: () => void;
|
|
fleetStackLabelMap: Record<number, Record<string, StackLabel[]>>;
|
|
updateStatusMap: Map<number, NodeUpdateStatus>;
|
|
onNavigateToNode: (nodeId: number, stackName: string) => void;
|
|
onUpdate?: (nodeId: number) => void;
|
|
updatingNodeId: number | null;
|
|
onRetryUpdate?: (nodeId: number) => void;
|
|
onDismissUpdate?: (nodeId: number) => void;
|
|
}
|
|
|
|
export function OverviewTab({
|
|
loading,
|
|
nodes,
|
|
processedNodes,
|
|
allNodes,
|
|
topologyNodes,
|
|
viewMode,
|
|
onViewModeChange,
|
|
searchQuery,
|
|
onSearchQueryChange,
|
|
prefs,
|
|
onPrefsChange,
|
|
fleetPalette,
|
|
labelFilters,
|
|
onLabelFiltersChange,
|
|
onClearFilters,
|
|
fleetStackLabelMap,
|
|
updateStatusMap,
|
|
onNavigateToNode,
|
|
onUpdate,
|
|
updatingNodeId,
|
|
onRetryUpdate,
|
|
onDismissUpdate,
|
|
}: OverviewTabProps) {
|
|
return (
|
|
<>
|
|
{loading && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
<div key={i} className="rounded-xl border bg-card p-4 space-y-3">
|
|
<Skeleton className="h-8 w-32" />
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<Skeleton className="h-14 rounded-lg" />
|
|
<Skeleton className="h-14 rounded-lg" />
|
|
<Skeleton className="h-14 rounded-lg" />
|
|
</div>
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-4 w-3/4" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && nodes.length === 0 && (
|
|
<div className="flex flex-col items-center justify-center py-20 text-center">
|
|
<Server className="w-12 h-12 text-muted-foreground/50 mb-4" />
|
|
<h3 className="text-lg font-medium mb-1">No nodes configured</h3>
|
|
<p className="text-sm text-muted-foreground">Add nodes in Settings to see your fleet here.</p>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && nodes.length > 0 && (
|
|
<>
|
|
<OverviewToolbar
|
|
viewMode={viewMode}
|
|
onViewModeChange={onViewModeChange}
|
|
searchQuery={searchQuery}
|
|
onSearchQueryChange={onSearchQueryChange}
|
|
prefs={prefs}
|
|
onPrefsChange={onPrefsChange}
|
|
fleetPalette={fleetPalette}
|
|
labelFilters={labelFilters}
|
|
onLabelFiltersChange={onLabelFiltersChange}
|
|
onClearFilters={onClearFilters}
|
|
/>
|
|
|
|
{viewMode === 'topology' && processedNodes.length > 0 ? (
|
|
<FleetTopology
|
|
nodes={topologyNodes}
|
|
onNodeClick={(id) => onNavigateToNode(id, '')}
|
|
/>
|
|
) : processedNodes.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 items-start">
|
|
{allNodes.map(node => (
|
|
<NodeCard
|
|
key={node.id}
|
|
node={node}
|
|
onNavigate={onNavigateToNode}
|
|
labelMap={fleetStackLabelMap[node.id] ?? {}}
|
|
updateStatus={updateStatusMap.get(node.id)}
|
|
onUpdate={onUpdate}
|
|
updatingNodeId={updatingNodeId}
|
|
onRetryUpdate={onRetryUpdate}
|
|
onDismissUpdate={onDismissUpdate}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
<Search className="w-10 h-10 text-muted-foreground/50 mb-3" />
|
|
<h3 className="text-sm font-medium mb-1">No nodes match your filters</h3>
|
|
<p className="text-xs text-muted-foreground">Try adjusting your search or filter criteria.</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="mt-3"
|
|
onClick={() => {
|
|
onSearchQueryChange('');
|
|
onClearFilters();
|
|
}}
|
|
>
|
|
<RotateCcw className="w-3.5 h-3.5 mr-1.5" />
|
|
Clear filters
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<p className="text-xs text-muted-foreground text-center mt-6">
|
|
Auto-refreshing every 30 seconds
|
|
</p>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|