FN-5878: re-hide revealed secrets from the eye toggle

Let users hide a revealed secret again from the row visibility toggle.

- add a hideSecret helper that clears reveal timers and resets the revealed value
- make the row eye toggle switch between reveal and hide states instead of reveal-only
- add a SecretsView regression test covering reveal, hide, and copy button state changes

Files changed:
 packages/dashboard/app/components/SecretsView.tsx         | 18 +++++++++-
 packages/dashboard/app/components/__tests__/SecretsView.test.tsx | 40 ++++++++++++++++++++++
 2 files changed, 57 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-5878

Fusion-Task-Lineage: 936195db-88c9-48f5-8273-9b930bf6b858
This commit is contained in:
gsxdsm
2026-06-02 08:14:10 -07:00
parent dd1024fe0b
commit 8b839d79e8
2 changed files with 57 additions and 1 deletions

View File

@@ -222,12 +222,22 @@ export const SecretsView = ({ addToast }: SecretsViewProps) => {
}
};
const hideSecret = (secret: SecretRecord) => {
const existing = revealTimersRef.current.get(secret.id);
if (existing) {
clearTimeout(existing);
revealTimersRef.current.delete(secret.id);
}
setRevealedValues((current) => ({ ...current, [secret.id]: null }));
};
const revealSecret = async (secret: SecretRecord) => {
const data = await request<{ key: string; value: string }>(`/api/secrets/${secret.scope}/${secret.id}/reveal`, { method: "POST" });
setRevealedValues((current) => ({ ...current, [secret.id]: data.value }));
addToast?.("Revealed", "success");
const timer = setTimeout(() => {
setRevealedValues((current) => ({ ...current, [secret.id]: null }));
revealTimersRef.current.delete(secret.id);
}, 30000);
const existing = revealTimersRef.current.get(secret.id);
if (existing) clearTimeout(existing);
@@ -314,7 +324,13 @@ export const SecretsView = ({ addToast }: SecretsViewProps) => {
<button
type="button"
className="btn btn-icon secrets-visibility-toggle"
onClick={() => void revealSecret(secret)}
onClick={() => {
if (revealed) {
hideSecret(secret);
return;
}
void revealSecret(secret);
}}
aria-label={revealed ? "Hide" : "Reveal"}
>
{revealed ? <EyeOff {...actionIconProps} /> : <Eye {...actionIconProps} />}

View File

@@ -225,6 +225,46 @@ describe("SecretsView", () => {
expectVisibleActionIcon(screen.getByRole("button", { name: "Delete" }));
});
it("revealed secret can be hidden again from the row toggle", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
mockJsonResponse({
ok: true,
body: {
secrets: [
{ id: "secret-1", key: "VISIBLE", scope: "project", description: null, accessPolicy: "prompt", envExportable: false, envExportKey: null, lastReadAt: null },
],
},
}),
)
.mockResolvedValueOnce(mockJsonResponse({ ok: true, body: { configured: false } }))
.mockResolvedValueOnce(mockJsonResponse({ ok: true, body: { key: "VISIBLE", value: "super-secret-value" } }));
vi.stubGlobal("fetch", fetchMock);
render(<SecretsView addToast={vi.fn()} />);
await screen.findByText("VISIBLE");
const revealButton = screen.getByRole("button", { name: "Reveal" });
const copyButton = screen.getByRole("button", { name: "Copy" });
expect(copyButton).toBeDisabled();
await userEvent.click(revealButton);
expect(await screen.findByText("super-secret-value")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Hide" })).toBeInTheDocument();
expect(copyButton).toBeEnabled();
await userEvent.click(screen.getByRole("button", { name: "Hide" }));
await waitFor(() => {
expect(screen.queryByText("super-secret-value")).not.toBeInTheDocument();
});
expect(screen.getByRole("button", { name: "Reveal" })).toBeInTheDocument();
expect(copyButton).toBeDisabled();
expect(fetchMock).toHaveBeenCalledTimes(3);
});
it.each(["dark", "light"] as const)("keeps the modal value toggle icon visible in %s theme", async (theme) => {
document.documentElement.dataset.theme = theme;