diff --git a/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.test.tsx b/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.test.tsx index 3b0f97c5..54247880 100644 --- a/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.test.tsx +++ b/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import Attachment from "./Attachment"; @@ -199,6 +199,30 @@ describe("Attachment", () => { expect(container.textContent).toContain("Expand to preview attachment"); }); + + it("should toggle between expanded and collapsed when the prompt is clicked", () => { + const { container } = render( + , + ); + + // Collapsed by default: shows the prompt, no content decoded yet. + expect(container.textContent).toContain("Expand to preview attachment"); + expect(container.textContent).not.toContain("Hello, World!"); + expect(mockCreateObjectURL).not.toHaveBeenCalled(); + + // Expanding renders the content and builds the download URL on demand. + fireEvent.click(screen.getByRole("button", { name: /expand to preview attachment/i })); + expect(container.textContent).toContain("Hello, World!"); + expect(mockCreateObjectURL).toHaveBeenCalledTimes(1); + + // Collapsing hides the content again. + fireEvent.click(screen.getByRole("button", { name: /collapse attachment/i })); + expect(container.textContent).toContain("Expand to preview attachment"); + expect(container.textContent).not.toContain("Hello, World!"); + }); }); describe("cleanup", () => { diff --git a/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.tsx b/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.tsx index 9f7b458f..83132d3f 100644 --- a/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.tsx +++ b/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.tsx @@ -1,7 +1,7 @@ import type { EnvelopeItem } from "@sentry/core"; import { ReactComponent as Download } from "@spotlight/ui/assets/download.svg"; import { base64Decode, safeAtob } from "@spotlight/ui/lib/base64"; -import { type ReactNode, useEffect, useMemo } from "react"; +import { type ReactNode, useEffect, useMemo, useState } from "react"; import JsonViewer from "../../shared/JsonViewer"; import { CodeViewer } from "./CodeViewer"; import { inferExtension } from "./contentType"; @@ -23,10 +23,15 @@ export default function Attachment({ const extension = inferExtension(header.content_type as string | null, header.type as string | null); const name = (header.filename as string) || `untitled.${extension}`; + // Lazily render the attachment: start from the `expanded` prop, but let the + // user expand it on demand so previews/downloads work wherever the component + // is used, without eagerly decoding every attachment. + const [isExpanded, setIsExpanded] = useState(expanded); + // Create download URL for attachment // Returns: string (success), null (decode error), undefined (not expanded yet) const downloadUrl = useMemo(() => { - if (!expanded) { + if (!isExpanded) { return undefined; // Not needed yet } @@ -54,7 +59,7 @@ export default function Attachment({ const blob = new Blob([blobData], { type: contentType || "application/octet-stream" }); return URL.createObjectURL(blob); - }, [expanded, attachment, extension, header.content_type]); + }, [isExpanded, attachment, extension, header.content_type]); // Cleanup blob URL on unmount or when URL changes useEffect(() => { @@ -69,7 +74,7 @@ export default function Attachment({ let content: ReactNode = null; - if (expanded) { + if (isExpanded) { if (decodeError) { content = (
@@ -114,7 +119,17 @@ export default function Attachment({
           
         
       
-      {expanded ? content : 

Expand to preview attachment.

} +
+ + {isExpanded ? content : null} +
); }