diff --git a/frontend/src/components/FleetSnapshots.tsx b/frontend/src/components/FleetSnapshots.tsx index a963416e..309996d9 100644 --- a/frontend/src/components/FleetSnapshots.tsx +++ b/frontend/src/components/FleetSnapshots.tsx @@ -18,6 +18,7 @@ import { apiFetch } from '@/lib/api'; import { useAuth } from '@/context/AuthContext'; import { useLicense } from '@/context/LicenseContext'; import { toast } from '@/components/ui/toast-store'; +import { FleetTabHeading, FleetEmptyState, FleetEmptyCard } from './fleet/FleetEmptyState'; // --- Types --- @@ -595,34 +596,33 @@ export default function FleetSnapshots() { return (
- {/* Header */} -
-
- -

Fleet Snapshots

-
-
- {needsPagination && ( -
- + + {safePage + 1} / {totalPages} + + +
+ )} + {isAdmin && !showCreateForm && ( + - - {safePage + 1} / {totalPages} - - -
- )} - {isAdmin && !showCreateForm && ( - - )} -
-
+ )} + + } + /> {/* Create form */} {showCreateForm && ( @@ -666,14 +666,19 @@ export default function FleetSnapshots() { ) : snapshots.length === 0 ? ( - /* Empty state */ -
- -

No snapshots yet

-

- Create your first fleet snapshot to back up compose files across all nodes. -

-
+ + setShowCreateForm(true)}> + + Create Snapshot + + ) : undefined} + /> + ) : ( /* Snapshots table */
diff --git a/frontend/src/components/blueprints/DeploymentsTab.tsx b/frontend/src/components/blueprints/DeploymentsTab.tsx index 8092d3cb..22a07cfe 100644 --- a/frontend/src/components/blueprints/DeploymentsTab.tsx +++ b/frontend/src/components/blueprints/DeploymentsTab.tsx @@ -1,5 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; +import { Plus } from 'lucide-react'; import { Modal, ModalHeader, ModalBody } from '@/components/ui/modal'; +import { Button } from '@/components/ui/button'; import { toast } from '@/components/ui/toast-store'; import { type BlueprintListItem, @@ -11,6 +13,7 @@ import { } from '@/lib/blueprintsApi'; import { BlueprintCatalog } from './BlueprintCatalog'; import { BlueprintEmptyState } from './BlueprintEmptyState'; +import { FleetTabHeading, FleetEmptyState } from '../fleet/FleetEmptyState'; import { BlueprintDetail } from './BlueprintDetail'; import { BlueprintEditor } from './BlueprintEditor'; import { useLicense } from '@/context/LicenseContext'; @@ -95,7 +98,21 @@ export function DeploymentsTab() { return (
{blueprints.length === 0 ? ( - setCreateOpen(true)} canCreate={canEdit} /> + <> + setCreateOpen(true)}> + + New Blueprint + + ) : undefined} + /> + + setCreateOpen(true)} canCreate={canEdit} /> + + ) : ( -
-
- -
-
-
Federation
-

Placement control

-

- Mark nodes unschedulable and pin blueprints to specific nodes. Sencho proposes placements; you approve them. Existing deployments are never moved without an explicit operator action. -

-
-
+
diff --git a/frontend/src/components/fleet/FleetEmptyState.tsx b/frontend/src/components/fleet/FleetEmptyState.tsx new file mode 100644 index 00000000..78b8be02 --- /dev/null +++ b/frontend/src/components/fleet/FleetEmptyState.tsx @@ -0,0 +1,61 @@ +import type { LucideIcon } from 'lucide-react'; +import type { ReactNode } from 'react'; + +interface FleetTabHeadingProps { + title: string; + subtitle: string; + action?: ReactNode; +} + +/** + * Standardized Fleet tab header: italic-serif title and muted subtitle on the + * left, an optional primary action on the right. Rendered in both empty and + * populated states so the tab chrome stays consistent. + */ +export function FleetTabHeading({ title, subtitle, action }: FleetTabHeadingProps) { + return ( +
+
+

{title}

+

{subtitle}

+
+ {action} +
+ ); +} + +/** + * Vertical-centering shell that floats an empty-state card in the middle of the + * tab body, below the heading. + */ +export function FleetEmptyState({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +interface FleetEmptyCardProps { + icon: LucideIcon; + title: string; + description: string; + action?: ReactNode; +} + +/** + * Minimal empty-state card: centered icon, italic headline, muted one-line + * description, optional CTA. + */ +export function FleetEmptyCard({ icon: Icon, title, description, action }: FleetEmptyCardProps) { + return ( +
+ +
+

{title}

+

{description}

+
+ {action} +
+ ); +} diff --git a/frontend/src/components/fleet/RoutingTab.tsx b/frontend/src/components/fleet/RoutingTab.tsx index bcf87e1f..25a6c877 100644 --- a/frontend/src/components/fleet/RoutingTab.tsx +++ b/frontend/src/components/fleet/RoutingTab.tsx @@ -12,6 +12,7 @@ import { MeshDiagnosticsSheet } from './MeshDiagnosticsSheet'; import { MeshActivitySheet } from './MeshActivitySheet'; import { MeshTopologyGraph, type MeshGraphEdgeMode } from './MeshTopologyGraph'; import { SegmentedControl } from '@/components/ui/segmented-control'; +import { FleetTabHeading, FleetEmptyState, FleetEmptyCard } from './FleetEmptyState'; import type { MeshAlias, MeshDataPlaneStatus, MeshNodeStatus, MeshProbeResult } from '@/types/mesh'; type RoutingViewMode = 'table' | 'graph'; @@ -137,12 +138,18 @@ export function RoutingTab({ canManage }: { canManage: boolean }) { if (status.length === 0) { return ( -
- -
No nodes available
-
- Add a node to the fleet to start routing traffic between containers across nodes. -
+
+ + + +
); } diff --git a/frontend/src/components/fleet/secrets/SecretsTab.tsx b/frontend/src/components/fleet/secrets/SecretsTab.tsx index 5bec6986..1fee33e9 100644 --- a/frontend/src/components/fleet/secrets/SecretsTab.tsx +++ b/frontend/src/components/fleet/secrets/SecretsTab.tsx @@ -5,6 +5,7 @@ import { toast } from '@/components/ui/toast-store'; import { listSecrets, deleteSecret, type SecretSummary } from '@/lib/secretsApi'; import { SecretBundleSheet } from './SecretBundleSheet'; import { SecretPushSheet } from './SecretPushSheet'; +import { FleetTabHeading, FleetEmptyState, FleetEmptyCard } from '../FleetEmptyState'; export function SecretsTab() { const [items, setItems] = useState([]); @@ -69,18 +70,29 @@ export function SecretsTab() { return (
-
-
-

Secret bundles

-

Centralized env-var bundles, encrypted at rest, versioned, pushed to labeled nodes.

-
- -
+ setCreating(true)} className="gap-1.5"> + New bundle + + } + /> {items.length === 0 ? ( - setCreating(true)} /> + + setCreating(true)} className="gap-1.5"> + Create your first bundle + + } + /> + ) : (
@@ -149,20 +161,3 @@ export function SecretsTab() { ); } - -function EmptyState({ onCreate }: { onCreate: () => void }) { - return ( -
- -
-

One source of truth for env

-

- Build a bundle of key=value pairs, push it to nodes by label, see exactly what changed before you write. -

-
- -
- ); -}