mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-24 08:57:25 +00:00
e5391e66cb
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
49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
const HUE_VARS = [
|
||
'teal', 'blue', 'purple', 'rose', 'amber',
|
||
'green', 'orange', 'pink', 'cyan', 'slate',
|
||
] as const;
|
||
|
||
type Hue = typeof HUE_VARS[number];
|
||
|
||
function hashLabel(label: string): Hue {
|
||
let h = 0;
|
||
for (let i = 0; i < label.length; i += 1) {
|
||
h = (h * 31 + label.charCodeAt(i)) | 0;
|
||
}
|
||
return HUE_VARS[Math.abs(h) % HUE_VARS.length];
|
||
}
|
||
|
||
interface NodeLabelPillProps {
|
||
label: string;
|
||
onRemove?: () => void;
|
||
size?: 'sm' | 'md';
|
||
}
|
||
|
||
export function NodeLabelPill({ label, onRemove, size = 'md' }: NodeLabelPillProps) {
|
||
const hue = hashLabel(label);
|
||
const sizeClasses = size === 'sm' ? 'text-[10px] px-1.5 py-0' : 'text-[11px] px-2 py-0.5';
|
||
return (
|
||
<span
|
||
className={`inline-flex items-center gap-1 rounded-md border font-mono ${sizeClasses}`}
|
||
style={{
|
||
backgroundColor: `var(--label-${hue}-bg)`,
|
||
color: `var(--label-${hue})`,
|
||
borderColor: `color-mix(in oklch, var(--label-${hue}) 30%, transparent)`,
|
||
}}
|
||
>
|
||
{label}
|
||
{onRemove && (
|
||
<button
|
||
type="button"
|
||
onClick={(e) => { e.stopPropagation(); onRemove(); }}
|
||
className="opacity-60 hover:opacity-100 ml-0.5 cursor-pointer"
|
||
aria-label={`Remove ${label}`}
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</span>
|
||
);
|
||
}
|
||
|