Canvas: Rework Deployment/Gateways tab

Change-Id: I938262b9a6ba2af060531e7dcdf91ddd66721385
diff --git a/apps/canvas/front/src/Gateways.tsx b/apps/canvas/front/src/Gateways.tsx
new file mode 100644
index 0000000..a1fce28
--- /dev/null
+++ b/apps/canvas/front/src/Gateways.tsx
@@ -0,0 +1,93 @@
+import { z } from "zod";
+import { accessSchema, useEnv } from "./lib/state";
+import { Copy, Globe, Terminal, Network, Database, Check } from "lucide-react";
+import { Button } from "./components/ui/button";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/ui/tooltip";
+import { useCallback, useState } from "react";
+
+export function Gateways() {
+	const env = useEnv();
+	const groupedAccess = env.access.reduce((acc, curr) => {
+		if (!acc.has(curr.name)) {
+			acc.set(curr.name, []);
+		}
+		acc.get(curr.name)!.push(curr);
+		return acc;
+	}, new Map<string, typeof env.access>());
+	return (
+		<ul>
+			{Array.from(groupedAccess.entries()).map(([name, access]) => (
+				<li key={name}>
+					{access.map((a) => (
+						<Gateway g={a} />
+					))}
+				</li>
+			))}
+		</ul>
+	);
+}
+
+function Gateway({ g }: { g: z.infer<typeof accessSchema> }) {
+	const [hidden, content] = (() => {
+		switch (g.type) {
+			case "https":
+				return [g.address, g.address];
+			case "ssh":
+			case "tcp":
+			case "udp":
+				return [`${g.host}:${g.port}`, `${g.host}:${g.port}`];
+			case "postgresql":
+				return [
+					`postgresql://${g.username}:*****@${g.host}:${g.port}/${g.database}`,
+					`postgresql://${g.username}:${g.password}@${g.host}:${g.port}/${g.database}`,
+				];
+			case "mongodb":
+				return [
+					`mongodb://${g.username}:*****@${g.host}:${g.port}/${g.database}`,
+					`mongodb://${g.username}:${g.password}@${g.host}:${g.port}/${g.database}`,
+				];
+		}
+	})();
+	const [clicked, setClicked] = useState(false);
+	const [open, setOpen] = useState(false);
+	const copy = useCallback(() => {
+		navigator.clipboard.writeText(content);
+		setClicked(true);
+		setOpen(true);
+		setTimeout(() => {
+			setClicked(false);
+			setOpen(false);
+		}, 1000);
+	}, [content, setClicked, setOpen]);
+	return (
+		<TooltipProvider>
+			<Tooltip delayDuration={100} open={open} onOpenChange={setOpen}>
+				<TooltipTrigger asChild>
+					<Button variant="ghost" onClick={copy}>
+						<AccessType type={g.type} className="w-4 h-4" />
+						<div className="hover:bg-gray-200 p-x-1">{hidden}</div>
+					</Button>
+				</TooltipTrigger>
+				<TooltipContent side="right" className="!bg-transparent cursor-pointer !p-0" sideOffset={1}>
+					{!clicked && <Copy className="w-4 h-4 !bg-transparent" color="black" />}
+					{clicked && <Check className="w-4 h-4 !bg-transparent" color="black" />}
+				</TooltipContent>
+			</Tooltip>
+		</TooltipProvider>
+	);
+}
+
+function AccessType({ type, className }: { type: z.infer<typeof accessSchema>["type"]; className?: string }) {
+	switch (type) {
+		case "https":
+			return <Globe className={className} />;
+		case "ssh":
+			return <Terminal className={className} />;
+		case "tcp":
+		case "udp":
+			return <Network className={className} />;
+		case "postgresql":
+		case "mongodb":
+			return <Database className={className} />;
+	}
+}