| 1 | import { useEffect, useRef, useState } from "react"; |
| 2 | import { Line, sleep, TermChrome, useSpinner } from "./_terminal"; |
| 3 | |
| 4 | const RESOURCES = [ |
| 5 | { id: "Bucket", type: "Cloudflare.R2.Bucket" }, |
| 6 | { id: "Worker", type: "Cloudflare.Worker" }, |
| 7 | ]; |
| 8 | |
| 9 | const OUTPUTS: { key: string; value: string }[] = [ |
| 10 | { key: "bucketName", value: '"myapp-dev_sam-bucket-a3f1"' }, |
| 11 | { |
| 12 | key: "url", |
| 13 | value: '"https://myapp-worker-dev_sam-abc123.workers.dev"', |
| 14 | }, |
| 15 | ]; |
| 16 | |
| 17 | const STAGE = "dev_sam"; |
| 18 | const COMMAND = `alchemy deploy --stage ${STAGE}`; |
| 19 | |
| 20 | type RowStatus = "pending" | "creating" | "created"; |
| 21 | |
| 22 | export default function StackOutputsTerminal({ |
| 23 | maxLines = 12, |
| 24 | }: { |
| 25 | maxLines?: number; |
| 26 | }) { |
| 27 | const [cmd, setCmd] = useState(""); |
| 28 | const [caret, setCaret] = useState(false); |
| 29 | const [rows, setRows] = useState< |
| 30 | { id: string; type: string; status: RowStatus }[] |
| 31 | >([]); |
| 32 | const [showOutputs, setShowOutputs] = useState(false); |
| 33 | const cancelRef = useRef(false); |
| 34 | |
| 35 | useEffect(() => { |
| 36 | cancelRef.current = false; |
| 37 | const aborted = () => cancelRef.current; |
| 38 | |
| 39 | const typeCmd = async (text: string) => { |
| 40 | setCmd(""); |
| 41 | setCaret(true); |
| 42 | for (let i = 1; i <= text.length; i++) { |
| 43 | if (aborted()) return; |
| 44 | setCmd(text.slice(0, i)); |
| 45 | await sleep(28 + Math.random() * 22); |
| 46 | } |
| 47 | await sleep(160); |
| 48 | setCaret(false); |
| 49 | }; |
| 50 | |
| 51 | const update = (id: string, status: RowStatus) => |
| 52 | setRows((rs) => rs.map((r) => (r.id === id ? { ...r, status } : r))); |
| 53 | |
| 54 | const run = async () => { |
| 55 | while (!aborted()) { |
| 56 | setCmd(""); |
| 57 | setCaret(false); |
| 58 | setRows([]); |
| 59 | setShowOutputs(false); |
| 60 | await sleep(900); |
| 61 | if (aborted()) return; |
| 62 | |
| 63 | await typeCmd(COMMAND); |
| 64 | if (aborted()) return; |
| 65 | |
| 66 | await sleep(220); |
| 67 | setRows( |
| 68 | RESOURCES.map((r) => ({ ...r, status: "pending" as RowStatus })), |
| 69 | ); |
| 70 | await sleep(260); |
| 71 | |
| 72 | for (const r of RESOURCES) { |
| 73 | if (aborted()) return; |
| 74 | update(r.id, "creating"); |
| 75 | await sleep(620); |
| 76 | if (aborted()) return; |
| 77 | update(r.id, "created"); |
| 78 | await sleep(140); |
| 79 | } |
| 80 | |
| 81 | await sleep(260); |
| 82 | setShowOutputs(true); |
| 83 | await sleep(4200); |
| 84 | } |
| 85 | }; |
| 86 | |
| 87 | run(); |
| 88 | return () => { |
| 89 | cancelRef.current = true; |
| 90 | }; |
| 91 | }, []); |
| 92 | |
| 93 | const anyInFlight = rows.some((r) => r.status === "creating"); |
| 94 | const spinner = useSpinner(anyInFlight); |
| 95 | const accent = "var(--alc-accent-bright)"; |
| 96 | |
| 97 | return ( |
| 98 | <TermChrome |
| 99 | title={`alchemy · ${STAGE}`} |
| 100 | badge="DEPLOY" |
| 101 | badgeColor={accent} |
| 102 | maxLines={maxLines} |
| 103 | > |
| 104 | <Line> |
| 105 | <span style={{ color: accent }}>$ </span> |
| 106 | {cmd} |
| 107 | {caret && <span style={{ color: "var(--alc-fg-invert)" }}>▍</span>} |
| 108 | </Line> |
| 109 | {rows.length > 0 && ( |
| 110 | <> |
| 111 | <Line> </Line> |
| 112 | {rows.map((r) => { |
| 113 | const inFlight = r.status === "creating"; |
| 114 | const done = r.status === "created"; |
| 115 | const icon = inFlight ? spinner : done ? "✓" : "+"; |
| 116 | return ( |
| 117 | <Line key={r.id}> |
| 118 | <span |
| 119 | style={{ |
| 120 | color: accent, |
| 121 | width: "1.2em", |
| 122 | display: "inline-block", |
| 123 | }} |
| 124 | > |
| 125 | {icon} |
| 126 | </span> |
| 127 | <span |
| 128 | style={{ color: "var(--alc-fg-invert)", fontWeight: 600 }} |
| 129 | > |
| 130 | {r.id} |
| 131 | </span> |
| 132 | <span style={{ color: "var(--alc-code-comment)" }}> |
| 133 | {` (${r.type})`} |
| 134 | </span> |
| 135 | {inFlight && ( |
| 136 | <span style={{ color: accent, marginLeft: 6 }}> |
| 137 | {r.status} |
| 138 | </span> |
| 139 | )} |
| 140 | </Line> |
| 141 | ); |
| 142 | })} |
| 143 | </> |
| 144 | )} |
| 145 | {showOutputs && ( |
| 146 | <> |
| 147 | <Line> </Line> |
| 148 | <Line> |
| 149 | <span style={{ color: accent }}>✓ </span> |
| 150 | <span>deployed in </span> |
| 151 | <span style={{ color: "var(--alc-fg-invert)", fontWeight: 600 }}> |
| 152 | 3.4s |
| 153 | </span> |
| 154 | </Line> |
| 155 | <Line> </Line> |
| 156 | <Line> |
| 157 | <span style={{ color: "var(--alc-code-comment)" }}>outputs:</span> |
| 158 | </Line> |
| 159 | <Line> |
| 160 | <span style={{ color: "var(--alc-fg-invert)" }}>{"{"}</span> |
| 161 | </Line> |
| 162 | {OUTPUTS.map((o) => ( |
| 163 | <Line key={o.key}> |
| 164 | <span style={{ color: "var(--alc-fg-invert)" }}>{" "}</span> |
| 165 | <span style={{ color: "var(--alc-fg-invert)" }}>{o.key}</span> |
| 166 | <span style={{ color: "var(--alc-code-comment)" }}>: </span> |
| 167 | <span style={{ color: accent }}>{o.value}</span> |
| 168 | <span style={{ color: "var(--alc-fg-invert)" }}>,</span> |
| 169 | </Line> |
| 170 | ))} |
| 171 | <Line> |
| 172 | <span style={{ color: "var(--alc-fg-invert)" }}>{"}"}</span> |
| 173 | </Line> |
| 174 | </> |
| 175 | )} |
| 176 | </TermChrome> |
| 177 | ); |
| 178 | } |