Canvas: Show env var values in deploy mode
Change-Id: Icfa8e33f1441e7bab1bb139286b38a223583919d
diff --git a/apps/canvas/back/eslint.config.mjs b/apps/canvas/back/eslint.config.mjs
index 20ef473..25eded9 100644
--- a/apps/canvas/back/eslint.config.mjs
+++ b/apps/canvas/back/eslint.config.mjs
@@ -13,9 +13,9 @@
"@typescript-eslint/no-unused-vars": [
"error",
{
- argsIgnorePattern: "^_$",
- varsIgnorePattern: "^_$",
- caughtErrorsIgnorePattern: "^_$",
+ argsIgnorePattern: "^_.*",
+ varsIgnorePattern: "^_.*",
+ caughtErrorsIgnorePattern: "^_.*",
},
],
},
diff --git a/apps/canvas/back/src/app_manager.ts b/apps/canvas/back/src/app_manager.ts
index 4fa8cfd..325216c 100644
--- a/apps/canvas/back/src/app_manager.ts
+++ b/apps/canvas/back/src/app_manager.ts
@@ -44,6 +44,11 @@
username: z.string(),
password: z.string(),
}),
+ z.object({
+ type: z.literal("env_var"),
+ name: z.string(),
+ var: z.string(),
+ }),
]);
export const DeployResponseSchema = z.object({
diff --git a/apps/canvas/config/eslint.config.mjs b/apps/canvas/config/eslint.config.mjs
index 20ef473..25eded9 100644
--- a/apps/canvas/config/eslint.config.mjs
+++ b/apps/canvas/config/eslint.config.mjs
@@ -13,9 +13,9 @@
"@typescript-eslint/no-unused-vars": [
"error",
{
- argsIgnorePattern: "^_$",
- varsIgnorePattern: "^_$",
- caughtErrorsIgnorePattern: "^_$",
+ argsIgnorePattern: "^_.*",
+ varsIgnorePattern: "^_.*",
+ caughtErrorsIgnorePattern: "^_.*",
},
],
},
diff --git a/apps/canvas/config/src/graph.ts b/apps/canvas/config/src/graph.ts
index fc3258e..66c3c07 100644
--- a/apps/canvas/config/src/graph.ts
+++ b/apps/canvas/config/src/graph.ts
@@ -333,6 +333,11 @@
username: z.string(),
password: z.string(),
}),
+ z.object({
+ type: z.literal("env_var"),
+ name: z.string(),
+ var: z.string(),
+ }),
]);
export const serviceInfoSchema = z.object({
diff --git a/apps/canvas/front/eslint.config.js b/apps/canvas/front/eslint.config.js
index 53e1e9b..91f88b5 100644
--- a/apps/canvas/front/eslint.config.js
+++ b/apps/canvas/front/eslint.config.js
@@ -28,9 +28,9 @@
"@typescript-eslint/no-unused-vars": [
"error",
{
- argsIgnorePattern: "^_$",
- varsIgnorePattern: "^_$",
- caughtErrorsIgnorePattern: "^_$",
+ argsIgnorePattern: "^_.*",
+ varsIgnorePattern: "^_.*",
+ caughtErrorsIgnorePattern: "^_.*",
},
],
},
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">