Canvas: Render network nodes

Change-Id: I63938da205af9377a1e210c0e972591142211a68
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index fcfe2a1..3598a4d 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -1,6 +1,6 @@
 import { v4 as uuidv4 } from "uuid";
 import { NodeRect } from './node-rect';
-import { useStateStore, ServiceNode, ServiceTypes, nodeLabel, BoundEnvVar, AppState, nodeIsConnectable } from '@/lib/state';
+import { useStateStore, ServiceNode, ServiceTypes, nodeLabel, BoundEnvVar, AppState, nodeIsConnectable, GatewayTCPNode, GatewayHttpsNode } from '@/lib/state';
 import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
 import { z } from "zod";
 import { DeepPartial, EventType, useForm } from 'react-hook-form';
@@ -204,16 +204,32 @@
     }
   }, [id, data, store]);
   const removePort = useCallback((portId: string) => {
+    // TODO(gio): this is ugly
+    const tcpRemoved = new Set<string>();
+    console.log(store.edges);
     store.setEdges(store.edges.filter((e) => {
       if (e.source !== id || e.sourceHandle !== "ports") {
         return true;
       }
+      const tn = store.nodes.find((n) => n.id == e.target)!;
       if (e.targetHandle === "https") {
-        return false;
+        const t = tn as GatewayHttpsNode;
+        if (t.data.https?.serviceId === id && t.data.https.portId === portId) {
+          return false;
+        }
+      }
+      if (e.targetHandle === "tcp") {
+        const t = tn as GatewayTCPNode;
+        if (tcpRemoved.has(t.id)) {
+          return true;
+        }
+        if (t.data.exposed.find((e) => e.serviceId === id && e.portId === portId)) {
+          console.log(11111, e);
+          tcpRemoved.add(t.id);
+          return false;
+        }
       }
       if (e.targetHandle === "env_var") {
-        const tn = store.nodes.find((n) => n.type === "app" && n.id == e.target);
-        console.log("111", tn!.data.envVars);
         if (tn && (tn.data.envVars || []).find((ev) => ev.source === id && "portId" in ev && ev.portId === portId)) {
           return false;
         }
@@ -225,6 +241,20 @@
         https: undefined,
       });
     });
+    store.nodes.filter((n) => n.type === "gateway-tcp").forEach((n) => {
+      const filtered = n.data.exposed.filter((e) => {
+        if (e.serviceId === id && e.portId === portId) {
+          return false;
+        } else {
+          return true;
+        }
+      })
+      if (filtered.length != n.data.exposed.length) {
+        store.updateNodeData<"gateway-tcp">(n.id, {
+          exposed: filtered,
+        });
+      }
+    });
     store.nodes.filter((n) => n.type === "app" && n.data.envVars).forEach((n) => {
       store.updateNodeData<"app">(n.id, {
         envVars: n.data.envVars.filter((ev) => {
@@ -234,7 +264,7 @@
           return true;
         })
       });
-    })
+    });
     store.updateNodeData<"app">(id, {
       ports: (data.ports || []).filter((p) => p.id !== portId),
       envVars: (data.envVars || []).filter((ev) => !(ev.source === null && "portId" in ev && ev.portId === portId)),