Canvas: Remove deployment action
Change-Id: I5887f130f5d11880271c943f58284d62f7d07a23
diff --git a/apps/canvas/back/prisma/migrations/20250515043851_state_string/migration.sql b/apps/canvas/back/prisma/migrations/20250515043851_state_string/migration.sql
new file mode 100644
index 0000000..b3b6605
--- /dev/null
+++ b/apps/canvas/back/prisma/migrations/20250515043851_state_string/migration.sql
@@ -0,0 +1,18 @@
+-- RedefineTables
+PRAGMA defer_foreign_keys=ON;
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_Project" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "userId" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "state" TEXT,
+ "draft" TEXT,
+ "instanceId" TEXT,
+ "deployKey" TEXT,
+ "githubToken" TEXT
+);
+INSERT INTO "new_Project" ("deployKey", "draft", "githubToken", "id", "instanceId", "name", "state", "userId") SELECT "deployKey", "draft", "githubToken", "id", "instanceId", "name", "state", "userId" FROM "Project";
+DROP TABLE "Project";
+ALTER TABLE "new_Project" RENAME TO "Project";
+PRAGMA foreign_keys=ON;
+PRAGMA defer_foreign_keys=OFF;
diff --git a/apps/canvas/back/prisma/schema.prisma b/apps/canvas/back/prisma/schema.prisma
index 8dd7ceb..09ddc69 100644
--- a/apps/canvas/back/prisma/schema.prisma
+++ b/apps/canvas/back/prisma/schema.prisma
@@ -17,8 +17,8 @@
id Int @id @default(autoincrement())
userId String
name String
- state Bytes?
- draft Bytes?
+ state String?
+ draft String?
instanceId String?
deployKey String?
githubToken String?
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 7c0fffe..cd8249d 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -68,7 +68,7 @@
userId: resp.locals.userId,
},
data: {
- draft: Buffer.from(JSON.stringify(req.body)),
+ draft: JSON.stringify(req.body),
},
});
resp.status(200);
@@ -171,12 +171,12 @@
}
};
-function extractGithubRepos(serializedState: Buffer | Uint8Array | null): string[] {
+function extractGithubRepos(serializedState: string | null): string[] {
if (!serializedState) {
return [];
}
try {
- const stateObj = JSON.parse(serializedState.toString());
+ const stateObj = JSON.parse(serializedState);
const githubNodes = stateObj.nodes.filter(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(n: any) => n.type === "github" && n.data?.repository?.id,
@@ -222,7 +222,7 @@
const handleDeploy: express.Handler = async (req, resp) => {
try {
const projectId = Number(req.params["projectId"]);
- const state = Buffer.from(JSON.stringify(req.body.state));
+ const state = JSON.stringify(req.body.state);
const p = await db.project.findUnique({
where: {
id: projectId,
@@ -339,6 +339,70 @@
}
};
+const handleRemoveDeployment: express.Handler = async (req, resp) => {
+ try {
+ const projectId = Number(req.params["projectId"]);
+ const p = await db.project.findUnique({
+ where: {
+ id: projectId,
+ userId: resp.locals.userId,
+ },
+ select: {
+ instanceId: true,
+ githubToken: true,
+ deployKey: true,
+ state: true,
+ draft: true,
+ },
+ });
+ if (p === null) {
+ resp.status(404);
+ resp.write(JSON.stringify({ error: "Project not found" }));
+ return;
+ }
+ if (p.instanceId == null) {
+ resp.status(400);
+ resp.write(JSON.stringify({ error: "Project not deployed" }));
+ return;
+ }
+ const removed = await appManager.removeInstance(p.instanceId);
+ if (!removed) {
+ resp.status(500);
+ resp.write(JSON.stringify({ error: "Failed to remove deployment from cluster" }));
+ return;
+ }
+ if (p.githubToken && p.deployKey && p.state) {
+ try {
+ const github = new GithubClient(p.githubToken);
+ const repos = extractGithubRepos(p.state);
+ const diff = { toDelete: repos, toAdd: [] };
+ await manageGithubKeys(github, p.deployKey, diff);
+ } catch (error) {
+ console.error("Error removing GitHub deploy keys:", error);
+ }
+ }
+ await db.project.update({
+ where: {
+ id: projectId,
+ },
+ data: {
+ instanceId: null,
+ deployKey: null,
+ state: null,
+ draft: p.draft ?? p.state,
+ },
+ });
+ resp.status(200);
+ resp.write(JSON.stringify({ success: true }));
+ } catch (e) {
+ console.error("Error removing deployment:", e);
+ resp.status(500);
+ resp.write(JSON.stringify({ error: "Internal server error" }));
+ } finally {
+ resp.end();
+ }
+};
+
const handleGithubRepos: express.Handler = async (req, resp) => {
try {
const projectId = Number(req.params["projectId"]);
@@ -603,6 +667,7 @@
app.get("/api/project/:projectId/env", handleEnv);
app.post("/api/project/:projectId/reload", handleReload);
app.get("/api/project/:projectId/logs/:service", handleServiceLogs);
+ app.post("/api/project/:projectId/remove-deployment", handleRemoveDeployment);
app.use("/", express.static("../front/dist"));
const api = express();
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index de04e9e..ec0d28a 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -84,7 +84,7 @@
"Content-Type": "application/json",
},
body: JSON.stringify({
- state: JSON.stringify(instance.toObject()),
+ state: instance.toObject(),
config,
}),
});
@@ -210,6 +210,44 @@
setReloading(false);
}
}, [projectId, toast]);
+ const removeDeployment = useCallback(async () => {
+ if (projectId == null) {
+ return;
+ }
+ if (!confirm("Are you sure you want to remove this deployment? This action cannot be undone.")) {
+ return;
+ }
+ setReloading(true);
+ try {
+ const resp = await fetch(`/api/project/${projectId}/remove-deployment`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (resp.ok) {
+ toast({
+ title: "Deployment removed successfully",
+ });
+ store.setMode("edit");
+ } else {
+ const errorData = await resp.json();
+ toast({
+ variant: "destructive",
+ title: "Failed to remove deployment",
+ description: errorData.error || "Unknown error",
+ });
+ }
+ } catch (e) {
+ console.log(e);
+ toast({
+ variant: "destructive",
+ title: "Failed to remove deployment",
+ });
+ } finally {
+ setReloading(false);
+ }
+ }, [projectId, toast, store]);
const [deployProps, setDeployProps] = useState({});
const [reloadProps, setReloadProps] = useState({});
useEffect(() => {
@@ -247,6 +285,13 @@
Reload Services
</DropdownMenuItem>
<DropdownMenuItem
+ onClick={removeDeployment}
+ disabled={projectId === undefined}
+ className="cursor-pointer hover:bg-gray-200"
+ >
+ Remove Deployment
+ </DropdownMenuItem>
+ <DropdownMenuItem
onClick={deleteProject}
disabled={projectId === undefined}
className="cursor-pointer hover:bg-gray-200"