Canvas: Add option to deploy latest draft
Change-Id: Ia74c64354e1f80ec794140ce406fb21c16feb0da
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 9456d17..fb77431 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -16,6 +16,7 @@
Env,
generateDodoConfig,
ConfigSchema,
+ Config,
ConfigWithInput,
configToGraph,
Network,
@@ -24,7 +25,7 @@
} from "config";
import { Instant, DateTimeFormatter, ZoneId } from "@js-joda/core";
import LogStore from "./log.js";
-import { GraphOrConfigSchema, GraphSchema } from "config/dist/graph.js";
+import { GraphOrConfigSchema, GraphSchema, GraphConfigOrDraft } from "config/dist/graph.js";
async function generateKey(root: string): Promise<[string, string]> {
const privKeyPath = path.join(root, "key");
@@ -167,10 +168,6 @@
};
}
-const projectDeleteReqSchema = z.object({
- state: z.optional(z.nullable(z.string())),
-});
-
const handleProjectDelete: express.Handler = async (req, resp) => {
try {
const projectId = Number(req.params["projectId"]);
@@ -191,20 +188,8 @@
resp.status(404);
return;
}
- const parseResult = projectDeleteReqSchema.safeParse(req.body);
- if (!parseResult.success) {
- resp.status(400);
- resp.write(JSON.stringify({ error: "Invalid request body", issues: parseResult.error.format() }));
- return;
- }
if (p.githubToken && p.deployKeyPublic) {
- const allRepos = [
- ...new Set([
- ...extractGithubRepos(p.state),
- ...extractGithubRepos(p.draft),
- ...extractGithubRepos(parseResult.data.state),
- ]),
- ];
+ const allRepos = [...new Set([...extractGithubRepos(p.state), ...extractGithubRepos(p.draft)])];
if (allRepos.length > 0) {
const diff: RepoDiff = { toDelete: allRepos, toAdd: [] };
const github = new GithubClient(p.githubToken);
@@ -300,6 +285,12 @@
const handleDeploy: express.Handler = async (req, resp) => {
try {
+ const reqParsed = GraphConfigOrDraft.safeParse(req.body);
+ if (!reqParsed.success) {
+ resp.status(400);
+ resp.write(JSON.stringify({ error: "Invalid request body", issues: reqParsed.error.format() }));
+ return;
+ }
const projectId = Number(req.params["projectId"]);
const p = await db.project.findUnique({
where: {
@@ -312,6 +303,7 @@
deployKey: true,
deployKeyPublic: true,
state: true,
+ draft: true,
geminiApiKey: true,
anthropicApiKey: true,
},
@@ -320,33 +312,54 @@
resp.status(404);
return;
}
- const config = ConfigSchema.safeParse(req.body.config);
- if (!config.success) {
- resp.status(400);
- resp.write(JSON.stringify({ error: "Invalid configuration", issues: config.error.format() }));
+ let graph: Graph | null = null;
+ let config: Config | null = null;
+ if (reqParsed.data.type === "config") {
+ const parsed = ConfigSchema.safeParse(reqParsed.data.config);
+ if (parsed.success) {
+ config = parsed.data;
+ } else {
+ resp.status(400);
+ resp.write(JSON.stringify({ error: "Invalid configuration", issues: parsed.error.format() }));
+ return;
+ }
+ let oldGraph: Graph | undefined = undefined;
+ if (p.state != null) {
+ oldGraph = parseGraph(p.state)!.data;
+ } else if (p.draft != null) {
+ oldGraph = parseGraph(p.draft)!.data;
+ }
+ let repos: GithubRepository[] = [];
+ if (p.githubToken) {
+ const github = new GithubClient(p.githubToken);
+ repos = await github.getRepositories();
+ }
+ graph = configToGraph(config, getNetworks(resp.locals.username), repos, oldGraph);
+ } else if (reqParsed.data.type === "graph") {
+ graph = reqParsed.data.graph;
+ const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
+ config = generateDodoConfig(projectId.toString(), graph?.nodes || [], env);
+ } else if (reqParsed.data.type === "draft") {
+ if (p.draft == null) {
+ resp.status(400);
+ resp.write(JSON.stringify({ error: "Invalid request body" }));
+ return;
+ }
+ graph = parseGraph(p.draft)!.data!;
+ const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
+ config = generateDodoConfig(projectId.toString(), graph?.nodes || [], env);
+ }
+ if (config == null || graph == null) {
+ resp.status(500);
+ resp.write(JSON.stringify({ error: "Failed to generate configuration" }));
return;
}
- let repos: GithubRepository[] = [];
- if (p.githubToken) {
- const github = new GithubClient(p.githubToken);
- repos = await github.getRepositories();
- }
- const state = req.body.state
- ? JSON.stringify(req.body.state)
- : JSON.stringify(
- configToGraph(
- config.data,
- getNetworks(resp.locals.username),
- repos,
- p.state ? parseGraph(p.state)!.data! : undefined,
- ),
- );
await db.project.update({
where: {
id: projectId,
},
data: {
- draft: state,
+ draft: JSON.stringify(graph),
},
});
let deployKey: string | null = p.deployKey;
@@ -360,7 +373,7 @@
}
let diff: RepoDiff | null = null;
const cfg: ConfigWithInput = {
- ...config.data,
+ ...config,
input: {
appId: projectId.toString(),
managerAddr: env.INTERNAL_API_ADDR!,
@@ -380,22 +393,22 @@
id: projectId,
},
data: {
- state,
+ state: JSON.stringify(graph),
draft: null,
instanceId: deployResponse.id,
access: JSON.stringify(deployResponse.access),
},
});
- diff = { toAdd: extractGithubRepos(state) };
+ diff = { toAdd: extractGithubRepos(JSON.stringify(graph)) };
} else {
const deployResponse = await appManager.update(p.instanceId, cfg);
- diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(state));
+ diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(JSON.stringify(graph)));
await db.project.update({
where: {
id: projectId,
},
data: {
- state,
+ state: JSON.stringify(graph),
draft: null,
access: JSON.stringify(deployResponse.access),
},
diff --git a/apps/canvas/config/src/graph.ts b/apps/canvas/config/src/graph.ts
index c2f8e05..a4634cb 100644
--- a/apps/canvas/config/src/graph.ts
+++ b/apps/canvas/config/src/graph.ts
@@ -609,6 +609,20 @@
}),
]);
+export const GraphConfigOrDraft = z.discriminatedUnion("type", [
+ z.object({
+ type: z.literal("graph"),
+ graph: GraphSchema,
+ }),
+ z.object({
+ type: z.literal("config"),
+ config: ConfigSchema,
+ }),
+ z.object({
+ type: z.literal("draft"),
+ }),
+]);
+
export type Edge = z.infer<typeof EdgeSchema>;
export type ViewportTransform = z.infer<typeof ViewportTransformSchema>;
export type Graph = z.infer<typeof GraphSchema>;
diff --git a/apps/canvas/config/src/index.ts b/apps/canvas/config/src/index.ts
index c10ce31..f6cc9a5 100644
--- a/apps/canvas/config/src/index.ts
+++ b/apps/canvas/config/src/index.ts
@@ -55,6 +55,7 @@
Edge,
GraphOrConfigSchema,
ViewportTransform,
+ GraphConfigOrDraft,
} from "./graph.js";
export { generateDodoConfig, configToGraph } from "./config.js";
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index 3047fdf..28d43dc 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -1,7 +1,7 @@
-import { nodeLabelFull, useEnv, useMessages, useProjectId, useStateStore } from "@/lib/state";
+import { nodeLabelFull, useMessages, useProjectId, useStateStore } from "@/lib/state";
import { Button } from "./ui/button";
import { useCallback, useEffect, useState } from "react";
-import { generateDodoConfig, AppNode } from "config";
+import { AppNode } from "config";
import { useNodes, useReactFlow } from "@xyflow/react";
import { useToast } from "@/hooks/use-toast";
import {
@@ -31,7 +31,6 @@
const store = useStateStore();
const projectId = useProjectId();
const nodes = useNodes<AppNode>();
- const env = useEnv();
const messages = useMessages();
const instance = useReactFlow();
const [ok, setOk] = useState(false);
@@ -98,18 +97,14 @@
}
setLoading(true);
try {
- const config = generateDodoConfig(projectId, nodes, env);
- if (config == null) {
- throw new Error("MUST NOT REACH!");
- }
const resp = await fetch(`/api/project/${projectId}/deploy`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
- state: instance.toObject(),
- config,
+ type: "graph",
+ graph: instance.toObject(),
}),
});
if (resp.ok) {
@@ -125,7 +120,7 @@
} finally {
setLoading(false);
}
- }, [projectId, instance, nodes, env, setLoading, info, error, monitor, store]);
+ }, [projectId, instance, setLoading, info, error, monitor, store]);
const save = useCallback(async () => {
if (projectId == null) {
return;
@@ -179,7 +174,6 @@
headers: {
"Content-Type": "application/json",
},
- body: JSON.stringify({ state: JSON.stringify(instance.toObject()) }),
});
if (resp.ok) {
clear();
@@ -188,7 +182,7 @@
} else {
error("Failed to delete project", await resp.text());
}
- }, [store, clear, projectId, info, error, instance]);
+ }, [store, clear, projectId, info, error]);
const reload = useCallback(async () => {
if (projectId == null) {
return;