feat(blueprints): add Fleet > Deployments tab UI, node labels, and docs (#861)

Implements the frontend layer for the Blueprint Model feature (backend
landed in PR #860). Fleet > Deployments tab is now live for Skipper+
users; Community users see the existing locked badge.

Key additions:
- blueprintsApi.ts: typed apiFetch wrappers (localOnly: true on all calls)
- BlueprintCatalog: featured hero, filter pills, classification-chipped tile grid
- BlueprintEditor: Monaco YAML editor with debounced live classification,
  label/node selector, three-mode drift radio cards, create/edit modes
- BlueprintDeploymentTable: per-node status rows with action buttons
  (Confirm deploy, Retry, Withdraw/Evict, DATA PINNED HERE for stateful)
- EvictionDialog: dual-affordance (Snapshot then evict / Evict and destroy)
- StateReviewDialog: fresh-deploy acceptance gate for stateful blueprints
- BlueprintClassificationBanner: real-time stateless/stateful/unknown banner
- DeploymentsTab: wires catalog, empty state, and create dialog
- BlueprintDetail: Sheet with Apply/Edit/overflow, themed delete dialog
- NodeLabelPicker + NodeLabelPill: label CRUD per node in NodeManager
- FleetView: gates Deployments tab behind isPaid; mounts DeploymentsTab
- NodeManager: Labels column with NodeLabelPicker (Skipper+ users)
- docs/features/blueprint-model.mdx + docs.json entry + 6 screenshots
This commit is contained in:
Anso
2026-05-01 19:47:05 -04:00
committed by GitHub
parent 685d5d729e
commit e5391e66cb
22 changed files with 2228 additions and 8 deletions
+14
View File
@@ -19,6 +19,9 @@ import { Plus, Trash2, Wifi, WifiOff, Star, Pencil, Monitor, Globe, Copy, KeyRou
import { formatTimeUntil, formatTimeAgo } from '@/lib/relativeTime';
import { SettingsPrimaryButton } from './settings/SettingsActions';
import { useMastheadStats } from './settings/MastheadStatsContext';
import { NodeLabelPicker } from './blueprints/NodeLabelPicker';
import { useLicense } from '@/context/LicenseContext';
import { useAuth } from '@/context/AuthContext';
interface NodeSchedulingSummary {
active_tasks: number;
@@ -60,6 +63,9 @@ const defaultFormData: NodeFormData = {
};
export function NodeManager() {
const { isPaid } = useLicense();
const { isAdmin } = useAuth();
const canEditLabels = isPaid && isAdmin;
const { nodes, refreshNodes } = useNodes();
useMastheadStats([
{ label: 'NODES', value: `${nodes.length}` },
@@ -497,6 +503,7 @@ export function NodeManager() {
<TableHead>Mode</TableHead>
<TableHead>Endpoint</TableHead>
<TableHead>Status</TableHead>
<TableHead>Labels</TableHead>
<TableHead>Schedules</TableHead>
<TableHead>Updates</TableHead>
<TableHead className="text-right">Actions</TableHead>
@@ -551,6 +558,13 @@ export function NodeManager() {
: (node.api_url || '-')}
</TableCell>
<TableCell>{getStatusBadge(node.status)}</TableCell>
<TableCell>
{isPaid ? (
<NodeLabelPicker nodeId={node.id} canEdit={canEditLabels} />
) : (
<span className="text-[10px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Upgrade</span>
)}
</TableCell>
<TableCell>
{(() => {
const summary = nodeSummary[node.id];