Canvas: Let user define name/value env var

Change-Id: I9beffcc6f0dcbb674ef82b37b93b5f5ef7d189bc
diff --git a/apps/canvas/config/src/config.ts b/apps/canvas/config/src/config.ts
index 9568c7e..b0aa744 100644
--- a/apps/canvas/config/src/config.ts
+++ b/apps/canvas/config/src/config.ts
@@ -75,10 +75,15 @@
 						})),
 					env: (n.data.envVars || [])
 						.filter((e) => "name" in e)
-						.map((e) => ({
-							name: e.name,
-							alias: "alias" in e ? e.alias : undefined,
-						})),
+						.map((e) => {
+							if ("value" in e) {
+								return { name: e.name, value: e.value };
+							}
+							return {
+								name: e.name,
+								alias: "alias" in e ? e.alias : undefined,
+							};
+						}),
 					ingress: ingressNodes
 						.filter((i) => i.data.https!.serviceId === n.id)
 						.map(
@@ -278,11 +283,18 @@
 					}),
 				),
 				envVars: (s.env || []).map((e): BoundEnvVar => {
-					if (e.alias != null) {
+					if (e.value != null) {
 						return {
 							id: uuidv4(),
-							name: e.name,
 							source: null,
+							name: e.name,
+							value: e.value,
+						};
+					} else if (e.alias != null && e.alias !== "") {
+						return {
+							id: uuidv4(),
+							source: null,
+							name: e.name,
 							alias: e.alias,
 							isEditting: false,
 						};
diff --git a/apps/canvas/config/src/graph.ts b/apps/canvas/config/src/graph.ts
index 90523d5..3c66fbd 100644
--- a/apps/canvas/config/src/graph.ts
+++ b/apps/canvas/config/src/graph.ts
@@ -62,6 +62,13 @@
 			name: string;
 			alias: string;
 			isEditting: boolean;
+	  }
+	| {
+			id: string;
+			source: null;
+			name: string;
+			value: string;
+			isEditting?: boolean;
 	  };
 
 export type EnvVar = {
diff --git a/apps/canvas/config/src/types.ts b/apps/canvas/config/src/types.ts
index e186f4b..15bf22d 100644
--- a/apps/canvas/config/src/types.ts
+++ b/apps/canvas/config/src/types.ts
@@ -90,6 +90,7 @@
 			z.object({
 				name: z.string(),
 				alias: z.string().optional(),
+				value: z.string().optional(),
 			}),
 		)
 		.optional(),
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index 55dced4..355e41b 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -558,6 +558,56 @@
 function EnvVars({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
 	const { id, data } = node;
 	const store = useStateStore();
+	const [name, setName] = useState("");
+	const [value, setValue] = useState("");
+
+	const addEnvVar = useCallback(() => {
+		if (!name.trim() || !value.trim()) return;
+		store.updateNodeData<"app">(id, {
+			envVars: (data.envVars || []).concat({
+				id: uuidv4(),
+				source: null,
+				name: name.toUpperCase(),
+				value: value,
+			}),
+		});
+		setName("");
+		setValue("");
+	}, [id, data, store, name, value]);
+
+	const removeEnvVar = useCallback(
+		(varId: string) => {
+			store.updateNodeData<"app">(id, {
+				envVars: (data.envVars || []).filter((v) => v.id !== varId),
+			});
+		},
+		[id, data, store],
+	);
+
+	const editValueEnvVar = useCallback(
+		(varId: string) => {
+			if (disabled) return;
+			store.updateNodeData<"app">(id, {
+				envVars: (data.envVars || []).map((v) => (v.id === varId ? { ...v, isEditting: true } : v)),
+			});
+		},
+		[id, data, store, disabled],
+	);
+
+	const saveValueEnvVar = useCallback(
+		(varId: string, newName: string, newValue: string) => {
+			store.updateNodeData<"app">(id, {
+				envVars: (data.envVars || []).map((v) => {
+					if (v.id === varId) {
+						return { ...v, name: newName.toUpperCase(), value: newValue, isEditting: false };
+					}
+					return v;
+				}),
+			});
+		},
+		[id, data, store],
+	);
+
 	const editAlias = useCallback(
 		(e: BoundEnvVar) => {
 			return () => {
@@ -580,6 +630,7 @@
 		},
 		[id, data, store, disabled],
 	);
+
 	const saveAlias = useCallback(
 		(e: BoundEnvVar, value: string, store: AppState) => {
 			store.updateNodeData(id, {
@@ -589,11 +640,19 @@
 						return o;
 					}
 					if (value) {
-						return {
-							...o,
-							isEditting: false,
-							alias: value.toUpperCase(),
-						};
+						if ("name" in o && value.toUpperCase() === o.name.toUpperCase()) {
+							return {
+								...o,
+								isEditting: false,
+								alias: undefined,
+							};
+						} else {
+							return {
+								...o,
+								isEditting: false,
+								alias: value.toUpperCase(),
+							};
+						}
 					}
 					if ("alias" in o) {
 						const { alias: _, ...rest } = o;
@@ -611,17 +670,23 @@
 		},
 		[id, data],
 	);
+
 	const saveAliasOnEnter = useCallback(
 		(e: BoundEnvVar) => {
 			return (event: KeyboardEvent<HTMLInputElement>) => {
 				if (event.key === "Enter") {
-					event.preventDefault();
 					saveAlias(e, event.currentTarget.value, store);
+				} else if (event.key === "Escape") {
+					store.updateNodeData(id, {
+						...data,
+						envVars: data.envVars!.map((o) => (o.id === e.id ? { ...o, isEditting: false } : o)),
+					});
 				}
 			};
 		},
-		[store, saveAlias],
+		[store, saveAlias, id, data],
 	);
+
 	const saveAliasOnBlur = useCallback(
 		(e: BoundEnvVar) => {
 			return (event: FocusEvent<HTMLInputElement>) => {
@@ -630,48 +695,153 @@
 		},
 		[store, saveAlias],
 	);
+
 	return (
-		<ul>
-			{data &&
-				data.envVars &&
-				data.envVars.map((v) => {
+		<div className="flex flex-col gap-1">
+			<div className="grid grid-cols-[auto_1fr_1fr_auto] gap-1">
+				{data?.envVars?.map((v) => {
+					if ("value" in v) {
+						if (v.isEditting) {
+							return (
+								<div key={v.id} className="contents">
+									<Input
+										className="uppercase col-start-2"
+										defaultValue={v.name}
+										onKeyUp={(e) => {
+											if (e.key === "Enter") {
+												const nameInput = e.currentTarget;
+												const valueInput = nameInput.parentElement?.querySelector(
+													'input[placeholder="Value"]',
+												) as HTMLInputElement;
+												if (valueInput) {
+													saveValueEnvVar(v.id, nameInput.value, valueInput.value);
+												}
+											} else if (e.key === "Escape") {
+												store.updateNodeData(id, {
+													...data,
+													envVars: data.envVars!.map((o) =>
+														o.id === v.id ? { ...o, isEditting: false } : o,
+													),
+												});
+											}
+										}}
+										autoFocus
+										disabled={disabled}
+									/>
+									<Input
+										placeholder="Value"
+										defaultValue={v.value}
+										onKeyUp={(e) => {
+											if (e.key === "Enter") {
+												const valueInput = e.currentTarget;
+												const nameInput = valueInput.parentElement?.querySelector(
+													'input:not([placeholder="Value"])',
+												) as HTMLInputElement;
+												if (nameInput) {
+													saveValueEnvVar(v.id, nameInput.value, valueInput.value);
+												}
+											} else if (e.key === "Escape") {
+												store.updateNodeData(id, {
+													...data,
+													envVars: data.envVars!.map((o) =>
+														o.id === v.id ? { ...o, isEditting: false } : o,
+													),
+												});
+											}
+										}}
+										disabled={disabled}
+									/>
+									<Button
+										variant="destructive"
+										size="sm"
+										onClick={() => removeEnvVar(v.id)}
+										disabled={disabled}
+									>
+										Remove
+									</Button>
+								</div>
+							);
+						}
+						return (
+							<div
+								key={v.id}
+								className={`contents ${disabled ? "" : "cursor-text"}`}
+								onClick={() => editValueEnvVar(v.id)}
+							>
+								<div>{!disabled && <Pencil className="w-4 h-4" />}</div>
+								<div className={`${disabled ? "col-span-2" : ""} col-start-2`}>{v.name}</div>
+								<div>{v.value}</div>
+								<Button
+									variant="destructive"
+									size="sm"
+									onClick={(e) => {
+										e.stopPropagation();
+										removeEnvVar(v.id);
+									}}
+									disabled={disabled}
+								>
+									Remove
+								</Button>
+							</div>
+						);
+					}
 					if ("name" in v) {
 						const value = "alias" in v ? v.alias : v.name;
 						if (v.isEditting) {
 							return (
-								<li key={v.id}>
-									<Input
-										type="text"
-										className="uppercase"
-										defaultValue={value}
-										onKeyUp={saveAliasOnEnter(v)}
-										onBlur={saveAliasOnBlur(v)}
-										autoFocus={true}
-										disabled={disabled}
-									/>
-								</li>
+								<Input
+									type="text"
+									className="uppercase col-start-2 col-span-3"
+									defaultValue={value}
+									onKeyUp={saveAliasOnEnter(v)}
+									onBlur={saveAliasOnBlur(v)}
+									autoFocus={true}
+									disabled={disabled}
+								/>
 							);
 						}
 						return (
-							<li key={v.id} onClick={editAlias(v)}>
-								<TooltipProvider>
-									<Tooltip>
-										<TooltipTrigger className="w-full">
-											<div
-												className={`w-full flex flex-row items-center gap-1 ${disabled ? "" : "cursor-text"}`}
-											>
-												{!disabled && <Pencil className="w-4 h-4" />}
-												<div className="uppercase">{value}</div>
-											</div>
-										</TooltipTrigger>
-										<TooltipContent>{v.name}</TooltipContent>
-									</Tooltip>
-								</TooltipProvider>
-							</li>
+							<div
+								key={v.id}
+								onClick={editAlias(v)}
+								className={`contents ${disabled ? "" : "cursor-text"}`}
+							>
+								{!disabled && <Pencil className="w-4 h-4" />}
+								<div className="col-start-2 col-span-3">
+									<TooltipProvider>
+										<Tooltip>
+											<TooltipTrigger className="uppercase">{value}</TooltipTrigger>
+											<TooltipContent>{v.name}</TooltipContent>
+										</Tooltip>
+									</TooltipProvider>
+								</div>
+							</div>
 						);
 					}
+					return null;
 				})}
-		</ul>
+				{!disabled && (
+					<div className="contents">
+						<Input
+							placeholder="Name"
+							className="uppercase col-start-2"
+							value={name}
+							onChange={(e) => setName(e.target.value)}
+							disabled={disabled}
+						/>
+						<Input
+							placeholder="Value"
+							value={value}
+							onChange={(e) => setValue(e.target.value)}
+							disabled={disabled}
+						/>
+						<Button onClick={addEnvVar} disabled={disabled || !name.trim() || !value.trim()}>
+							Add
+						</Button>
+					</div>
+				)}
+			</div>
+		</div>
 	);
 }