Canvas: Show env var values in deploy mode

Change-Id: Icfa8e33f1441e7bab1bb139286b38a223583919d
diff --git a/apps/canvas/front/src/Gateways.tsx b/apps/canvas/front/src/Gateways.tsx
index d6de75b..e4785bd 100644
--- a/apps/canvas/front/src/Gateways.tsx
+++ b/apps/canvas/front/src/Gateways.tsx
@@ -2,13 +2,17 @@
 import { Copy, Check } from "lucide-react";
 import { Button } from "./components/ui/button";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/ui/tooltip";
-import { useCallback, useState } from "react";
+import { useCallback, useMemo, useState } from "react";
 import { AccessType } from "./components/icon";
 import { Access } from "config";
 
 export function Gateways() {
 	const env = useEnv();
-	const groupedAccess = env.access.reduce((acc, curr) => {
+	console.log(env.access);
+	const filtered = useMemo(() => {
+		return env.access.filter((a) => a.type !== "env_var");
+	}, [env.access]);
+	const groupedAccess = filtered.reduce((acc, curr) => {
 		if (!acc.has(curr.name)) {
 			acc.set(curr.name, []);
 		}
diff --git a/apps/canvas/front/src/Tools.tsx b/apps/canvas/front/src/Tools.tsx
index f128ed0..fc3bc1f 100644
--- a/apps/canvas/front/src/Tools.tsx
+++ b/apps/canvas/front/src/Tools.tsx
@@ -16,7 +16,9 @@
 				</TabsTrigger>
 				<TabsTrigger value="gateways" className="space-x-2">
 					<div>Gateways</div>
-					<Badge className="h-5 min-w-5 rounded-full px-2 font-mono tabular-nums">{env.access.length}</Badge>
+					<Badge className="h-5 min-w-5 rounded-full px-2 font-mono tabular-nums">
+						{env.access.filter((a) => a.type !== "env_var").length}
+					</Badge>
 				</TabsTrigger>
 				<TabsTrigger value="deployKeys">Deploy keys</TabsTrigger>
 			</TabsList>
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index e63c76a..a6eaaf9 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -1,6 +1,14 @@
 import { v4 as uuidv4 } from "uuid";
 import { NodeRect } from "./node-rect";
-import { useStateStore, nodeLabel, AppState, nodeIsConnectable, useEnv, useGithubRepositories } from "@/lib/state";
+import {
+	useStateStore,
+	nodeLabel,
+	AppState,
+	nodeIsConnectable,
+	useEnv,
+	useGithubRepositories,
+	useMode,
+} from "@/lib/state";
 import {
 	ServiceNode,
 	ServiceTypes,
@@ -27,7 +35,7 @@
 import { Switch } from "./ui/switch";
 import { Label } from "./ui/label";
 import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
-import { Code, Container, Network, Pencil, Variable } from "lucide-react";
+import { Check, Code, Container, Copy, Network, Pencil, Variable } from "lucide-react";
 import { Badge } from "./ui/badge";
 import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "./ui/accordion";
 import { Name } from "./node-name";
@@ -894,6 +902,8 @@
 
 function EnvVars({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
 	const { id, data } = node;
+	const mode = useMode();
+	const env = useEnv();
 	const store = useStateStore();
 	const [name, setName] = useState("");
 	const [value, setValue] = useState("");
@@ -1032,6 +1042,84 @@
 		},
 		[store, saveAlias],
 	);
+	const envVars = useMemo(() => {
+		if (mode !== "deploy") {
+			return [];
+		}
+		return env.access
+			.filter((a) => a.name === data.label)
+			.filter((a) => a.type === "env_var")
+			.map((a) => ({
+				name: a.var.split("=", 2)[0],
+				value: a.var.split("=", 2)[1],
+			}));
+	}, [mode, env.access, data.label]);
+
+	const hiddenEnvVars = useMemo(() => {
+		return envVars
+			.map((v) => {
+				const { name, value } = v;
+				const match = value.match(/^(postgresql|mongodb):\/\/([^:]+):([^@]+)@([^:/]+)(?::(\d+))?\/(.+)$/);
+				if (match) {
+					const [_, protocol, username, _password, host, port, database] = match;
+					return {
+						name,
+						value,
+						hidden: `${protocol}://${username}:*****@${host}${port ? `:${port}` : ""}/${database}`,
+					};
+				}
+				return {
+					name,
+					value,
+					hidden: value,
+				};
+			})
+			.map((v) => {
+				return {
+					...v,
+					hidden: v.hidden.length > 50 ? v.hidden.slice(0, 50) + "..." : v.hidden,
+				};
+			});
+	}, [envVars]);
+
+	const [copied, setCopied] = useState(false);
+	const [blip, setBlip] = useState(false);
+
+	const handleCopy = () => {
+		navigator.clipboard.writeText(envVars.map((v) => `${v.name}=${v.value}`).join("\n"));
+		setCopied(true);
+		setBlip(true);
+		setTimeout(() => setCopied(false), 1000);
+		setTimeout(() => setBlip(false), 300);
+	};
+
+	if (hiddenEnvVars.length > 0) {
+		return (
+			<div className="flex flex-col gap-1">
+				<div className="grid grid-cols-[auto_minmax(0,1fr)] gap-1 flex-shrink">
+					{hiddenEnvVars.map((v) => (
+						<div key={v.name} className="contents">
+							<div className="uppercase">{v.name}</div>
+							<div className="min-w-0 truncate">{v.hidden}</div>
+						</div>
+					))}
+				</div>
+				<div className="flex justify-end">
+					<Button onClick={handleCopy} className={blip ? "bg-green-100 transition-colors" : ""}>
+						{copied ? (
+							<>
+								<Check className="w-4 h-4 text-green-600" /> Copy
+							</>
+						) : (
+							<>
+								<Copy className="w-4 h-4" /> Copy
+							</>
+						)}
+					</Button>
+				</div>
+			</div>
+		);
+	}
 
 	return (
 		<div className="flex flex-col gap-1">