180 lines
4.5 KiB
JavaScript
180 lines
4.5 KiB
JavaScript
import React, { useEffect, useReducer } from "react";
|
|
import { Box, Text } from "ink";
|
|
import Spinner from "ink-spinner";
|
|
import SelectInput from "ink-select-input";
|
|
|
|
import { startRun } from "../lib/runner.js";
|
|
|
|
const MAX_LOG_LINES = 12;
|
|
|
|
function reducer(state, action) {
|
|
switch (action.type) {
|
|
case "step-start":
|
|
return {
|
|
...state,
|
|
currentIndex: action.index,
|
|
statuses: { ...state.statuses, [action.index]: "running" },
|
|
};
|
|
case "step-done":
|
|
return {
|
|
...state,
|
|
statuses: { ...state.statuses, [action.index]: action.status },
|
|
errors:
|
|
action.error != null
|
|
? { ...state.errors, [action.index]: action.error }
|
|
: state.errors,
|
|
};
|
|
case "log": {
|
|
const next = [...state.log, action];
|
|
if (next.length > MAX_LOG_LINES * 4) {
|
|
next.splice(0, next.length - MAX_LOG_LINES * 4);
|
|
}
|
|
return { ...state, log: next };
|
|
}
|
|
case "done":
|
|
return { ...state, finished: true, success: action.success, cancelled: !!action.cancelled };
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
export default function RunLog({ nav, plan, origin }) {
|
|
const [state, dispatch] = useReducer(reducer, {
|
|
statuses: {},
|
|
errors: {},
|
|
currentIndex: -1,
|
|
log: [],
|
|
finished: false,
|
|
success: false,
|
|
cancelled: false,
|
|
});
|
|
|
|
useEffect(() => {
|
|
const run = startRun(plan.steps);
|
|
run.on("event", (event) => {
|
|
dispatch(event);
|
|
});
|
|
return () => run.cancel();
|
|
}, [plan]);
|
|
|
|
const recentLog = state.log.slice(-MAX_LOG_LINES);
|
|
|
|
return (
|
|
<Box flexDirection="column">
|
|
<Text bold>{plan.title}</Text>
|
|
<Box flexDirection="column" marginTop={1}>
|
|
{plan.steps.map((step, index) => (
|
|
<StepRow
|
|
key={index}
|
|
index={index}
|
|
step={step}
|
|
status={state.statuses[index]}
|
|
error={state.errors[index]}
|
|
/>
|
|
))}
|
|
</Box>
|
|
{recentLog.length > 0 ? (
|
|
<Box flexDirection="column" marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
{recentLog.map((entry, i) => (
|
|
<Text key={i} color={entry.stream === "stderr" ? "yellow" : undefined} dimColor>
|
|
{entry.text}
|
|
</Text>
|
|
))}
|
|
</Box>
|
|
) : null}
|
|
{state.finished ? <FinishedFooter state={state} nav={nav} origin={origin} /> : (
|
|
<Box marginTop={1}>
|
|
<Text color="cyan">
|
|
<Spinner type="dots" />
|
|
</Text>
|
|
<Text> running…</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function StepRow({ index, step, status, error }) {
|
|
const symbol = symbolFor(status);
|
|
const color = colorFor(status);
|
|
return (
|
|
<Box>
|
|
<Box width={3}>
|
|
<Text color={color}>{symbol}</Text>
|
|
</Box>
|
|
<Box width={3}>
|
|
<Text dimColor>{`${index + 1}.`}</Text>
|
|
</Box>
|
|
<Box flexDirection="column">
|
|
<Text>{step.label}</Text>
|
|
{error ? <Text color="red"> {error}</Text> : null}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function symbolFor(status) {
|
|
switch (status) {
|
|
case "running":
|
|
return "◐";
|
|
case "ok":
|
|
return "✓";
|
|
case "failed":
|
|
return "✗";
|
|
case "cancelled":
|
|
return "⊘";
|
|
default:
|
|
return "·";
|
|
}
|
|
}
|
|
|
|
function colorFor(status) {
|
|
switch (status) {
|
|
case "running":
|
|
return "cyan";
|
|
case "ok":
|
|
return "green";
|
|
case "failed":
|
|
return "red";
|
|
case "cancelled":
|
|
return "yellow";
|
|
default:
|
|
return "gray";
|
|
}
|
|
}
|
|
|
|
function FinishedFooter({ state, nav, origin }) {
|
|
const items = [
|
|
{ label: "Back to main menu", value: "home" },
|
|
...(origin === "frp" ? [{ label: "Back to FRP menu", value: "frp" }] : []),
|
|
{ label: "Quit", value: "quit" },
|
|
];
|
|
|
|
const handleSelect = (item) => {
|
|
if (item.value === "home") {
|
|
nav.home();
|
|
} else if (item.value === "frp") {
|
|
nav.replace({ name: "frp" });
|
|
// collapse stack down to just main + frp
|
|
// (replace just swaps top; we want clean stack)
|
|
} else if (item.value === "quit") {
|
|
nav.exit();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box flexDirection="column" marginTop={1}>
|
|
{state.cancelled ? (
|
|
<Text color="yellow">Cancelled.</Text>
|
|
) : state.success ? (
|
|
<Text color="green">All steps completed successfully.</Text>
|
|
) : (
|
|
<Text color="red">Run failed.</Text>
|
|
)}
|
|
<Box marginTop={1}>
|
|
<SelectInput items={items} onSelect={handleSelect} />
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|