Copilot commented on code in PR #37679:
URL: https://github.com/apache/superset/pull/37679#discussion_r2885677650


##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/useBulkExport.ts:
##########
@@ -0,0 +1,254 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useCallback } from 'react';
+import { SupersetClient } from '@superset-ui/core';
+import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
+import { ensureAppRoot } from 'src/utils/pathUtils';
+
+export interface ChartExportData {
+  chartId: number;
+  chartName: string;
+  data: any[];

Review Comment:
   New code introduces `any[]` for `ChartExportData.data`. AGENTS.md asks to 
avoid `any`; prefer a typed row shape (often `Record<string, 
unknown>`/`JsonObject`) or `unknown[]` with narrowing so exported data handling 
stays type-safe.
   ```suggestion
     data: Record<string, unknown>[];
   ```



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/index.tsx:
##########
@@ -0,0 +1,286 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useEffect, useMemo } from 'react';
+import { styled, t } from '@apache-superset/core';
+import { Modal, Button } from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { ChartSelector, ChartInfo } from './ChartSelector';
+import { ExportProgress } from './ExportProgress';
+import { useBulkExport } from './useBulkExport';
+
+const ModalContent = styled.div`
+  min-height: 400px;
+`;
+
+const Description = styled.div`
+  ${({ theme }) => `
+    margin-bottom: ${theme.sizeUnit * 4}px;
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const WarningMessage = styled.div`
+  ${({ theme }) => `
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    background-color: ${theme.colorWarningBg};
+    border: 1px solid ${theme.colorWarningBorder};
+    border-radius: ${theme.sizeUnit}px;
+    color: ${theme.colorWarningText};
+    font-size: ${theme.sizeUnit * 3.5}px;
+    margin-bottom: ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+const SummaryMessage = styled.div<{ success?: boolean }>`
+  ${({ theme, success }) => `
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    background-color: ${success ? theme.colorSuccessBg : theme.colorErrorBg};
+    border: 1px solid ${success ? theme.colorSuccessBorder : 
theme.colorErrorBorder};
+    border-radius: ${theme.sizeUnit}px;
+    color: ${success ? theme.colorSuccessText : theme.colorErrorText};
+    font-size: ${theme.sizeUnit * 3.5}px;
+    margin-bottom: ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+export interface ExportDashboardDataModalProps {
+  show: boolean;
+  onHide: () => void;
+  dashboardTitle: string;
+  charts: ChartInfo[];
+  slices: Record<number, any>; // Slice entities from Redux
+}
+
+export const ExportDashboardDataModal = ({
+  show,
+  onHide,
+  dashboardTitle,
+  charts,
+  slices,
+}: ExportDashboardDataModalProps) => {
+  const { addSuccessToast, addDangerToast, addWarningToast } = useToasts();
+  const [selectedChartIds, setSelectedChartIds] = useState<Set<number>>(
+    new Set(),
+  );
+  const [exportComplete, setExportComplete] = useState(false);
+
+  const { isExporting, progress, exportCharts, generateExcelFile, reset } =
+    useBulkExport({
+      onComplete: async results => {
+        const successCount = results.filter(r => r.status === 
'success').length;
+        const failureCount = results.length - successCount;
+
+        if (successCount > 0) {
+          try {
+            const filename = await generateExcelFile(results, dashboardTitle);
+            addSuccessToast(
+              failureCount > 0
+                ? t(
+                    '%s of %s charts exported successfully to %s',
+                    successCount,
+                    results.length,
+                    filename,
+                  )
+                : t(
+                    'All %s charts exported successfully to %s',
+                    successCount,
+                    filename,
+                  ),
+            );
+            setExportComplete(true);
+          } catch (error: any) {
+            addDangerToast(
+              t(
+                'Failed to generate Excel file: %s',
+                error.message || 'Unknown error',
+              ),
+            );
+          }
+        }
+      },
+      onError: error => {
+        addDangerToast(error);
+      },
+    });
+
+  // Initialize with all charts selected
+  useEffect(() => {
+    if (show && charts.length > 0) {
+      const allIds = new Set(charts.map(c => c.id));
+      setSelectedChartIds(allIds);
+      setExportComplete(false);
+      reset();
+    }
+  }, [show, charts, reset]);
+
+  const handleExport = async () => {
+    if (selectedChartIds.size === 0) {
+      addWarningToast(t('Please select at least one chart to export'));
+      return;
+    }
+
+    if (selectedChartIds.size > 20) {
+      addWarningToast(
+        t(
+          'Exporting more than 20 charts may take a while and could impact 
browser performance',
+        ),
+      );
+    }
+
+    // Prepare chart data for export
+    const chartsToExport = Array.from(selectedChartIds)
+      .map(id => {
+        const slice = slices[id];
+        if (!slice) return null;
+
+        return {
+          id,
+          name: slice.slice_name || `Chart ${id}`,
+          formData: slice.form_data || {},
+        };

Review Comment:
   Export requests are built from `slice.form_data` only, which means 
dashboard-applied native filters / dataMask / cross-filter state may be 
ignored. The codebase already has utilities to build chart formData that 
reflects dashboard state (e.g., getFormDataWithExtraFilters + ownState merging 
in embedded/utils.ts); consider reusing that approach so exported data matches 
what users are viewing.



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/useBulkExport.ts:
##########
@@ -0,0 +1,254 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useCallback } from 'react';
+import { SupersetClient } from '@superset-ui/core';
+import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
+import { ensureAppRoot } from 'src/utils/pathUtils';
+
+export interface ChartExportData {
+  chartId: number;
+  chartName: string;
+  data: any[];
+  columns: string[];
+  status: 'pending' | 'exporting' | 'success' | 'error';
+  error?: string;
+}
+
+export interface BulkExportProgress {
+  current: number;
+  total: number;
+  charts: ChartExportData[];
+}
+
+export interface UseBulkExportProps {
+  onComplete?: (results: ChartExportData[]) => void;
+  onError?: (error: string) => void;
+}
+
+export const useBulkExport = ({
+  onComplete,
+  onError,
+}: UseBulkExportProps = {}) => {
+  const [isExporting, setIsExporting] = useState(false);
+  const [progress, setProgress] = useState<BulkExportProgress>({
+    current: 0,
+    total: 0,
+    charts: [],
+  });
+
+  const sanitizeSheetName = (name: string): string => {
+    // Excel sheet names: max 31 chars, no special characters
+    const sanitized = name
+      .replace(/[:\\/?*[\]]/g, '_') // Replace invalid chars
+      .substring(0, 31); // Truncate to 31 chars
+
+    return sanitized || 'Sheet';
+  };
+
+  const exportSingleChart = async (
+    chartId: number,
+    chartName: string,
+    formData: any,
+  ): Promise<ChartExportData> => {
+    const chartData: ChartExportData = {
+      chartId,
+      chartName,
+      data: [],
+      columns: [],
+      status: 'exporting',
+    };
+
+    try {
+      // Build query payload
+      const payload = await buildV1ChartDataPayload({
+        formData: {
+          ...formData,
+          slice_id: chartId,
+        },
+        force: false,
+        resultFormat: 'json',
+        resultType: 'full',
+        setDataMask: () => {},
+        ownState: {},
+      });
+
+      // Call chart data API
+      const response = await SupersetClient.post({
+        endpoint: ensureAppRoot('/api/v1/chart/data'),
+        jsonPayload: payload,
+      });
+
+      // Extract data from response
+      if (response.json?.result?.[0]) {
+        const [queryResult] = response.json.result;
+        chartData.data = queryResult.data || [];
+        chartData.columns = queryResult.colnames || [];
+        chartData.status = 'success';
+      } else {
+        throw new Error('No data returned from API');
+      }
+    } catch (error: any) {
+      chartData.status = 'error';
+      chartData.error = error.message || 'Failed to export chart';
+    }
+
+    return chartData;
+  };
+
+  const exportCharts = useCallback(
+    async (charts: { id: number; name: string; formData: any }[]) => {
+      setIsExporting(true);
+
+      const initialCharts: ChartExportData[] = charts.map(chart => ({
+        chartId: chart.id,
+        chartName: chart.name,
+        data: [],
+        columns: [],
+        status: 'pending',
+      }));
+
+      setProgress({
+        current: 0,
+        total: charts.length,
+        charts: initialCharts,
+      });
+
+      const results: ChartExportData[] = [];
+
+      // Export charts sequentially to avoid overwhelming the server
+      for (let i = 0; i < charts.length; i += 1) {
+        const chart = charts[i];
+
+        // Update progress: mark current chart as exporting
+        setProgress(prev => ({
+          ...prev,
+          current: i + 1,
+          charts: prev.charts.map(c =>
+            c.chartId === chart.id ? { ...c, status: 'exporting' } : c,
+          ),
+        }));
+
+        // Export the chart
+        const result = await exportSingleChart(
+          chart.id,
+          chart.name,
+          chart.formData,
+        );
+        results.push(result);
+
+        // Update progress: mark chart as complete
+        setProgress(prev => ({
+          ...prev,
+          charts: prev.charts.map(c => (c.chartId === chart.id ? result : c)),
+        }));
+      }
+
+      setIsExporting(false);
+
+      // Check if any succeeded
+      const successCount = results.filter(r => r.status === 'success').length;
+
+      if (successCount === 0) {
+        onError?.('All chart exports failed');
+      } else {
+        onComplete?.(results);
+      }
+
+      return results;
+    },
+    [onComplete, onError],
+  );
+
+  const generateExcelFile = useCallback(
+    async (results: ChartExportData[], dashboardTitle: string) => {
+      // Dynamically import xlsx to reduce initial bundle size
+      const XLSX = await import('xlsx');
+
+      const workbook = XLSX.utils.book_new();
+
+      // Track sheet names to handle duplicates
+      const usedSheetNames = new Set<string>();
+
+      results.forEach(result => {
+        if (result.status !== 'success' || !result.data.length) {
+          return; // Skip failed or empty charts
+        }
+
+        // Generate unique sheet name
+        let sheetName = sanitizeSheetName(result.chartName);
+        let counter = 1;
+
+        while (usedSheetNames.has(sheetName)) {
+          const suffix = `(${counter})`;
+          const baseName = sanitizeSheetName(result.chartName).substring(
+            0,
+            31 - suffix.length,
+          );
+          sheetName = `${baseName}${suffix}`;
+          counter += 1;
+        }
+
+        usedSheetNames.add(sheetName);
+
+        // Convert data to worksheet
+        const worksheet = XLSX.utils.json_to_sheet(result.data);
+
+        // Auto-size columns (approximate)
+        const columnWidths = result.columns.map(col => ({
+          wch: Math.max(10, col.length + 2),
+        }));
+        worksheet['!cols'] = columnWidths;
+
+        // Add worksheet to workbook
+        XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
+      });
+
+      // Generate filename with timestamp
+      const timestamp = new Date()
+        .toISOString()
+        .replace(/[:.]/g, '-')
+        .substring(0, 19);
+      const sanitizedTitle = dashboardTitle.replace(/[^a-zA-Z0-9_-]/g, '_');
+      const filename = `${sanitizedTitle}_${timestamp}.xlsx`;
+
+      // Trigger download
+      XLSX.writeFile(workbook, filename);
+
+      return filename;
+    },
+    [],

Review Comment:
   `generateExcelFile` is memoized with useCallback but uses values from the 
outer scope (e.g. sanitizeSheetName). With react-hooks/exhaustive-deps enabled, 
an empty dependency array here will be flagged. Move helpers like 
sanitizeSheetName to module scope, or include dependencies / inline the helper 
to satisfy the hook lint rule.
   ```suggestion
       [sanitizeSheetName],
   ```



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/useBulkExport.ts:
##########
@@ -0,0 +1,254 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useCallback } from 'react';
+import { SupersetClient } from '@superset-ui/core';
+import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
+import { ensureAppRoot } from 'src/utils/pathUtils';
+
+export interface ChartExportData {
+  chartId: number;
+  chartName: string;
+  data: any[];
+  columns: string[];
+  status: 'pending' | 'exporting' | 'success' | 'error';
+  error?: string;
+}
+
+export interface BulkExportProgress {
+  current: number;
+  total: number;
+  charts: ChartExportData[];
+}
+
+export interface UseBulkExportProps {
+  onComplete?: (results: ChartExportData[]) => void;
+  onError?: (error: string) => void;
+}
+
+export const useBulkExport = ({
+  onComplete,
+  onError,
+}: UseBulkExportProps = {}) => {
+  const [isExporting, setIsExporting] = useState(false);
+  const [progress, setProgress] = useState<BulkExportProgress>({
+    current: 0,
+    total: 0,
+    charts: [],
+  });
+
+  const sanitizeSheetName = (name: string): string => {
+    // Excel sheet names: max 31 chars, no special characters
+    const sanitized = name
+      .replace(/[:\\/?*[\]]/g, '_') // Replace invalid chars
+      .substring(0, 31); // Truncate to 31 chars
+
+    return sanitized || 'Sheet';
+  };
+
+  const exportSingleChart = async (
+    chartId: number,
+    chartName: string,
+    formData: any,
+  ): Promise<ChartExportData> => {
+    const chartData: ChartExportData = {
+      chartId,
+      chartName,
+      data: [],
+      columns: [],
+      status: 'exporting',
+    };
+
+    try {
+      // Build query payload
+      const payload = await buildV1ChartDataPayload({
+        formData: {
+          ...formData,
+          slice_id: chartId,
+        },
+        force: false,
+        resultFormat: 'json',
+        resultType: 'full',
+        setDataMask: () => {},
+        ownState: {},
+      });
+
+      // Call chart data API
+      const response = await SupersetClient.post({
+        endpoint: ensureAppRoot('/api/v1/chart/data'),
+        jsonPayload: payload,

Review Comment:
   The chart data request is parsed with SupersetClient’s default `parseMethod` 
(json), but chart payloads are typically parsed with `json-bigint` to preserve 
large numeric values (ChartMetadata defaults parseMethod to json-bigint). 
Consider passing `parseMethod: 'json-bigint'` (or deriving parseMethod from the 
chart’s viz metadata) to avoid precision loss in exported data.
   ```suggestion
           jsonPayload: payload,
           parseMethod: 'json-bigint',
   ```



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/index.tsx:
##########
@@ -0,0 +1,286 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useEffect, useMemo } from 'react';
+import { styled, t } from '@apache-superset/core';
+import { Modal, Button } from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { ChartSelector, ChartInfo } from './ChartSelector';
+import { ExportProgress } from './ExportProgress';
+import { useBulkExport } from './useBulkExport';
+
+const ModalContent = styled.div`
+  min-height: 400px;
+`;
+
+const Description = styled.div`
+  ${({ theme }) => `
+    margin-bottom: ${theme.sizeUnit * 4}px;
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const WarningMessage = styled.div`
+  ${({ theme }) => `
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    background-color: ${theme.colorWarningBg};
+    border: 1px solid ${theme.colorWarningBorder};
+    border-radius: ${theme.sizeUnit}px;
+    color: ${theme.colorWarningText};
+    font-size: ${theme.sizeUnit * 3.5}px;
+    margin-bottom: ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+const SummaryMessage = styled.div<{ success?: boolean }>`
+  ${({ theme, success }) => `
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    background-color: ${success ? theme.colorSuccessBg : theme.colorErrorBg};
+    border: 1px solid ${success ? theme.colorSuccessBorder : 
theme.colorErrorBorder};
+    border-radius: ${theme.sizeUnit}px;
+    color: ${success ? theme.colorSuccessText : theme.colorErrorText};
+    font-size: ${theme.sizeUnit * 3.5}px;
+    margin-bottom: ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+export interface ExportDashboardDataModalProps {
+  show: boolean;
+  onHide: () => void;
+  dashboardTitle: string;
+  charts: ChartInfo[];
+  slices: Record<number, any>; // Slice entities from Redux

Review Comment:
   New frontend code here introduces `any` types (e.g. `slices: Record<number, 
any>`). AGENTS.md explicitly calls out avoiding `any`; please replace this with 
an existing slice entity type (or `unknown` + narrowing) so type errors surface 
at compile time.
   ```suggestion
     slices: Record<number, unknown>; // Slice entities from Redux
   ```



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/index.tsx:
##########
@@ -0,0 +1,286 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useEffect, useMemo } from 'react';
+import { styled, t } from '@apache-superset/core';
+import { Modal, Button } from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { ChartSelector, ChartInfo } from './ChartSelector';
+import { ExportProgress } from './ExportProgress';
+import { useBulkExport } from './useBulkExport';
+
+const ModalContent = styled.div`
+  min-height: 400px;
+`;
+
+const Description = styled.div`
+  ${({ theme }) => `
+    margin-bottom: ${theme.sizeUnit * 4}px;
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const WarningMessage = styled.div`
+  ${({ theme }) => `
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    background-color: ${theme.colorWarningBg};
+    border: 1px solid ${theme.colorWarningBorder};
+    border-radius: ${theme.sizeUnit}px;
+    color: ${theme.colorWarningText};
+    font-size: ${theme.sizeUnit * 3.5}px;
+    margin-bottom: ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+const SummaryMessage = styled.div<{ success?: boolean }>`
+  ${({ theme, success }) => `
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    background-color: ${success ? theme.colorSuccessBg : theme.colorErrorBg};
+    border: 1px solid ${success ? theme.colorSuccessBorder : 
theme.colorErrorBorder};
+    border-radius: ${theme.sizeUnit}px;
+    color: ${success ? theme.colorSuccessText : theme.colorErrorText};
+    font-size: ${theme.sizeUnit * 3.5}px;
+    margin-bottom: ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+export interface ExportDashboardDataModalProps {
+  show: boolean;
+  onHide: () => void;
+  dashboardTitle: string;
+  charts: ChartInfo[];
+  slices: Record<number, any>; // Slice entities from Redux
+}
+
+export const ExportDashboardDataModal = ({
+  show,
+  onHide,
+  dashboardTitle,
+  charts,
+  slices,
+}: ExportDashboardDataModalProps) => {
+  const { addSuccessToast, addDangerToast, addWarningToast } = useToasts();
+  const [selectedChartIds, setSelectedChartIds] = useState<Set<number>>(
+    new Set(),
+  );
+  const [exportComplete, setExportComplete] = useState(false);
+
+  const { isExporting, progress, exportCharts, generateExcelFile, reset } =
+    useBulkExport({
+      onComplete: async results => {
+        const successCount = results.filter(r => r.status === 
'success').length;
+        const failureCount = results.length - successCount;
+
+        if (successCount > 0) {
+          try {
+            const filename = await generateExcelFile(results, dashboardTitle);
+            addSuccessToast(
+              failureCount > 0
+                ? t(
+                    '%s of %s charts exported successfully to %s',
+                    successCount,
+                    results.length,
+                    filename,
+                  )
+                : t(
+                    'All %s charts exported successfully to %s',
+                    successCount,
+                    filename,
+                  ),
+            );
+            setExportComplete(true);
+          } catch (error: any) {
+            addDangerToast(
+              t(
+                'Failed to generate Excel file: %s',
+                error.message || 'Unknown error',
+              ),
+            );
+          }
+        }
+      },
+      onError: error => {
+        addDangerToast(error);
+      },
+    });
+
+  // Initialize with all charts selected
+  useEffect(() => {
+    if (show && charts.length > 0) {
+      const allIds = new Set(charts.map(c => c.id));
+      setSelectedChartIds(allIds);
+      setExportComplete(false);
+      reset();
+    }
+  }, [show, charts, reset]);

Review Comment:
   This initialization effect depends on the `charts` prop, so it will reset 
selection and call reset() any time the `charts` array reference changes while 
the modal is open. Since the parent currently constructs `charts` inline, this 
can cause unexpected resets. Consider initializing only when `show` becomes 
true (and/or ensuring `charts` is memoized).
   ```suggestion
     }, [show, reset, charts.length]);
   ```



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/ChartSelector.tsx:
##########
@@ -0,0 +1,181 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { styled, t } from '@apache-superset/core';
+import { Checkbox } from '@superset-ui/core/components';
+
+const Container = styled.div`
+  ${({ theme }) => `
+    padding: ${theme.sizeUnit * 4}px 0;
+  `}
+`;
+
+const HeaderRow = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: ${theme.sizeUnit * 3}px;
+    padding: 0 ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+const SelectionInfo = styled.div`
+  ${({ theme }) => `
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const Actions = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    gap: ${theme.sizeUnit * 2}px;
+  `}
+`;
+
+const ActionLink = styled.a`
+  ${({ theme }) => `
+    color: ${theme.colorPrimary};
+    cursor: pointer;
+    font-size: ${theme.sizeUnit * 3.5}px;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  `}
+`;
+
+const ChartList = styled.div`
+  ${({ theme }) => `
+    max-height: 400px;
+    overflow-y: auto;
+    border: 1px solid ${theme.colorBorder};
+    border-radius: ${theme.sizeUnit}px;
+  `}
+`;
+
+const ChartItem = styled.div<{ disabled?: boolean }>`
+  ${({ theme, disabled }) => `
+    display: flex;
+    align-items: center;
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    border-bottom: 1px solid ${theme.colorBorderSecondary};
+    cursor: ${disabled ? 'not-allowed' : 'pointer'};
+    opacity: ${disabled ? 0.5 : 1};
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &:hover {
+      background-color: ${disabled ? 'transparent' : theme.colorBgTextHover};
+    }
+  `}
+`;
+
+const ChartName = styled.span`
+  ${({ theme }) => `
+    margin-left: ${theme.sizeUnit * 2}px;
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const ChartMeta = styled.span`
+  ${({ theme }) => `
+    margin-left: auto;
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3}px;
+  `}
+`;
+
+export interface ChartInfo {
+  id: number;
+  name: string;
+  vizType?: string;
+}
+
+export interface ChartSelectorProps {
+  charts: ChartInfo[];
+  selectedChartIds: Set<number>;
+  onSelectionChange: (selectedIds: Set<number>) => void;
+  disabled?: boolean;
+}
+
+export const ChartSelector = ({
+  charts,
+  selectedChartIds,
+  onSelectionChange,
+  disabled = false,
+}: ChartSelectorProps) => {
+  const handleSelectAll = () => {
+    const allIds = new Set(charts.map(c => c.id));
+    onSelectionChange(allIds);
+  };
+
+  const handleDeselectAll = () => {
+    onSelectionChange(new Set());
+  };
+
+  const handleToggleChart = (chartId: number) => {
+    if (disabled) return;
+
+    const newSelection = new Set(selectedChartIds);
+    if (newSelection.has(chartId)) {
+      newSelection.delete(chartId);
+    } else {
+      newSelection.add(chartId);
+    }
+    onSelectionChange(newSelection);
+  };
+
+  return (
+    <Container>
+      <HeaderRow>
+        <SelectionInfo>
+          {t('%s of %s charts selected', selectedChartIds.size, charts.length)}
+        </SelectionInfo>
+        <Actions>
+          <ActionLink onClick={handleSelectAll}>{t('Select all')}</ActionLink>
+          <span>|</span>
+          <ActionLink onClick={handleDeselectAll}>
+            {t('Deselect all')}
+          </ActionLink>

Review Comment:
   The “Select all / Deselect all” controls are rendered as <a> elements 
without an href. Anchors without href are not consistently keyboard-focusable 
and are not ideal for button-like actions. Use a <button> (or add 
role="button", tabIndex, and key handlers) to ensure accessibility.



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/index.tsx:
##########
@@ -0,0 +1,286 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useEffect, useMemo } from 'react';
+import { styled, t } from '@apache-superset/core';
+import { Modal, Button } from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { ChartSelector, ChartInfo } from './ChartSelector';
+import { ExportProgress } from './ExportProgress';
+import { useBulkExport } from './useBulkExport';
+
+const ModalContent = styled.div`
+  min-height: 400px;
+`;
+
+const Description = styled.div`
+  ${({ theme }) => `
+    margin-bottom: ${theme.sizeUnit * 4}px;
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const WarningMessage = styled.div`
+  ${({ theme }) => `
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    background-color: ${theme.colorWarningBg};
+    border: 1px solid ${theme.colorWarningBorder};
+    border-radius: ${theme.sizeUnit}px;
+    color: ${theme.colorWarningText};
+    font-size: ${theme.sizeUnit * 3.5}px;
+    margin-bottom: ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+const SummaryMessage = styled.div<{ success?: boolean }>`
+  ${({ theme, success }) => `
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    background-color: ${success ? theme.colorSuccessBg : theme.colorErrorBg};
+    border: 1px solid ${success ? theme.colorSuccessBorder : 
theme.colorErrorBorder};
+    border-radius: ${theme.sizeUnit}px;
+    color: ${success ? theme.colorSuccessText : theme.colorErrorText};
+    font-size: ${theme.sizeUnit * 3.5}px;
+    margin-bottom: ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+export interface ExportDashboardDataModalProps {
+  show: boolean;
+  onHide: () => void;
+  dashboardTitle: string;
+  charts: ChartInfo[];
+  slices: Record<number, any>; // Slice entities from Redux
+}
+
+export const ExportDashboardDataModal = ({
+  show,
+  onHide,
+  dashboardTitle,
+  charts,
+  slices,
+}: ExportDashboardDataModalProps) => {
+  const { addSuccessToast, addDangerToast, addWarningToast } = useToasts();
+  const [selectedChartIds, setSelectedChartIds] = useState<Set<number>>(
+    new Set(),
+  );
+  const [exportComplete, setExportComplete] = useState(false);
+
+  const { isExporting, progress, exportCharts, generateExcelFile, reset } =
+    useBulkExport({
+      onComplete: async results => {
+        const successCount = results.filter(r => r.status === 
'success').length;
+        const failureCount = results.length - successCount;
+
+        if (successCount > 0) {
+          try {
+            const filename = await generateExcelFile(results, dashboardTitle);
+            addSuccessToast(
+              failureCount > 0
+                ? t(
+                    '%s of %s charts exported successfully to %s',
+                    successCount,
+                    results.length,
+                    filename,
+                  )
+                : t(
+                    'All %s charts exported successfully to %s',
+                    successCount,
+                    filename,
+                  ),
+            );
+            setExportComplete(true);
+          } catch (error: any) {
+            addDangerToast(
+              t(
+                'Failed to generate Excel file: %s',
+                error.message || 'Unknown error',
+              ),
+            );
+          }
+        }
+      },
+      onError: error => {
+        addDangerToast(error);
+      },
+    });
+
+  // Initialize with all charts selected
+  useEffect(() => {
+    if (show && charts.length > 0) {
+      const allIds = new Set(charts.map(c => c.id));
+      setSelectedChartIds(allIds);
+      setExportComplete(false);
+      reset();
+    }
+  }, [show, charts, reset]);
+
+  const handleExport = async () => {
+    if (selectedChartIds.size === 0) {
+      addWarningToast(t('Please select at least one chart to export'));
+      return;
+    }
+
+    if (selectedChartIds.size > 20) {
+      addWarningToast(
+        t(
+          'Exporting more than 20 charts may take a while and could impact 
browser performance',
+        ),
+      );
+    }
+
+    // Prepare chart data for export
+    const chartsToExport = Array.from(selectedChartIds)
+      .map(id => {
+        const slice = slices[id];
+        if (!slice) return null;
+
+        return {
+          id,
+          name: slice.slice_name || `Chart ${id}`,
+          formData: slice.form_data || {},
+        };
+      })
+      .filter(Boolean) as { id: number; name: string; formData: any }[];
+

Review Comment:
   `chartsToExport` is cast to a type with `formData: any`. Per AGENTS.md, 
please avoid `any` in new frontend code—prefer `QueryFormData`/`JsonObject` (or 
`unknown` with runtime narrowing) for `formData` so payload building remains 
type-safe.



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/ChartSelector.tsx:
##########
@@ -0,0 +1,181 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { styled, t } from '@apache-superset/core';
+import { Checkbox } from '@superset-ui/core/components';
+
+const Container = styled.div`
+  ${({ theme }) => `
+    padding: ${theme.sizeUnit * 4}px 0;
+  `}
+`;
+
+const HeaderRow = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: ${theme.sizeUnit * 3}px;
+    padding: 0 ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+const SelectionInfo = styled.div`
+  ${({ theme }) => `
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const Actions = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    gap: ${theme.sizeUnit * 2}px;
+  `}
+`;
+
+const ActionLink = styled.a`
+  ${({ theme }) => `
+    color: ${theme.colorPrimary};
+    cursor: pointer;
+    font-size: ${theme.sizeUnit * 3.5}px;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  `}
+`;
+
+const ChartList = styled.div`
+  ${({ theme }) => `
+    max-height: 400px;
+    overflow-y: auto;
+    border: 1px solid ${theme.colorBorder};
+    border-radius: ${theme.sizeUnit}px;
+  `}
+`;
+
+const ChartItem = styled.div<{ disabled?: boolean }>`
+  ${({ theme, disabled }) => `
+    display: flex;
+    align-items: center;
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    border-bottom: 1px solid ${theme.colorBorderSecondary};
+    cursor: ${disabled ? 'not-allowed' : 'pointer'};
+    opacity: ${disabled ? 0.5 : 1};
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &:hover {
+      background-color: ${disabled ? 'transparent' : theme.colorBgTextHover};
+    }
+  `}
+`;
+
+const ChartName = styled.span`
+  ${({ theme }) => `
+    margin-left: ${theme.sizeUnit * 2}px;
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const ChartMeta = styled.span`
+  ${({ theme }) => `
+    margin-left: auto;
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3}px;
+  `}
+`;
+
+export interface ChartInfo {
+  id: number;
+  name: string;
+  vizType?: string;
+}
+
+export interface ChartSelectorProps {
+  charts: ChartInfo[];
+  selectedChartIds: Set<number>;
+  onSelectionChange: (selectedIds: Set<number>) => void;
+  disabled?: boolean;
+}
+
+export const ChartSelector = ({
+  charts,
+  selectedChartIds,
+  onSelectionChange,
+  disabled = false,
+}: ChartSelectorProps) => {
+  const handleSelectAll = () => {
+    const allIds = new Set(charts.map(c => c.id));
+    onSelectionChange(allIds);
+  };
+
+  const handleDeselectAll = () => {
+    onSelectionChange(new Set());
+  };
+
+  const handleToggleChart = (chartId: number) => {
+    if (disabled) return;
+
+    const newSelection = new Set(selectedChartIds);
+    if (newSelection.has(chartId)) {
+      newSelection.delete(chartId);
+    } else {
+      newSelection.add(chartId);
+    }
+    onSelectionChange(newSelection);
+  };
+
+  return (
+    <Container>
+      <HeaderRow>
+        <SelectionInfo>
+          {t('%s of %s charts selected', selectedChartIds.size, charts.length)}
+        </SelectionInfo>
+        <Actions>
+          <ActionLink onClick={handleSelectAll}>{t('Select all')}</ActionLink>
+          <span>|</span>
+          <ActionLink onClick={handleDeselectAll}>
+            {t('Deselect all')}
+          </ActionLink>
+        </Actions>
+      </HeaderRow>
+
+      <ChartList>
+        {charts.map(chart => (
+          <ChartItem
+            key={chart.id}
+            onClick={() => handleToggleChart(chart.id)}
+            disabled={disabled}
+          >
+            <Checkbox
+              checked={selectedChartIds.has(chart.id)}
+              onChange={() => handleToggleChart(chart.id)}

Review Comment:
   Clicking the checkbox will likely toggle selection twice because the row is 
clickable (ChartItem onClick) and the Checkbox onChange also toggles. This can 
result in no net change when the checkbox itself is clicked. Consider removing 
one handler, or stopping propagation from the checkbox interaction so only one 
toggle occurs.
   ```suggestion
                 onChange={event => {
                   event.stopPropagation();
                   handleToggleChart(chart.id);
                 }}
   ```



##########
superset-frontend/src/dashboard/components/Header/index.tsx:
##########
@@ -942,6 +952,18 @@ const Header = (): ReactElement => {
         onConfirmNavigation={handleConfirmNavigation}
         handleSave={handleSaveAndCloseModal}
       />
+
+      <ExportDashboardDataModal
+        show={showExportModal}
+        onHide={() => setShowExportModal(false)}
+        dashboardTitle={dashboardTitle ?? ''}
+        charts={chartIds.map(id => ({
+          id,
+          name: sliceEntities[id]?.slice_name || `Chart ${id}`,
+          vizType: sliceEntities[id]?.viz_type,
+        }))}

Review Comment:
   `charts` is passed as `chartIds.map(...)`, which creates a new array every 
Header render. Since the modal initialization effect depends on the `charts` 
prop reference, this can trigger re-initialization/resets while the modal is 
open. Memoize the mapped `charts` array (useMemo) or adjust the modal effect 
dependencies to prevent unintended resets.



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/useBulkExport.ts:
##########
@@ -0,0 +1,254 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useCallback } from 'react';
+import { SupersetClient } from '@superset-ui/core';
+import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
+import { ensureAppRoot } from 'src/utils/pathUtils';
+
+export interface ChartExportData {
+  chartId: number;
+  chartName: string;
+  data: any[];
+  columns: string[];
+  status: 'pending' | 'exporting' | 'success' | 'error';
+  error?: string;
+}
+
+export interface BulkExportProgress {
+  current: number;
+  total: number;
+  charts: ChartExportData[];
+}
+
+export interface UseBulkExportProps {
+  onComplete?: (results: ChartExportData[]) => void;
+  onError?: (error: string) => void;
+}
+
+export const useBulkExport = ({
+  onComplete,
+  onError,
+}: UseBulkExportProps = {}) => {
+  const [isExporting, setIsExporting] = useState(false);
+  const [progress, setProgress] = useState<BulkExportProgress>({
+    current: 0,
+    total: 0,
+    charts: [],
+  });
+
+  const sanitizeSheetName = (name: string): string => {
+    // Excel sheet names: max 31 chars, no special characters
+    const sanitized = name
+      .replace(/[:\\/?*[\]]/g, '_') // Replace invalid chars
+      .substring(0, 31); // Truncate to 31 chars
+
+    return sanitized || 'Sheet';
+  };
+
+  const exportSingleChart = async (
+    chartId: number,
+    chartName: string,
+    formData: any,
+  ): Promise<ChartExportData> => {
+    const chartData: ChartExportData = {
+      chartId,
+      chartName,
+      data: [],
+      columns: [],
+      status: 'exporting',
+    };
+
+    try {
+      // Build query payload
+      const payload = await buildV1ChartDataPayload({
+        formData: {
+          ...formData,
+          slice_id: chartId,
+        },
+        force: false,
+        resultFormat: 'json',
+        resultType: 'full',
+        setDataMask: () => {},
+        ownState: {},
+      });
+
+      // Call chart data API
+      const response = await SupersetClient.post({
+        endpoint: ensureAppRoot('/api/v1/chart/data'),
+        jsonPayload: payload,
+      });
+
+      // Extract data from response
+      if (response.json?.result?.[0]) {
+        const [queryResult] = response.json.result;
+        chartData.data = queryResult.data || [];
+        chartData.columns = queryResult.colnames || [];
+        chartData.status = 'success';
+      } else {
+        throw new Error('No data returned from API');
+      }
+    } catch (error: any) {
+      chartData.status = 'error';
+      chartData.error = error.message || 'Failed to export chart';
+    }
+
+    return chartData;
+  };
+
+  const exportCharts = useCallback(
+    async (charts: { id: number; name: string; formData: any }[]) => {
+      setIsExporting(true);

Review Comment:
   `exportCharts` is wrapped in useCallback but closes over `exportSingleChart` 
(declared in the component scope) without listing it in the dependency array. 
With react-hooks/exhaustive-deps enabled, this will be flagged and can create 
stale-closure issues if `exportSingleChart` starts using props/state. Consider 
moving `exportSingleChart` to module scope or memoizing it and adding it to 
deps.



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/ChartSelector.tsx:
##########
@@ -0,0 +1,181 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { styled, t } from '@apache-superset/core';
+import { Checkbox } from '@superset-ui/core/components';
+
+const Container = styled.div`
+  ${({ theme }) => `
+    padding: ${theme.sizeUnit * 4}px 0;
+  `}
+`;
+
+const HeaderRow = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: ${theme.sizeUnit * 3}px;
+    padding: 0 ${theme.sizeUnit * 4}px;
+  `}
+`;
+
+const SelectionInfo = styled.div`
+  ${({ theme }) => `
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const Actions = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    gap: ${theme.sizeUnit * 2}px;
+  `}
+`;
+
+const ActionLink = styled.a`
+  ${({ theme }) => `
+    color: ${theme.colorPrimary};
+    cursor: pointer;
+    font-size: ${theme.sizeUnit * 3.5}px;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  `}
+`;
+
+const ChartList = styled.div`
+  ${({ theme }) => `
+    max-height: 400px;
+    overflow-y: auto;
+    border: 1px solid ${theme.colorBorder};
+    border-radius: ${theme.sizeUnit}px;
+  `}
+`;
+
+const ChartItem = styled.div<{ disabled?: boolean }>`
+  ${({ theme, disabled }) => `
+    display: flex;
+    align-items: center;
+    padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 4}px;
+    border-bottom: 1px solid ${theme.colorBorderSecondary};
+    cursor: ${disabled ? 'not-allowed' : 'pointer'};
+    opacity: ${disabled ? 0.5 : 1};
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &:hover {
+      background-color: ${disabled ? 'transparent' : theme.colorBgTextHover};
+    }
+  `}
+`;
+
+const ChartName = styled.span`
+  ${({ theme }) => `
+    margin-left: ${theme.sizeUnit * 2}px;
+    font-size: ${theme.sizeUnit * 3.5}px;
+  `}
+`;
+
+const ChartMeta = styled.span`
+  ${({ theme }) => `
+    margin-left: auto;
+    color: ${theme.colorTextSecondary};
+    font-size: ${theme.sizeUnit * 3}px;
+  `}
+`;
+
+export interface ChartInfo {
+  id: number;
+  name: string;
+  vizType?: string;
+}
+
+export interface ChartSelectorProps {
+  charts: ChartInfo[];
+  selectedChartIds: Set<number>;
+  onSelectionChange: (selectedIds: Set<number>) => void;
+  disabled?: boolean;
+}
+
+export const ChartSelector = ({
+  charts,
+  selectedChartIds,
+  onSelectionChange,
+  disabled = false,
+}: ChartSelectorProps) => {
+  const handleSelectAll = () => {
+    const allIds = new Set(charts.map(c => c.id));
+    onSelectionChange(allIds);
+  };
+
+  const handleDeselectAll = () => {
+    onSelectionChange(new Set());
+  };
+
+  const handleToggleChart = (chartId: number) => {
+    if (disabled) return;
+
+    const newSelection = new Set(selectedChartIds);
+    if (newSelection.has(chartId)) {
+      newSelection.delete(chartId);
+    } else {
+      newSelection.add(chartId);
+    }
+    onSelectionChange(newSelection);
+  };
+
+  return (
+    <Container>
+      <HeaderRow>
+        <SelectionInfo>
+          {t('%s of %s charts selected', selectedChartIds.size, charts.length)}
+        </SelectionInfo>
+        <Actions>
+          <ActionLink onClick={handleSelectAll}>{t('Select all')}</ActionLink>
+          <span>|</span>
+          <ActionLink onClick={handleDeselectAll}>
+            {t('Deselect all')}
+          </ActionLink>
+        </Actions>
+      </HeaderRow>
+
+      <ChartList>
+        {charts.map(chart => (
+          <ChartItem
+            key={chart.id}
+            onClick={() => handleToggleChart(chart.id)}
+            disabled={disabled}
+          >

Review Comment:
   Chart rows are clickable but rendered as a plain <div>, which is not 
keyboard accessible by default. Consider using a semantic button/list item 
pattern, or add role/tabIndex and handle Enter/Space key events so users can 
toggle selection via keyboard.



##########
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx:
##########
@@ -175,7 +176,17 @@ export const useDownloadMenuItems = (
         },
       ];
 
+  const dataExportMenuItem: MenuItem = {
+    key: 'export-dashboard-data',
+    label: t('Export Dashboard Data'),
+    onClick: props.onExportDashboardData,
+  };
+
   const exportMenuItems: MenuItem[] = [
+    ...(props.onExportDashboardData ? [dataExportMenuItem] : []),
+    ...(props.onExportDashboardData
+      ? [{ type: 'divider' as const, key: 'data-export-divider' }]
+      : []),

Review Comment:
   This file already has Jest tests (DownloadMenuItems.test.tsx), but the new 
`onExportDashboardData` behavior isn’t covered. Consider adding a test that 
passes `onExportDashboardData` and asserts the new “Export Dashboard Data” item 
(and divider) are rendered and invoke the callback when clicked.



##########
superset-frontend/src/dashboard/components/Header/index.tsx:
##########
@@ -194,6 +195,9 @@ const Header = (): ReactElement => {
   const dispatch = useDispatch();
   const [didNotifyMaxUndoHistoryToast, setDidNotifyMaxUndoHistoryToast] =
     useState<boolean>(false);
+  const sliceEntities = useSelector(
+    (state: RootState) => state.sliceEntities?.slices || {},
+  );

Review Comment:
   Using `state.sliceEntities?.slices || {}` in a selector returns a new object 
literal when sliceEntities is undefined, which can trigger unnecessary 
rerenders. Prefer `??` with a module-level EMPTY object (or handle defaulting 
outside useSelector) to keep selector results referentially stable.



##########
superset-frontend/src/dashboard/components/ExportDashboardDataModal/useBulkExport.ts:
##########
@@ -0,0 +1,254 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useCallback } from 'react';
+import { SupersetClient } from '@superset-ui/core';
+import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
+import { ensureAppRoot } from 'src/utils/pathUtils';
+
+export interface ChartExportData {
+  chartId: number;
+  chartName: string;
+  data: any[];
+  columns: string[];
+  status: 'pending' | 'exporting' | 'success' | 'error';
+  error?: string;
+}
+
+export interface BulkExportProgress {
+  current: number;
+  total: number;
+  charts: ChartExportData[];
+}
+
+export interface UseBulkExportProps {
+  onComplete?: (results: ChartExportData[]) => void;
+  onError?: (error: string) => void;
+}
+
+export const useBulkExport = ({
+  onComplete,
+  onError,
+}: UseBulkExportProps = {}) => {
+  const [isExporting, setIsExporting] = useState(false);
+  const [progress, setProgress] = useState<BulkExportProgress>({
+    current: 0,
+    total: 0,
+    charts: [],
+  });
+
+  const sanitizeSheetName = (name: string): string => {
+    // Excel sheet names: max 31 chars, no special characters
+    const sanitized = name
+      .replace(/[:\\/?*[\]]/g, '_') // Replace invalid chars
+      .substring(0, 31); // Truncate to 31 chars
+
+    return sanitized || 'Sheet';
+  };
+
+  const exportSingleChart = async (
+    chartId: number,
+    chartName: string,
+    formData: any,
+  ): Promise<ChartExportData> => {

Review Comment:
   `exportSingleChart` accepts `formData: any`. Per AGENTS.md, please use an 
existing form-data type (e.g. `QueryFormData`) or `unknown` + narrowing to 
avoid losing type safety when building the chart payload.



-- 
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