Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 85 additions & 16 deletions src/Terrabuild.UI/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const App = () => {
Record<string, { x: number; y: number }>
>({});
const [draggedNodeId, setDraggedNodeId] = useState<string | null>(null);
const [buildEndedAt, setBuildEndedAt] = useState<Date | null>(null);
const [nodes, setNodes] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const flowInstance = useRef<ReactFlowInstance | null>(null);
Expand All @@ -121,6 +122,7 @@ const App = () => {
null
);
const pendingBuildLogRef = useRef(false);
const lastApiNoticeRef = useRef(0);
const applyTerminalTheme = () => {
if (!terminal.current || !terminalReady.current) {
return;
Expand All @@ -138,6 +140,19 @@ const App = () => {
effectiveColorScheme === "dark" ? "#3b3f45" : "#c9d0d8",
};
};

const notifyApiUnavailable = () => {
const now = Date.now();
if (now - lastApiNoticeRef.current < 15000) {
return;
}
lastApiNoticeRef.current = now;
notifications.show({
color: "red",
title: "API unavailable",
message: "Unable to reach the Terrabuild API.",
});
};
const flushPendingTerminalActions = () => {
if (!terminalReady.current) {
return;
Expand Down Expand Up @@ -278,15 +293,23 @@ const App = () => {

useEffect(() => {
const load = async () => {
const [targetsRes, projectsRes] = await Promise.all([
fetch("/api/targets"),
fetch("/api/projects"),
]);
if (targetsRes.ok) {
setTargets(await targetsRes.json());
}
if (projectsRes.ok) {
setProjects(await projectsRes.json());
try {
const [targetsRes, projectsRes] = await Promise.all([
fetch("/api/targets"),
fetch("/api/projects"),
]);
if (targetsRes.ok) {
setTargets(await targetsRes.json());
} else {
notifyApiUnavailable();
}
if (projectsRes.ok) {
setProjects(await projectsRes.json());
} else {
notifyApiUnavailable();
}
} catch {
notifyApiUnavailable();
}
};
load().catch(() => null);
Expand Down Expand Up @@ -345,6 +368,7 @@ const App = () => {
fetchGraph().catch(() => {
setGraphError("Failed to load graph.");
setGraph(null);
notifyApiUnavailable();
});
}, [selectedTargets, selectedProjects, configuration, environment, engine]);

Expand Down Expand Up @@ -510,7 +534,14 @@ const App = () => {
const controller = new AbortController();
logAbort.current = controller;

const response = await fetch("/api/build/log", { signal: controller.signal });
let response: Response;
try {
response = await fetch("/api/build/log", { signal: controller.signal });
} catch {
notifyApiUnavailable();
setBuildRunning(false);
return;
}
if (!response.body) {
return;
}
Expand Down Expand Up @@ -632,6 +663,7 @@ const App = () => {
message: "Build command copied to clipboard.",
});
} catch {
notifyApiUnavailable();
notifications.show({
color: "red",
title: "Copy failed",
Expand All @@ -647,13 +679,22 @@ const App = () => {
setBuildRunning(true);
setBuildError(null);
const payload = buildPayload();
const response = await fetch("/api/build", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
let response: Response;
try {
response = await fetch("/api/build", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
} catch {
setBuildRunning(false);
setBuildEndedAt(null);
notifyApiUnavailable();
return;
}
if (!response.ok) {
setBuildRunning(false);
setBuildEndedAt(null);
const message = await response.text();
setBuildError(message);
notifications.show({
Expand All @@ -663,13 +704,15 @@ const App = () => {
});
return;
}
setBuildEndedAt(null);
notifications.show({
color: "blue",
title: "Build started",
message: "Build is running.",
});
startLogStream().catch(() => {
setBuildRunning(false);
notifyApiUnavailable();
notifications.show({
color: "red",
title: "Build failed",
Expand Down Expand Up @@ -708,7 +751,9 @@ const App = () => {
setProjectStatus(statusMap);
}
};
refresh().catch(() => null);
refresh().catch(() => {
notifyApiUnavailable();
});
}, [
buildRunning,
selectedTargets,
Expand Down Expand Up @@ -749,6 +794,12 @@ const App = () => {
if (!terminal.current) {
return;
}
const summary = nodeResults[key];
if (summary?.endedAt) {
setBuildEndedAt(new Date(summary.endedAt));
} else {
setBuildEndedAt(null);
}
setSelectedTargetKey(key);
terminal.current.reset();
setShowTerminal(true);
Expand Down Expand Up @@ -779,6 +830,16 @@ const App = () => {
await loadTargetLog(key, target);
};

useEffect(() => {
if (!selectedTargetKey) {
return;
}
const summary = nodeResults[selectedTargetKey];
if (summary?.endedAt) {
setBuildEndedAt(new Date(summary.endedAt));
}
}, [selectedTargetKey, nodeResults]);

const handleNodesChange: OnNodesChange = (changes) => {
setNodes((current) => {
const updated = applyNodeChanges(changes, current);
Expand All @@ -796,6 +857,13 @@ const App = () => {

const terminalBackground =
effectiveColorScheme === "dark" ? theme.colors.dark[7] : theme.white;
const buildLogTitle = buildEndedAt
? `Build Log ${buildEndedAt
.toISOString()
.replace("T", " ")
.replace("Z", "")
.slice(0, 19)}`
: "Build Log";

return (
<AppShell
Expand Down Expand Up @@ -917,6 +985,7 @@ const App = () => {
<BuildLogPanel
showTerminal={showTerminal}
buildRunning={buildRunning}
title={buildLogTitle}
onHide={() => setShowTerminal(false)}
terminalRef={terminalRef}
background={terminalBackground}
Expand Down
4 changes: 3 additions & 1 deletion src/Terrabuild.UI/src/components/BuildLogPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RefObject } from "react";
type BuildLogPanelProps = {
showTerminal: boolean;
buildRunning: boolean;
title: string;
onHide: () => void;
terminalRef: RefObject<HTMLDivElement | null>;
background: string;
Expand All @@ -13,6 +14,7 @@ type BuildLogPanelProps = {
const BuildLogPanel = ({
showTerminal,
buildRunning,
title,
onHide,
terminalRef,
background,
Expand All @@ -35,7 +37,7 @@ const BuildLogPanel = ({
>
{showTerminal && (
<Group justify="space-between" align="center" mb="sm">
<Title order={4}>Build Log</Title>
<Title order={4}>{title}</Title>
<Group spacing="xs" align="center" justify="flex-end">
<Badge color={buildRunning ? "orange" : "gray"}>
{buildRunning ? "Live" : "Idle"}
Expand Down
12 changes: 10 additions & 2 deletions src/Terrabuild/Core/Logs.fs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ let dumpLogs (logId: Guid) (options: ConfigOptions.Options) (cache: ICache) (gra
let dumpTerminal (nodes: GraphDef.Node seq) =
let dumpTerminal (node: GraphDef.Node) =
let label = $"{node.Target} {node.ProjectDir}"
let formatEndedAt (value: System.DateTime) =
value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture)

let getHeaderFooter success =
let color =
Expand All @@ -164,7 +166,9 @@ let dumpLogs (logId: Guid) (options: ConfigOptions.Options) (cache: ICache) (gra
let dumpLogs () =
summary.Operations |> Seq.iter (fun group ->
group |> Seq.iter (fun step ->
$"{Ansi.Styles.yellow}{step.MetaCommand} {Ansi.Styles.dimwhite}(exit code {step.ExitCode}){Ansi.Styles.reset}" |> Terminal.writeLine
let endedAt = formatEndedAt step.EndedAt
$"{Ansi.Styles.yellow}{step.MetaCommand} {Ansi.Styles.dimwhite}(exit code {step.ExitCode} - {endedAt}){Ansi.Styles.reset}"
|> Terminal.writeLine
if options.Debug then
$"{Ansi.Styles.cyan}{step.Command} {step.Arguments}{Ansi.Styles.reset}" |> Terminal.writeLine
step.Log |> IO.readTextFile |> Terminal.write
Expand All @@ -189,6 +193,8 @@ let dumpLogs (logId: Guid) (options: ConfigOptions.Options) (cache: ICache) (gra
let label = $"{node.Target} {node.ProjectDir}"
let cacheEntryId = GraphDef.buildCacheKey node
let summary = cache.TryGetSummaryOnly false cacheEntryId
let formatEndedAt (value: System.DateTime) =
value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture)

match summary with
| Some (_, summary) ->
Expand All @@ -198,7 +204,9 @@ let dumpLogs (logId: Guid) (options: ConfigOptions.Options) (cache: ICache) (gra
| Some command ->
match command |> List.tryLast with
| Some operation ->
$"::group::{operation.MetaCommand}" |> Terminal.writeLine
let endedAt = formatEndedAt operation.EndedAt
$"::group::{operation.MetaCommand} (exit code {operation.ExitCode} - {endedAt})"
|> Terminal.writeLine
operation.Log |> IO.readTextFile |> Terminal.write
$"::endgroup::" |> Terminal.writeLine
| _ -> ()
Expand Down
12 changes: 7 additions & 5 deletions src/Terrabuild/Helpers/Exec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,18 @@ let execCaptureTimestampedOutput (workingDir: string) (command: string) (args: s
try
use logWriter = new StreamWriter(logFile)
let writeLock = Lock()

let inline lockWrite (from: string) (msg: string | null) =
let inline lockWrite (msg: string | null) =
match msg with
| NonNull msg -> lock writeLock (fun () -> logWriter.WriteLine($"{DateTime.UtcNow} {from} {msg}"))
| NonNull msg ->
lock writeLock (fun () ->
logWriter.WriteLine(msg)
)
| _ -> ()

Log.Debug("Running and capturing timestamped output of '{Command}' with arguments '{Args}' in working dir '{WorkingDir}'", command, args, workingDir)
use proc = createProcess workingDir command args envs true
proc.OutputDataReceived.Add(fun e -> lockWrite "OUT" e.Data)
proc.ErrorDataReceived.Add(fun e -> lockWrite "ERR" e.Data)
proc.OutputDataReceived.Add(fun e -> lockWrite e.Data)
proc.ErrorDataReceived.Add(fun e -> lockWrite e.Data)
proc.BeginOutputReadLine()
proc.BeginErrorReadLine()
proc.WaitForExit()
Expand Down