Canvas: Monitor deployment

Change-Id: If5895724025e8e4082a372563c159cbf2216b97f
diff --git a/apps/canvas/back/index.js b/apps/canvas/back/index.js
index 6117e46..ef3449f 100644
--- a/apps/canvas/back/index.js
+++ b/apps/canvas/back/index.js
@@ -199,6 +199,43 @@
         resp.end();
     }
 });
+const handleStatus = (req, resp) => __awaiter(void 0, void 0, void 0, function* () {
+    try {
+        const projectId = Number(req.params["projectId"]);
+        const p = yield db.project.findUnique({
+            where: {
+                id: projectId,
+            },
+            select: {
+                instanceId: true,
+            },
+        });
+        console.log(projectId, p);
+        if (p === null) {
+            resp.status(404);
+            return;
+        }
+        if (p.instanceId == null) {
+            resp.status(404);
+            return;
+        }
+        const r = yield axios_1.default.request({
+            url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/tasks/${p.instanceId}`,
+            method: "get",
+        });
+        resp.status(r.status);
+        if (r.status === 200) {
+            resp.write(JSON.stringify(r.data));
+        }
+    }
+    catch (e) {
+        console.log(e);
+        resp.status(500);
+    }
+    finally {
+        resp.end();
+    }
+});
 function start() {
     return __awaiter(this, void 0, void 0, function* () {
         yield db.$connect();
@@ -207,6 +244,7 @@
         app.post("/api/project/:projectId/saved", handleSave);
         app.get("/api/project/:projectId/saved", handleSavedGet);
         app.post("/api/project/:projectId/deploy", handleDeploy);
+        app.get("/api/project/:projectId/status", handleStatus);
         app.get("/api/project", handleProjectAll);
         app.post("/api/project", handleProjectCreate);
         app.use("/", express_1.default.static("../front/dist"));
diff --git a/apps/canvas/back/index.ts b/apps/canvas/back/index.ts
index 0f7a573..d10a4a6 100644
--- a/apps/canvas/back/index.ts
+++ b/apps/canvas/back/index.ts
@@ -6,197 +6,234 @@
 const db = new PrismaClient();
 
 const handleProjectCreate: express.Handler = async (req, resp) => {
-  try {
-    const { id } = await db.project.create({
-      data: {
-        userId: "gio", // req.get("x-forwarded-userid")!,
-        name: req.body.name,
-      },
-    });
-    resp.status(200);
-    resp.header("Content-Type", "application/json");
-    resp.write(
-      JSON.stringify({
-        id,
-      }),
-    );
-  } catch (e) {
-    console.log(e);
-    resp.status(500);
-  } finally {
-    resp.end();
-  }
+    try {
+        const { id } = await db.project.create({
+            data: {
+                userId: "gio", // req.get("x-forwarded-userid")!,
+                name: req.body.name,
+            },
+        });
+        resp.status(200);
+        resp.header("Content-Type", "application/json");
+        resp.write(
+            JSON.stringify({
+                id,
+            }),
+        );
+    } catch (e) {
+        console.log(e);
+        resp.status(500);
+    } finally {
+        resp.end();
+    }
 };
 
 const handleProjectAll: express.Handler = async (req, resp) => {
-  try {
-    const r = await db.project.findMany({
-      where: {
-        userId: "gio", // req.get("x-forwarded-userid")!,
-      },
-    });
-    resp.status(200);
-    resp.header("Content-Type", "application/json");
-    resp.write(
-      JSON.stringify(
-        r.map((p) => ({
-          id: p.id.toString(),
-          name: p.name,
-        })),
-      ),
-    );
-  } catch (e) {
-    console.log(e);
-    resp.status(500);
-  } finally {
-    resp.end();
-  }
+    try {
+        const r = await db.project.findMany({
+            where: {
+                userId: "gio", // req.get("x-forwarded-userid")!,
+            },
+        });
+        resp.status(200);
+        resp.header("Content-Type", "application/json");
+        resp.write(
+            JSON.stringify(
+                r.map((p) => ({
+                    id: p.id.toString(),
+                    name: p.name,
+                })),
+            ),
+        );
+    } catch (e) {
+        console.log(e);
+        resp.status(500);
+    } finally {
+        resp.end();
+    }
 };
 
 const handleSave: express.Handler = async (req, resp) => {
-  try {
-    await db.project.update({
-      where: {
-        id: Number(req.params["projectId"]),
-      },
-      data: {
-        draft: Buffer.from(JSON.stringify(req.body)),
-      },
-    });
-    resp.status(200);
-  } catch (e) {
-    console.log(e);
-    resp.status(500);
-  } finally {
-    resp.end();
-  }
+    try {
+        await db.project.update({
+            where: {
+                id: Number(req.params["projectId"]),
+            },
+            data: {
+                draft: Buffer.from(JSON.stringify(req.body)),
+            },
+        });
+        resp.status(200);
+    } catch (e) {
+        console.log(e);
+        resp.status(500);
+    } finally {
+        resp.end();
+    }
 };
 
 const handleSavedGet: express.Handler = async (req, resp) => {
-  try {
-    const r = await db.project.findUnique({
-      where: {
-        id: Number(req.params["projectId"]),
-      },
-      select: {
-        state: true,
-        draft: true,
-      },
-    });
-    if (r == null) {
-      resp.status(404);
-    } else {
-      resp.status(200);
-      resp.header("content-type", "application/json");
-      if (r.draft == null) {
-        if (r.state == null) {
-          resp.send({
-            nodes: [],
-            edges: [],
-            viewport: { x: 0, y: 0, zoom: 1 },
-          });
+    try {
+        const r = await db.project.findUnique({
+            where: {
+                id: Number(req.params["projectId"]),
+            },
+            select: {
+                state: true,
+                draft: true,
+            },
+        });
+        if (r == null) {
+            resp.status(404);
         } else {
-          resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
+            resp.status(200);
+            resp.header("content-type", "application/json");
+            if (r.draft == null) {
+                if (r.state == null) {
+                    resp.send({
+                        nodes: [],
+                        edges: [],
+                        viewport: { x: 0, y: 0, zoom: 1 },
+                    });
+                } else {
+                    resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
+                }
+            } else {
+                resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
+            }
         }
-      } else {
-        resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
-      }
+    } catch (e) {
+        console.log(e);
+        resp.status(500);
+    } finally {
+        resp.end();
     }
-  } catch (e) {
-    console.log(e);
-    resp.status(500);
-  } finally {
-    resp.end();
-  }
 };
 
 const handleDeploy: express.Handler = async (req, resp) => {
-  try {
-    const projectId = Number(req.params["projectId"]);
-    const state = Buffer.from(JSON.stringify(req.body.state));
-    const p = await db.project.findUnique({
-      where: {
-        id: projectId,
-      },
-      select: {
-        instanceId: true,
-      },
-    });
-    if (p === null) {
-      resp.status(404);
-      return;
-    }
-    await db.project.update({
-      where: {
-        id: projectId,
-      },
-      data: {
-        draft: state,
-      },
-    });
-    let r: { status: number; data: { id: string; deployKey: string } };
-    if (p.instanceId == null) {
-      r = await axios.request({
-        url: "http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app",
-        method: "post",
-        data: {
-          config: req.body.config,
-        },
-      });
-      if (r.status === 200) {
-        await db.project.update({
-          where: {
-            id: projectId,
-          },
-          data: {
-            state,
-            draft: null,
-            instanceId: r.data.id,
-            deployKey: r.data.deployKey,
-          },
+    try {
+        const projectId = Number(req.params["projectId"]);
+        const state = Buffer.from(JSON.stringify(req.body.state));
+        const p = await db.project.findUnique({
+            where: {
+                id: projectId,
+            },
+            select: {
+                instanceId: true,
+            },
         });
-      }
-    } else {
-      r = await axios.request({
-        url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/${p.instanceId}`,
-        method: "put",
-        data: {
-          config: req.body.config,
-        },
-      });
-      if (r.status === 200) {
+        if (p === null) {
+            resp.status(404);
+            return;
+        }
         await db.project.update({
-          where: {
-            id: projectId,
-          },
-          data: {
-            state,
-            draft: null,
-          },
+            where: {
+                id: projectId,
+            },
+            data: {
+                draft: state,
+            },
         });
-      }
+        let r: { status: number; data: { id: string; deployKey: string } };
+        if (p.instanceId == null) {
+            r = await axios.request({
+                url: "http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app",
+                method: "post",
+                data: {
+                    config: req.body.config,
+                },
+            });
+            if (r.status === 200) {
+                await db.project.update({
+                    where: {
+                        id: projectId,
+                    },
+                    data: {
+                        state,
+                        draft: null,
+                        instanceId: r.data.id,
+                        deployKey: r.data.deployKey,
+                    },
+                });
+            }
+        } else {
+            r = await axios.request({
+                url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/${p.instanceId}`,
+                method: "put",
+                data: {
+                    config: req.body.config,
+                },
+            });
+            if (r.status === 200) {
+                await db.project.update({
+                    where: {
+                        id: projectId,
+                    },
+                    data: {
+                        state,
+                        draft: null,
+                    },
+                });
+            }
+        }
+    } catch (e) {
+        console.log(e);
+        resp.status(500);
+    } finally {
+        resp.end();
     }
-  } catch (e) {
-    console.log(e);
-    resp.status(500);
-  } finally {
-    resp.end();
-  }
+};
+
+const handleStatus: express.Handler = async (req, resp) => {
+    try {
+        const projectId = Number(req.params["projectId"]);
+        const p = await db.project.findUnique({
+            where: {
+                id: projectId,
+            },
+            select: {
+                instanceId: true,
+            },
+        });
+        console.log(projectId, p);
+        if (p === null) {
+            resp.status(404);
+            return;
+        }
+        if (p.instanceId == null) {
+            resp.status(404);
+            return;
+        }
+        const r = await axios.request({
+            url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/tasks/${p.instanceId}`,
+            method: "get",
+        });
+        resp.status(r.status);
+        if (r.status === 200) {
+            resp.write(JSON.stringify(r.data));
+        }
+    } catch (e) {
+        console.log(e);
+        resp.status(500);
+    } finally {
+        resp.end();
+    }
 };
 
 async function start() {
-  await db.$connect();
-  const app = express();
-  app.use(express.json());
-  app.post("/api/project/:projectId/saved", handleSave);
-  app.get("/api/project/:projectId/saved", handleSavedGet);
-  app.post("/api/project/:projectId/deploy", handleDeploy);
-  app.get("/api/project", handleProjectAll);
-  app.post("/api/project", handleProjectCreate);
-  app.use("/", express.static("../front/dist"));
-  app.listen(env.DODO_PORT_WEB, () => {
-    console.log("started");
-  });
+    await db.$connect();
+    const app = express();
+    app.use(express.json());
+    app.post("/api/project/:projectId/saved", handleSave);
+    app.get("/api/project/:projectId/saved", handleSavedGet);
+    app.post("/api/project/:projectId/deploy", handleDeploy);
+    app.get("/api/project/:projectId/status", handleStatus);
+    app.get("/api/project", handleProjectAll);
+    app.post("/api/project", handleProjectCreate);
+    app.use("/", express.static("../front/dist"));
+    app.listen(env.DODO_PORT_WEB, () => {
+        console.log("started");
+    });
 }
 
 start();
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index cc09a59..7d0afe7 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -1,4 +1,4 @@
-import { AppNode, useEnv, useMessages, useProjectId, useStateStore } from "@/lib/state";
+import { AppNode, nodeLabel, useEnv, useMessages, useProjectId, useStateStore } from "@/lib/state";
 import { Button } from "./ui/button";
 import { useCallback, useEffect, useState } from "react";
 import { generateDodoConfig } from "@/lib/config";
@@ -18,6 +18,31 @@
     useEffect(() => {
         setOk(!messages.some((m) => m.type === "FATAL"));
     }, [messages, setOk]);
+    const monitor = useCallback(async () => {
+        const m = async function() {
+            const resp = await fetch(`/api/project/${projectId}/status`, {
+                method: "GET",
+                headers: {
+                    "Content-Type": "application/json",
+                },
+            })
+            if (resp.status !== 200) {
+                return;
+            }
+            const data: { type: string, name: string, status: string }[] = await resp.json();
+            console.log(data);
+            for (const n of nodes) {
+                console.log(nodeLabel(n));
+                for (const d of data) {
+                    if (nodeLabel(n) === d.name) {
+                        store.updateNodeData(n.id, { state: d.status });
+                    }
+                }
+            }
+            setTimeout(m, 1000);
+        };
+        setTimeout(m, 100);
+    }, [projectId, nodes]);
     const deploy = useCallback(async () => {
         if (projectId == null) {
             return;
@@ -42,6 +67,7 @@
                 toast({
                     title: "Deployment succeeded",
                 });
+                monitor();
             } else {
                 toast({
                     variant: "destructive",
@@ -96,6 +122,10 @@
         store.setEdges(inst.edges || []);
         instance.setViewport({ x, y, zoom });
     }, [projectId, instance, st]);
+    const clear = useCallback(() => {
+        store.setEdges([]);
+        store.setNodes([]);
+    }, [store]);
     const [props, setProps] = useState({});
     useEffect(() => {
         if (loading) {
@@ -111,6 +141,7 @@
             <Button onClick={deploy} {...props}>Deploy</Button>
             <Button onClick={save}>Save</Button>
             <Button onClick={restoreSaved}>Restore</Button>
+            <Button onClick={clear}>Clear</Button>
         </>
     )
 }
\ No newline at end of file
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index a445c72..fcfe2a1 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -18,7 +18,7 @@
   const isConnectablePorts = useMemo(() => nodeIsConnectable(node, "ports"), [node]);
   const isConnectableRepository = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
   return (
-    <NodeRect id={id} selected={selected} type={node.type}>
+    <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
       <div style={{ padding: '10px 20px' }}>
         {nodeLabel(node)}
         <Handle
diff --git a/apps/canvas/front/src/components/node-gateway-https.tsx b/apps/canvas/front/src/components/node-gateway-https.tsx
index be624a3..fdf710d 100644
--- a/apps/canvas/front/src/components/node-gateway-https.tsx
+++ b/apps/canvas/front/src/components/node-gateway-https.tsx
@@ -23,7 +23,7 @@
   const { id, selected } = node;
   const isConnectable = useMemo(() => nodeIsConnectable(node, "https"), [node]);
   return (
-    <NodeRect id={id} selected={selected} type={node.type}>
+    <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
       {nodeLabel(node)}
       <Handle
         type={"target"}
diff --git a/apps/canvas/front/src/components/node-gateway-tcp.tsx b/apps/canvas/front/src/components/node-gateway-tcp.tsx
index 8cfebff..aeca41e 100644
--- a/apps/canvas/front/src/components/node-gateway-tcp.tsx
+++ b/apps/canvas/front/src/components/node-gateway-tcp.tsx
@@ -25,7 +25,7 @@
   const { id, selected } = node;
   const isConnectable = useMemo(() => nodeIsConnectable(node, "tcp"), [node]);
   return (
-    <NodeRect id={id} selected={selected} type={node.type}>
+    <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
       {nodeLabel(node)}
       <Handle
         type={"target"}
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index 13b257b..aa48cd1 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -12,7 +12,7 @@
   const { id, selected } = node;
   const isConnectable = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
   return (
-    <NodeRect id={id} selected={selected} type={node.type}>
+    <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
       <div style={{ padding: '10px 20px' }}>
         {nodeLabel(node)}
         <Handle
diff --git a/apps/canvas/front/src/components/node-mongodb.tsx b/apps/canvas/front/src/components/node-mongodb.tsx
index 89323b9..40c9748 100644
--- a/apps/canvas/front/src/components/node-mongodb.tsx
+++ b/apps/canvas/front/src/components/node-mongodb.tsx
@@ -11,7 +11,7 @@
 export function NodeMongoDB(node: MongoDBNode) {
   const { id, selected } = node;
   return (
-    <NodeRect id={id} selected={selected} type={node.type}>
+    <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
       <div style={{ padding: '10px 20px' }}>
         {nodeLabel(node)}
         <Handle
diff --git a/apps/canvas/front/src/components/node-postgresql.tsx b/apps/canvas/front/src/components/node-postgresql.tsx
index db7f067..4213645 100644
--- a/apps/canvas/front/src/components/node-postgresql.tsx
+++ b/apps/canvas/front/src/components/node-postgresql.tsx
@@ -11,7 +11,7 @@
 export function NodePostgreSQL(node: PostgreSQLNode) {
   const { id, selected } = node;
   return (
-    <NodeRect id={id} selected={selected} type={node.type}>
+    <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
       <div style={{ padding: '10px 20px' }}>
         {nodeLabel(node)}
         <Handle
diff --git a/apps/canvas/front/src/components/node-rect.tsx b/apps/canvas/front/src/components/node-rect.tsx
index 5164a13..ad0bee7 100644
--- a/apps/canvas/front/src/components/node-rect.tsx
+++ b/apps/canvas/front/src/components/node-rect.tsx
@@ -1,34 +1,43 @@
 import { NodeType, useNodeMessages } from "@/lib/state";
 import { Icon } from "./icon";
+import { useEffect, useState } from "react";
 
 export type Props = {
     id: string;
     selected?: boolean;
     children: any;
     type: NodeType;
+    state: string | null;
 };
 
 export function NodeRect(p: Props) {
-    const { id, selected, children } = p;
+    const { id, selected, children, state } = p;
     const messages = useNodeMessages(id);
     const hasFatal = messages.some((m) => m.type === "FATAL");
     const hasWarning = messages.some((m) => m.type === "WARNING");
-    const classes = ["px-4", "py-2", "rounded-md", "bg-white"];
-    if (hasFatal) {
-        classes.push("border-red-500");
-    } else if (hasWarning) {
-        classes.push("border-yellow-500");
-    } else {
-        classes.push("border-black");
-    }
-    if (selected) {
-        classes.push("border-2");
-    } else {
-        classes.push("border");
-    }
+    const [classes, setClasses] = useState<string[]>([]);
+    useEffect(() => {
+        const classes = ["px-4", "py-2", "rounded-md", "bg-white"];
+        if (hasFatal) {
+            classes.push("border-red-500");
+        } else if (hasWarning) {
+            classes.push("border-yellow-500");
+        } else {
+            classes.push("border-black");
+        }
+        if (selected) {
+            classes.push("border-2");
+        } else {
+            classes.push("border");
+        }
+        if (state === "running") {
+            classes.push("animate-pulse");
+        }
+        setClasses(classes);
+    }, [selected, hasFatal, hasWarning, state, setClasses]);
     return (
         <div className={classes.join(" ")}>
-            <div style={{ position: "absolute", top: "5px", left: "5px"}}>
+            <div style={{ position: "absolute", top: "5px", left: "5px" }}>
                 {Icon(p.type)}
             </div>
             {children}
diff --git a/apps/canvas/front/src/components/node-volume.tsx b/apps/canvas/front/src/components/node-volume.tsx
index 57c488d..430d1e9 100644
--- a/apps/canvas/front/src/components/node-volume.tsx
+++ b/apps/canvas/front/src/components/node-volume.tsx
@@ -13,7 +13,7 @@
   const { id, data, selected } = node;
   const isConnectable = useMemo(() => nodeIsConnectable(node, "volume"), [node]);
   return (
-    <NodeRect id={id} selected={selected} type={node.type}>
+    <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
       <div style={{ padding: '10px 20px' }}>
         <div>{nodeLabel(node)}</div>
         <div>{data.type && `${data.type}`}</div>
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 55fbc5d..f2836f1 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -21,6 +21,7 @@
 
 export type NodeData = InitData & {
   activeField?: string | undefined;
+  state: string | null;
 };
 
 export type PortConnectedTo = {