/** @jsxImportSource react */ import { useMemo, type JSX } from "react"; import { Box, Text } from "ink"; import type { Plan as AlchemyPlan, BindingAction, CRUD, ActionApply, ActionDelete, } from "../../../Plan.ts"; import { buildNamespaceTree, flattenTree, type DerivedAction, type ActionVerb, } from "../../NamespaceTree.ts"; import { formatModeNote } from "../../ModeTag.ts"; export interface PlanProps { plan: AlchemyPlan; } export function Plan({ plan }: PlanProps): JSX.Element { const items = useMemo( () => [ ...Object.values(plan.resources), ...Object.values(plan.deletions), ] as CRUD[], [plan], ); const taskItems = useMemo( () => [ ...Object.values(plan.actions ?? {}), ...Object.values(plan.actionDeletions ?? {}), ].filter((t): t is ActionApply | ActionDelete => t !== undefined), [plan], ); const flatItems = useMemo(() => { const tree = buildNamespaceTree(items, taskItems); return flattenTree(tree); }, [items, taskItems]); if (items.length === 0 && taskItems.length === 0) { return No changes planned; } const counts = items.reduce((acc, item) => (acc[item.action]++, acc), { create: 0, update: 0, delete: 0, noop: 0, replace: 0, }); const taskCounts = taskItems.reduce( (acc, item) => (acc[item.action]++, acc), { run: 0, noop: 0, delete: 0 }, ); const actions = (["create", "update", "delete", "replace"] as const).filter( (action) => counts[action] > 0, ); const taskHeaderEntries = [ taskCounts.run > 0 ? `${taskCounts.run} to run` : undefined, taskCounts.delete > 0 ? `${taskCounts.delete} to drop` : undefined, ].filter((s): s is string => !!s); return ( Plan : {[ ...actions.map((action) => { const count = counts[action]; const color = actionColor(action); return ( {count} to {action} ); }), ...taskHeaderEntries.map((label, i) => ( {label} )), ].flatMap((box, i, arr) => i === arr.length - 1 ? [box] : [box, | ], )} {flatItems.map((item) => { const indent = " ".repeat(item.depth); const color = getActionColor(item.action); const icon = getActionIcon(item.action); const key = item.path.join("/"); if (item.type === "namespace") { return ( {indent} {icon} {item.id} ); } if (item.type === "binding") { return ( {indent} {icon} {item.bindingSid} ); } if (item.type === "action") { return ( {indent} {icon} {item.id} ({item.actionType}) [action] ); } // Resource item const modeNote = formatModeNote({ mode: item.providerMode, priorMode: item.fromProviderMode, defaultMode: plan.defaultMode, }); return ( {indent} {icon} {item.id} ({item.resourceType}) {modeNote && ( ({modeNote}) )} {item.bindingCount !== undefined && item.bindingCount > 0 && ( ({item.bindingCount} bindings) )} ); })} ); } type Color = Parameters[0]["color"]; type AnyAction = CRUD["action"] | BindingAction | DerivedAction | ActionVerb; const getActionColor = (action: AnyAction): Color => ({ noop: "gray", create: "green", update: "yellow", delete: "red", replace: "magenta", mixed: "cyan", run: "cyan", })[action] ?? "gray"; const getActionIcon = (action: AnyAction): string => ({ create: "+", update: "~", delete: "-", noop: "•", replace: "!", mixed: "*", run: "λ", })[action] ?? "?"; const actionColor = (action: CRUD["action"]): Color => getActionColor(action);