/** @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);