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
1 change: 1 addition & 0 deletions src/Terrabuild.UI/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@emotion/styled": "^11.11.5",
"@mantine/core": "^6.0.0",
"@mantine/hooks": "^6.0.0",
"@mantine/notifications": "^6.0.0",
"@tabler/icons-react": "^3.19.0",
"dagre": "^0.8.5",
"react": "^18.3.1",
Expand Down
58 changes: 58 additions & 0 deletions src/Terrabuild.UI/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

143 changes: 129 additions & 14 deletions src/Terrabuild.UI/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
useMantineTheme,
useMantineColorScheme,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import ReactFlow, {
Background,
Controls,
Expand All @@ -36,6 +37,7 @@ import { FitAddon } from "xterm-addon-fit";
import "xterm/css/xterm.css";
import {
IconAffiliate,
IconCopy,
IconMoon,
IconSun,
IconSquareRoundedChevronDown,
Expand Down Expand Up @@ -483,36 +485,136 @@ const App = () => {
terminal.current.write(chunk);
}
}
try {
const params = new URLSearchParams();
selectedTargets.forEach((target) => params.append("targets", target));
selectedProjects.forEach((project) => params.append("projects", project));
const statusResponse = await fetch(
`/api/build/status?${params.toString()}`
);
if (statusResponse.ok) {
const statusData = (await statusResponse.json()) as ProjectStatus[];
const hasFailure = statusData.some((item) => item.status === "failed");
notifications.show({
color: hasFailure ? "red" : "green",
title: hasFailure ? "Build completed with failures" : "Build completed",
message: hasFailure
? "One or more targets failed."
: "All targets completed successfully.",
});
} else {
notifications.show({
color: "yellow",
title: "Build completed",
message: "Unable to fetch build status.",
});
}
} catch {
notifications.show({
color: "yellow",
title: "Build completed",
message: "Unable to fetch build status.",
});
}
setBuildRunning(false);
};

const startBuild = async () => {
if (selectedTargets.length === 0 || buildRunning) {
return;
}
setBuildRunning(true);
setBuildError(null);
const buildPayload = () => {
const parallel =
parallelism.trim().length > 0 ? Number(parallelism) : null;
const payload = {
return {
targets: selectedTargets,
projects: selectedProjects,
parallelism: parallel && parallel > 0 ? parallel : undefined,
force: forceBuild,
retry: retryBuild,
};
};

const copyTextToClipboard = async (value: string) => {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(value);
return;
}
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
};

const copyBuildCommand = async () => {
if (selectedTargets.length === 0) {
return;
}
try {
const response = await fetch("/api/build/command", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(buildPayload()),
});
if (!response.ok) {
notifications.show({
color: "red",
title: "Copy failed",
message: "Failed to generate build command.",
});
return;
}
const command = await response.text();
await copyTextToClipboard(command);
notifications.show({
color: "green",
title: "Copied",
message: "Build command copied to clipboard.",
});
} catch {
notifications.show({
color: "red",
title: "Copy failed",
message: "Failed to copy build command.",
});
}
};

const startBuild = async () => {
if (selectedTargets.length === 0 || buildRunning) {
return;
}
setBuildRunning(true);
setBuildError(null);
const payload = buildPayload();
const response = await fetch("/api/build", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
setBuildRunning(false);
setBuildError(await response.text());
const message = await response.text();
setBuildError(message);
notifications.show({
color: "red",
title: "Build failed to start",
message,
});
return;
}
notifications.show({
color: "blue",
title: "Build started",
message: "Build is running.",
});
startLogStream().catch(() => {
setBuildRunning(false);
notifications.show({
color: "red",
title: "Build failed",
message: "Build log stream failed.",
});
});
};

Expand Down Expand Up @@ -744,18 +846,31 @@ const App = () => {
}}
/>

<Button
onClick={startBuild}
disabled={buildRunning || selectedTargets.length === 0}
>
{buildRunning ? "Building..." : "Build"}
</Button>
<Group spacing="xs" noWrap>
<Button
onClick={startBuild}
disabled={buildRunning || selectedTargets.length === 0}
style={{ flex: 1 }}
>
{buildRunning ? "Building..." : "Build"}
</Button>
<ActionIcon
size="lg"
variant="light"
onClick={copyBuildCommand}
disabled={selectedTargets.length === 0}
aria-label="Copy build command"
>
<IconCopy size={18} />
</ActionIcon>
</Group>

{buildError && (
<Text size="sm" c="red">
{buildError}
</Text>
)}

</Stack>
</Paper>

Expand Down
18 changes: 17 additions & 1 deletion src/Terrabuild.UI/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import { useLocalStorage } from "@mantine/hooks";
import App from "./App";
import "./styles.css";
Expand All @@ -15,6 +16,20 @@ const Root = () => {
const toggleColorScheme = (value?: ColorScheme) =>
setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"));

const notificationTheme = {
colorScheme,
components: {
Notification: {
styles: (theme: any) => ({
root: {
border: `1px solid ${theme.colors.gray[3]}`,
borderRadius: 8,
},
}),
},
},
};

return (
<ColorSchemeProvider
colorScheme={colorScheme}
Expand All @@ -23,8 +38,9 @@ const Root = () => {
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{ colorScheme }}
theme={notificationTheme}
>
<Notifications position="top-right" />
<App />
</MantineProvider>
</ColorSchemeProvider>
Expand Down
Loading