Canvas: Service dev UI

Change-Id: I11968dbf5ec51c5fd234ad927d40b0b3983e71dd
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index a3784c1..60d9966 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -1,4 +1,4 @@
-import { AppNode, Env, GatewayHttpsNode, Message, MessageType, NodeType, ServiceType, VolumeType } from "./state";
+import { AppNode, Env, Message, MessageType, NodeType, ServiceType, VolumeType } from "./state";
 
 export type AuthDisabled = {
 	enabled: false;
@@ -57,6 +57,11 @@
 	expose?: PortDomain[];
 	volume?: string[];
 	preBuildCommands?: { bin: string }[];
+	dev?: {
+		enabled: boolean;
+		ssh?: Domain;
+		codeServer?: Domain;
+	};
 };
 
 export type Volume = {
@@ -143,7 +148,7 @@
 						ingress: ingressNodes
 							.filter((i) => i.data.https!.serviceId === n.id)
 							.map(
-								(i: GatewayHttpsNode): Ingress => ({
+								(i): Ingress => ({
 									network: networkMap.get(i.data.network!)!,
 									subdomain: i.data.subdomain!,
 									port: {
@@ -165,6 +170,23 @@
 						preBuildCommands: n.data.preBuildCommands
 							? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
 							: [],
+						dev: {
+							enabled: n.data.dev ? n.data.dev.enabled : false,
+							codeServer:
+								n.data.dev?.enabled && n.data.dev.expose != null
+									? {
+											network: n.data.dev.expose.network,
+											subdomain: n.data.dev.expose.subdomain,
+										}
+									: undefined,
+							ssh:
+								n.data.dev?.enabled && n.data.dev.expose != null
+									? {
+											network: n.data.dev.expose.network,
+											subdomain: n.data.dev.expose.subdomain,
+										}
+									: undefined,
+						},
 					};
 				}),
 			volume: nodes
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index b8db5b5..6e04e4c 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -41,6 +41,7 @@
 };
 
 export type GatewayHttpsData = NodeData & {
+	readonly?: boolean;
 	network?: string;
 	subdomain?: string;
 	https?: PortConnectedTo;
@@ -56,6 +57,7 @@
 };
 
 export type GatewayTCPData = NodeData & {
+	readonly?: boolean;
 	network?: string;
 	subdomain?: string;
 	exposed: PortConnectedTo[];
@@ -87,6 +89,11 @@
 ] as const;
 export type ServiceType = (typeof ServiceTypes)[number];
 
+export type Domain = {
+	network: string;
+	subdomain: string;
+};
+
 export type ServiceData = NodeData & {
 	type: ServiceType;
 	repository:
@@ -106,6 +113,17 @@
 	volume: string[];
 	preBuildCommands: string;
 	isChoosingPortToConnect: boolean;
+	dev?:
+		| {
+				enabled: false;
+				expose?: Domain;
+		  }
+		| {
+				enabled: true;
+				expose?: Domain;
+				codeServerNodeId: string;
+				sshNodeId: string;
+		  };
 };
 
 export type ServiceNode = Node<ServiceData> & {
@@ -168,35 +186,41 @@
 	| NANode;
 
 export function nodeLabel(n: AppNode): string {
-	switch (n.type) {
-		case "network":
-			return n.data.domain;
-		case "app":
-			return n.data.label || "Service";
-		case "github":
-			return n.data.repository?.fullName || "Github";
-		case "gateway-https": {
-			if (n.data && n.data.network && n.data.subdomain) {
-				return `https://${n.data.subdomain}.${n.data.network}`;
-			} else {
-				return "HTTPS Gateway";
+	try {
+		switch (n.type) {
+			case "network":
+				return n.data.domain;
+			case "app":
+				return n.data.label || "Service";
+			case "github":
+				return n.data.repository?.fullName || "Github";
+			case "gateway-https": {
+				if (n.data && n.data.network && n.data.subdomain) {
+					return `https://${n.data.subdomain}.${n.data.network}`;
+				} else {
+					return "HTTPS Gateway";
+				}
 			}
-		}
-		case "gateway-tcp": {
-			if (n.data && n.data.network && n.data.subdomain) {
-				return `${n.data.subdomain}.${n.data.network}`;
-			} else {
-				return "TCP Gateway";
+			case "gateway-tcp": {
+				if (n.data && n.data.network && n.data.subdomain) {
+					return `${n.data.subdomain}.${n.data.network}`;
+				} else {
+					return "TCP Gateway";
+				}
 			}
+			case "mongodb":
+				return n.data.label || "MongoDB";
+			case "postgresql":
+				return n.data.label || "PostgreSQL";
+			case "volume":
+				return n.data.label || "Volume";
+			case undefined:
+				throw new Error("MUST NOT REACH!");
 		}
-		case "mongodb":
-			return n.data.label || "MongoDB";
-		case "postgresql":
-			return n.data.label || "PostgreSQL";
-		case "volume":
-			return n.data.label || "Volume";
-		case undefined:
-			throw new Error("MUST NOT REACH!");
+	} catch (e) {
+		console.error("opaa", e);
+	} finally {
+		console.log("done");
 	}
 }
 
@@ -464,14 +488,36 @@
 		});
 	};
 
+	const injectNetworkNodes = () => {
+		const newNetworks = get().env.networks.filter(
+			(x) => !get().nodes.some((n) => n.type === "network" && n.data.domain === x.domain),
+		);
+		newNetworks.forEach((n) => {
+			get().addNode({
+				id: n.domain,
+				type: "network",
+				connectable: true,
+				data: {
+					domain: n.domain,
+					label: n.domain,
+					envVars: [],
+					ports: [],
+					state: "success", // TODO(gio): monitor network health
+				},
+			});
+			console.log("added network", n.domain);
+		});
+	};
+
 	const restoreSaved = async () => {
 		const { projectId } = get();
 		const resp = await fetch(`/api/project/${projectId}/saved/${get().mode === "deploy" ? "deploy" : "draft"}`, {
 			method: "GET",
 		});
 		const inst = await resp.json();
-		setN(inst.nodes || []);
-		set({ edges: inst.edges || [] });
+		setN(inst.nodes);
+		set({ edges: inst.edges });
+		injectNetworkNodes();
 		if (
 			get().zoom.x !== inst.viewport.x ||
 			get().zoom.y !== inst.viewport.y ||
@@ -780,23 +826,7 @@
 			} finally {
 				if (JSON.stringify(get().env) !== JSON.stringify(env)) {
 					set({ env });
-					const newNetworks = env.networks.filter(
-						(x) => !get().nodes.some((n) => n.type === "network" && n.data.domain === x.domain),
-					);
-					newNetworks.forEach((n) => {
-						get().addNode({
-							id: n.domain,
-							type: "network",
-							connectable: true,
-							data: {
-								domain: n.domain,
-								label: n.domain,
-								envVars: [],
-								ports: [],
-								state: "success", // TODO(gio): monitor network health
-							},
-						});
-					});
+					injectNetworkNodes();
 
 					if (env.integrations.github) {
 						set({ githubService: new GitHubServiceImpl(projectId!) });