diff --git a/apps/web/src/components/EnvironmentVariablesEditor.test.tsx b/apps/web/src/components/EnvironmentVariablesEditor.test.tsx new file mode 100644 index 000000000..0478a88c7 --- /dev/null +++ b/apps/web/src/components/EnvironmentVariablesEditor.test.tsx @@ -0,0 +1,23 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; + +import { EnvironmentVariablesEditor } from "./EnvironmentVariablesEditor"; + +describe("EnvironmentVariablesEditor", () => { + it("hides saved values by default and shows an eye toggle", () => { + const markup = renderToStaticMarkup( + entries} + />, + ); + + expect(markup).toContain('aria-label="Show value"'); + expect(markup).toContain("lucide-eye"); + expect(markup).toContain("-webkit-text-security:disc"); + }); +}); diff --git a/apps/web/src/components/EnvironmentVariablesEditor.tsx b/apps/web/src/components/EnvironmentVariablesEditor.tsx index 27f7a0fb2..44809a620 100644 --- a/apps/web/src/components/EnvironmentVariablesEditor.tsx +++ b/apps/web/src/components/EnvironmentVariablesEditor.tsx @@ -1,5 +1,5 @@ -import { PlusIcon, Trash2Icon } from "lucide-react"; -import { type ReactNode, useEffect, useState } from "react"; +import { EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-react"; +import { type CSSProperties, type ReactNode, useEffect, useState } from "react"; import { ENVIRONMENT_VARIABLE_KEY_MAX_LENGTH, ENVIRONMENT_VARIABLE_MAX_COUNT, @@ -10,6 +10,7 @@ import { import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Textarea } from "./ui/textarea"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { cn } from "~/lib/utils"; type DraftRow = { @@ -121,11 +122,13 @@ export function EnvironmentVariablesEditor({ disabled = false, }: EnvironmentVariablesEditorProps) { const [rows, setRows] = useState(() => rowsFromEntries(entries)); + const [visibleValueRowIds, setVisibleValueRowIds] = useState>(() => new Set()); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); useEffect(() => { setRows(rowsFromEntries(entries)); + setVisibleValueRowIds(new Set()); setSaveError(null); }, [entries]); @@ -142,6 +145,18 @@ export function EnvironmentVariablesEditor({ setSaveError(null); }; + const toggleValueVisibility = (rowId: string) => { + setVisibleValueRowIds((current) => { + const next = new Set(current); + if (next.has(rowId)) { + next.delete(rowId); + } else { + next.add(rowId); + } + return next; + }); + }; + const addRow = () => { if (!canAddRow) return; setRows((current) => [...current, createDraftRow()]); @@ -150,11 +165,18 @@ export function EnvironmentVariablesEditor({ const removeRow = (rowId: string) => { setRows((current) => current.filter((row) => row.id !== rowId)); + setVisibleValueRowIds((current) => { + if (!current.has(rowId)) return current; + const next = new Set(current); + next.delete(rowId); + return next; + }); setSaveError(null); }; const resetRows = () => { setRows(rowsFromEntries(entries)); + setVisibleValueRowIds(new Set()); setSaveError(null); }; @@ -165,6 +187,7 @@ export function EnvironmentVariablesEditor({ try { const savedEntries = await onSave(normalizedEntries); setRows(rowsFromEntries(savedEntries)); + setVisibleValueRowIds(new Set()); } catch (error) { setSaveError( error instanceof Error ? error.message : "Failed to save environment variables.", @@ -215,6 +238,13 @@ export function EnvironmentVariablesEditor({ {rows.map((row, index) => { const errors = rowErrors.get(row.id); const isBlank = !hasDraftMeaningfulContent(row); + const isValueVisible = visibleValueRowIds.has(row.id); + const valueFieldId = `env-var-value-${row.id}`; + const valueMaskStyle = { + WebkitTextSecurity: isValueVisible ? "none" : "disc", + } as CSSProperties & { + WebkitTextSecurity?: string; + }; return (
-