From 1ba5b8e0299380545ae37aa798bac4d5e2338582 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-FE Date: Sun, 17 May 2026 22:06:29 +0000 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20honest=20secrets=20UI=20?= =?UTF-8?q?=E2=80=94=20write-only=20indicator=20+=20honest=20Test=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two staging-port fixes addressing internal#490 and internal#491: - SecretRow: remove dead RevealToggle (value is write-only, never readable). Replace with honest πŸ”’ indicator + aria-label explaining rotation is via Edit. Removes revealed-state hooks and AUTO_HIDE_MS. Adds .secret-row__write-only CSS class. - TestConnectionButton: catch block now distinguishes network/abort errors (no server answer β†’ "Could not reach…") from server-answered ApiErrors (404/501 β†’ "not available yet", other β†’ status code). Uses duck-typed isApiError() guard for test mock compatibility. Test updates: SecretRow test checks write-only indicator; both TestConnectionButton test suites updated for new honest error copy. Co-Authored-By: Claude Opus 4.7 --- .../__tests__/TestConnectionButton.test.tsx | 7 ++-- canvas/src/components/settings/SecretRow.tsx | 42 +++++++++---------- .../settings/__tests__/SecretRow.test.tsx | 5 ++- .../components/ui/TestConnectionButton.tsx | 23 +++++++++- .../__tests__/TestConnectionButton.test.tsx | 6 ++- canvas/src/styles/settings-panel.css | 9 ++++ 6 files changed, 60 insertions(+), 32 deletions(-) diff --git a/canvas/src/components/__tests__/TestConnectionButton.test.tsx b/canvas/src/components/__tests__/TestConnectionButton.test.tsx index 15f1dd9cf..207a37a8f 100644 --- a/canvas/src/components/__tests__/TestConnectionButton.test.tsx +++ b/canvas/src/components/__tests__/TestConnectionButton.test.tsx @@ -102,7 +102,7 @@ describe("TestConnectionButton β€” state machine", () => { expect(screen.getByText("Permission denied")).toBeTruthy(); }); - it("shows generic error message on unexpected exception", async () => { + it("shows honest network error on unexpected exception", async () => { vi.mocked(validateSecret).mockRejectedValue(new Error("timeout")); render(); @@ -110,8 +110,9 @@ describe("TestConnectionButton β€” state machine", () => { await act(async () => { /* flush */ }); expect(screen.getByRole("alert")).toBeTruthy(); - // The error detail is hardcoded to "Connection timed out. Service may be down." - expect(document.body.querySelector('[role="alert"]')?.textContent).toMatch(/timed out/i); + // The new error detail distinguishes network/abort errors (no server answer) + // from server-answered ApiErrors (404 = not available, other = status code). + expect(document.body.querySelector('[role="alert"]')?.textContent).toMatch(/Could not reach/i); }); }); diff --git a/canvas/src/components/settings/SecretRow.tsx b/canvas/src/components/settings/SecretRow.tsx index 92bb633ea..10339e869 100644 --- a/canvas/src/components/settings/SecretRow.tsx +++ b/canvas/src/components/settings/SecretRow.tsx @@ -3,16 +3,24 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import type { Secret, SecretGroup } from '@/types/secrets'; import { useSecretsStore } from '@/stores/secrets-store'; import { StatusBadge } from '@/components/ui/StatusBadge'; -import { RevealToggle } from '@/components/ui/RevealToggle'; import { KeyValueField } from '@/components/ui/KeyValueField'; import { ValidationHint } from '@/components/ui/ValidationHint'; import { TestConnectionButton } from '@/components/ui/TestConnectionButton'; import { validateSecretValue } from '@/lib/validation/secret-formats'; import { SERVICES } from '@/lib/services'; -const AUTO_HIDE_MS = 30_000; const VALIDATION_DEBOUNCE_MS = 400; +// Secret values are write-only from the browser: the server List endpoint +// "Never exposes values", there is no per-secret decrypt route, and the +// only decrypted path (GET /secrets/values) is bulk + token-gated for +// remote agents. The old eye/RevealToggle was a dead affordance β€” it +// flipped its own icon but could never reveal anything, which read as +// "this doesn't work" (esp. once clicked β†’ eye-with-slash). We show an +// honest static indicator instead; rotation is via Edit. +const WRITE_ONLY_TITLE = + 'Value is write-only and cannot be revealed β€” use Edit to replace/rotate it'; + interface SecretRowProps { secret: Secret; workspaceId: string; @@ -31,28 +39,12 @@ export function SecretRow({ secret, workspaceId }: SecretRowProps) { const setSecretStatus = useSecretsStore((s) => s.setSecretStatus); const isEditing = editingKey === secret.name; - const [revealed, setRevealed] = useState(false); const [editValue, setEditValue] = useState(''); const [validationError, setValidationError] = useState(null); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); const debounceRef = useRef>(undefined); const editBtnRef = useRef(null); - const revealTimerRef = useRef>(undefined); - - // Auto-hide revealed value after 30s - useEffect(() => { - if (revealed) { - clearTimeout(revealTimerRef.current); - revealTimerRef.current = setTimeout(() => setRevealed(false), AUTO_HIDE_MS); - return () => clearTimeout(revealTimerRef.current); - } - }, [revealed]); - - // Reset revealed state when panel closes (session-only) - useEffect(() => { - return () => setRevealed(false); - }, []); // Debounced validation useEffect(() => { @@ -133,11 +125,15 @@ export function SecretRow({ secret, workspaceId }: SecretRowProps) { {secret.masked_value}
- setRevealed((r) => !r)} - label={`Toggle reveal ${secret.name}`} - /> + + πŸ”’ +