korbit-ai[bot] commented on code in PR #35584:
URL: https://github.com/apache/superset/pull/35584#discussion_r2417494571


##########
superset-frontend/src/utils/export.ts:
##########
@@ -16,34 +16,60 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import parseCookie from 'src/utils/parseCookie';
+import { SupersetClient, logging } from '@superset-ui/core';
 import rison from 'rison';
-import { nanoid } from 'nanoid';
+import contentDisposition from 'content-disposition';
 import { ensureAppRoot } from './pathUtils';
 
-export default function handleResourceExport(
+export default async function handleResourceExport(
   resource: string,
   ids: number[],
   done: () => void,
-  interval = 200,
-): void {
-  const token = nanoid();
-  const url = ensureAppRoot(
-    `/api/v1/${resource}/export/?q=${rison.encode(ids)}&token=${token}`,
+): Promise<void> {
+  const endpoint = ensureAppRoot(
+    `/api/v1/${resource}/export/?q=${rison.encode(ids)}`,
   );
 
-  // create new iframe for export
-  const iframe = document.createElement('iframe');
-  iframe.style.display = 'none';
-  iframe.src = url;
-  document.body.appendChild(iframe);
+  try {
+    // Use fetch with blob response instead of iframe to avoid CSP frame-src 
violations
+    const response = await SupersetClient.get({
+      endpoint,
+      headers: {
+        Accept: 'application/zip, application/x-zip-compressed, text/plain',
+      },
+      parseMethod: 'raw',
+    });
 
-  const timer = window.setInterval(() => {
-    const cookie: { [cookieId: string]: string } = parseCookie();
-    if (cookie[token] === 'done') {
-      window.clearInterval(timer);
-      document.body.removeChild(iframe);
-      done();
+    // Parse filename from Content-Disposition header
+    const disposition = response.headers.get('Content-Disposition');
+    let fileName = `${resource}_export.zip`;
+
+    if (disposition) {
+      try {
+        const parsed = contentDisposition.parse(disposition);
+        if (parsed?.parameters?.filename) {
+          fileName = parsed.parameters.filename;
+        }
+      } catch (error) {
+        logging.warn('Failed to parse Content-Disposition header:', error);
+      }
     }
-  }, interval);
+
+    // Convert response to blob and trigger download
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = fileName;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);

Review Comment:
   ### Unnecessary DOM layout thrashing <sub>![category 
Performance](https://img.shields.io/badge/Performance-4f46e5)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   Synchronous DOM manipulation with appendChild/removeChild causes unnecessary 
layout thrashing for the download trigger.
   
   
   ###### Why this matters
   The browser performs layout calculations when adding and immediately 
removing the anchor element, which is wasteful since the element is never meant 
to be visible.
   
   ###### Suggested change ∙ *Feature Preview*
   Use a more efficient approach that avoids DOM manipulation:
   ```typescript
   const a = document.createElement('a');
   a.href = url;
   a.download = fileName;
   a.style.display = 'none';
   a.click();
   ```
   Or use the modern approach:
   ```typescript
   if ('showSaveFilePicker' in window) {
     // Use File System Access API when available
   } else {
     // Fallback to current approach
   }
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/fc9c882b-b0eb-4f77-b218-117a8723f669/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/fc9c882b-b0eb-4f77-b218-117a8723f669?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/fc9c882b-b0eb-4f77-b218-117a8723f669?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/fc9c882b-b0eb-4f77-b218-117a8723f669?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/fc9c882b-b0eb-4f77-b218-117a8723f669)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:7b7271ec-7f28-4c84-bfcb-fe24163ee3a1 -->
   
   
   [](7b7271ec-7f28-4c84-bfcb-fe24163ee3a1)



##########
superset-frontend/src/utils/export.ts:
##########
@@ -16,34 +16,60 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import parseCookie from 'src/utils/parseCookie';
+import { SupersetClient, logging } from '@superset-ui/core';
 import rison from 'rison';
-import { nanoid } from 'nanoid';
+import contentDisposition from 'content-disposition';
 import { ensureAppRoot } from './pathUtils';
 
-export default function handleResourceExport(
+export default async function handleResourceExport(
   resource: string,
   ids: number[],
   done: () => void,
-  interval = 200,
-): void {
-  const token = nanoid();
-  const url = ensureAppRoot(
-    `/api/v1/${resource}/export/?q=${rison.encode(ids)}&token=${token}`,
+): Promise<void> {
+  const endpoint = ensureAppRoot(
+    `/api/v1/${resource}/export/?q=${rison.encode(ids)}`,
   );
 
-  // create new iframe for export
-  const iframe = document.createElement('iframe');
-  iframe.style.display = 'none';
-  iframe.src = url;
-  document.body.appendChild(iframe);
+  try {
+    // Use fetch with blob response instead of iframe to avoid CSP frame-src 
violations
+    const response = await SupersetClient.get({
+      endpoint,
+      headers: {
+        Accept: 'application/zip, application/x-zip-compressed, text/plain',
+      },
+      parseMethod: 'raw',
+    });
 
-  const timer = window.setInterval(() => {
-    const cookie: { [cookieId: string]: string } = parseCookie();
-    if (cookie[token] === 'done') {
-      window.clearInterval(timer);
-      document.body.removeChild(iframe);
-      done();
+    // Parse filename from Content-Disposition header
+    const disposition = response.headers.get('Content-Disposition');
+    let fileName = `${resource}_export.zip`;
+
+    if (disposition) {
+      try {
+        const parsed = contentDisposition.parse(disposition);
+        if (parsed?.parameters?.filename) {
+          fileName = parsed.parameters.filename;
+        }
+      } catch (error) {
+        logging.warn('Failed to parse Content-Disposition header:', error);
+      }
     }
-  }, interval);
+
+    // Convert response to blob and trigger download
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = fileName;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    window.URL.revokeObjectURL(url);

Review Comment:
   ### Mixed responsibilities <sub>![category 
Design](https://img.shields.io/badge/Design-0d9488)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   The file download logic is mixed with the resource export logic, violating 
the Single Responsibility Principle.
   
   
   ###### Why this matters
   This mixing of concerns makes the code less reusable and harder to test. The 
file download functionality could be needed elsewhere and should be separated.
   
   ###### Suggested change ∙ *Feature Preview*
   ```typescript
   // Extract to separate utility function
   async function downloadBlob(blob: Blob, fileName: string): Promise<void> {
     const url = window.URL.createObjectURL(blob);
     try {
       const a = document.createElement('a');
       a.href = url;
       a.download = fileName;
       document.body.appendChild(a);
       a.click();
       document.body.removeChild(a);
     } finally {
       window.URL.revokeObjectURL(url);
     }
   }
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/284f416e-112a-4eec-ba90-ca52b55d8ef1/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/284f416e-112a-4eec-ba90-ca52b55d8ef1?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/284f416e-112a-4eec-ba90-ca52b55d8ef1?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/284f416e-112a-4eec-ba90-ca52b55d8ef1?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/284f416e-112a-4eec-ba90-ca52b55d8ef1)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:0da801e4-8a2d-4d3d-98f6-05cf9448db01 -->
   
   
   [](0da801e4-8a2d-4d3d-98f6-05cf9448db01)



##########
superset-frontend/src/utils/export.ts:
##########
@@ -16,34 +16,60 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import parseCookie from 'src/utils/parseCookie';
+import { SupersetClient, logging } from '@superset-ui/core';
 import rison from 'rison';
-import { nanoid } from 'nanoid';
+import contentDisposition from 'content-disposition';
 import { ensureAppRoot } from './pathUtils';
 
-export default function handleResourceExport(
+export default async function handleResourceExport(
   resource: string,
   ids: number[],
   done: () => void,
-  interval = 200,
-): void {
-  const token = nanoid();
-  const url = ensureAppRoot(
-    `/api/v1/${resource}/export/?q=${rison.encode(ids)}&token=${token}`,
+): Promise<void> {
+  const endpoint = ensureAppRoot(
+    `/api/v1/${resource}/export/?q=${rison.encode(ids)}`,
   );
 
-  // create new iframe for export
-  const iframe = document.createElement('iframe');
-  iframe.style.display = 'none';
-  iframe.src = url;
-  document.body.appendChild(iframe);
+  try {
+    // Use fetch with blob response instead of iframe to avoid CSP frame-src 
violations
+    const response = await SupersetClient.get({
+      endpoint,
+      headers: {
+        Accept: 'application/zip, application/x-zip-compressed, text/plain',
+      },
+      parseMethod: 'raw',
+    });
 
-  const timer = window.setInterval(() => {
-    const cookie: { [cookieId: string]: string } = parseCookie();
-    if (cookie[token] === 'done') {
-      window.clearInterval(timer);
-      document.body.removeChild(iframe);
-      done();
+    // Parse filename from Content-Disposition header
+    const disposition = response.headers.get('Content-Disposition');
+    let fileName = `${resource}_export.zip`;
+
+    if (disposition) {
+      try {
+        const parsed = contentDisposition.parse(disposition);
+        if (parsed?.parameters?.filename) {
+          fileName = parsed.parameters.filename;
+        }
+      } catch (error) {
+        logging.warn('Failed to parse Content-Disposition header:', error);
+      }
     }
-  }, interval);
+
+    // Convert response to blob and trigger download
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);

Review Comment:
   ### Unbounded memory usage for large exports <sub>![category 
Performance](https://img.shields.io/badge/Performance-4f46e5)</sub>
   
   <details>
     <summary>Tell me more</summary>
   
   ###### What is the issue?
   Creating object URLs for potentially large export files without size 
validation can cause memory issues and browser crashes.
   
   
   ###### Why this matters
   Large export files loaded entirely into memory as blobs can exhaust 
available RAM, especially on resource-constrained devices or when multiple 
exports are triggered simultaneously.
   
   ###### Suggested change ∙ *Feature Preview*
   Add content-length validation before blob creation and consider streaming 
downloads for large files:
   ```typescript
   const contentLength = response.headers.get('Content-Length');
   const MAX_BLOB_SIZE = 100 * 1024 * 1024; // 100MB limit
   
   if (contentLength && parseInt(contentLength, 10) > MAX_BLOB_SIZE) {
     // Use streaming approach or direct link download
     window.location.href = endpoint;
     return;
   }
   
   const blob = await response.blob();
   ```
   
   
   ###### Provide feedback to improve future suggestions
   [![Nice 
Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/4e60cc02-9860-4f69-93a3-92397676e6e8/upvote)
 
[![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/4e60cc02-9860-4f69-93a3-92397676e6e8?what_not_true=true)
  [![Not in 
Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/4e60cc02-9860-4f69-93a3-92397676e6e8?what_out_of_scope=true)
 [![Not in coding 
standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/4e60cc02-9860-4f69-93a3-92397676e6e8?what_not_in_standard=true)
 
[![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/4e60cc02-9860-4f69-93a3-92397676e6e8)
   </details>
   
   <sub>
   
   💬 Looking for more details? Reply to this comment to chat with Korbit.
   </sub>
   
   <!--- korbi internal id:fcda2314-f400-47ef-bad8-1d3be86951eb -->
   
   
   [](fcda2314-f400-47ef-bad8-1d3be86951eb)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to