This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch fire-alert
in repository https://gitbox.apache.org/repos/asf/superset.git

commit dcbfbed68452e9dedb0d0b5ce9ed038750360c22
Author: Maxime Beauchemin <[email protected]>
AuthorDate: Wed Sep 10 00:02:55 2025 -0700

    feat: Add "Trigger Now" functionality for Alerts & Reports
    
    Add manual execution capability to Alerts & Reports CRUD interface with 
immediate trigger functionality.
    
    ## Backend Implementation
    - Add `/api/v1/report/{id}/execute` REST endpoint with proper RBAC
    - Create `ExecuteReportScheduleNowCommand` following existing Command 
patterns
    - Use `security_manager.raise_for_ownership()` for consistent permission 
checking
    - Reuse existing `AsyncExecuteReportScheduleCommand` via Celery for 
execution
    - Add `ReportScheduleExecuteResponseSchema` for structured API responses
    - Add `ReportScheduleCeleryNotConfiguredError` for helpful Celery setup 
guidance
    
    ## Frontend Implementation
    - Add "Trigger Now" (⚡) button to AlertReportList actions column
    - Create `useExecuteReportSchedule` hook for API integration
    - Implement per-button loading states with `executingIds` Set tracking
    - Add success/error toast notifications with clear messaging
    - Gate feature behind existing edit permissions (`allowEdit`)
    
    ## Testing & Error Handling
    - Add 5 comprehensive backend tests covering success, 404, 403, Celery 
errors, and feature disabled
    - Detect Celery backend configuration issues with helpful error messages
    - All tests passing with proper mocking and security validation
    
    ## Key Features
    - One-click manual execution from CRUD list view
    - Smart loading states preventing double-clicks
    - Professional "Trigger Now" terminology throughout
    - Proper error handling for missing Celery backend
    - Maintains all existing security and permission patterns
    
    Resolves common user need for immediate alert/report execution without 
waiting for scheduled cron jobs.
    
    🤖 Generated with [Claude Code](https://claude.ai/code)
    
    Co-Authored-By: Claude <[email protected]>
---
 superset-frontend/package-lock.json                |   5 +-
 .../src/components/Modal/ModalFormField.tsx        |   2 +-
 .../alerts/hooks/useExecuteReportSchedule.test.ts  | 121 +++++++++++++++++
 .../alerts/hooks/useExecuteReportSchedule.ts       |  86 ++++++++++++
 .../src/pages/AlertReportList/index.tsx            |  69 +++++++++-
 superset/commands/report/exceptions.py             |  12 ++
 superset/commands/report/execute_now.py            | 147 +++++++++++++++++++++
 superset/reports/api.py                            |  79 +++++++++++
 superset/reports/schemas.py                        |  12 ++
 tests/integration_tests/reports/api_tests.py       |  85 ++++++++++++
 10 files changed, 614 insertions(+), 4 deletions(-)

diff --git a/superset-frontend/package-lock.json 
b/superset-frontend/package-lock.json
index d2f9ca5b31..d4c0646fbb 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -60642,7 +60642,7 @@
     },
     "packages/superset-core": {
       "name": "@apache-superset/core",
-      "version": "0.0.1-rc2",
+      "version": "0.0.1-rc3",
       "license": "ISC",
       "devDependencies": {
         "@babel/cli": "^7.26.4",
@@ -60652,7 +60652,8 @@
         "@babel/preset-typescript": "^7.26.0",
         "@types/react": "^17.0.83",
         "install": "^0.13.0",
-        "npm": "^11.1.0"
+        "npm": "^11.1.0",
+        "typescript": "^5.0.0"
       },
       "peerDependencies": {
         "antd": "^5.24.6",
diff --git a/superset-frontend/src/components/Modal/ModalFormField.tsx 
b/superset-frontend/src/components/Modal/ModalFormField.tsx
index db2293a4e5..95f8f87dcd 100644
--- a/superset-frontend/src/components/Modal/ModalFormField.tsx
+++ b/superset-frontend/src/components/Modal/ModalFormField.tsx
@@ -48,7 +48,7 @@ const StyledFieldContainer = styled.div<{ bottomSpacing: 
boolean }>`
 
     .required {
       margin-left: ${theme.sizeUnit / 2}px;
-      color: ${theme.colorError};
+      color: ${theme.colorIcon};
     }
 
     .helper {
diff --git 
a/superset-frontend/src/features/alerts/hooks/useExecuteReportSchedule.test.ts 
b/superset-frontend/src/features/alerts/hooks/useExecuteReportSchedule.test.ts
new file mode 100644
index 0000000000..33b61e8711
--- /dev/null
+++ 
b/superset-frontend/src/features/alerts/hooks/useExecuteReportSchedule.test.ts
@@ -0,0 +1,121 @@
+/**
+ * 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 { act } from '@testing-library/react';
+import { renderHook } from '@testing-library/react-hooks';
+import fetchMock from 'fetch-mock';
+import { SupersetClient } from '@superset-ui/core';
+
+import { useExecuteReportSchedule } from './useExecuteReportSchedule';
+
+const mockExecuteResponse = {
+  execution_id: 'test-uuid-123',
+  message: 'Report schedule execution started successfully',
+};
+
+beforeAll(() => {
+  SupersetClient.configure().init();
+});
+
+afterEach(() => {
+  fetchMock.reset();
+});
+
+test('successfully executes a report', async () => {
+  const reportId = 123;
+  fetchMock.post(
+    `glob:*/api/v1/report/${reportId}/execute`,
+    mockExecuteResponse,
+  );
+
+  const { result } = renderHook(() => useExecuteReportSchedule());
+
+  expect(result.current.loading).toBe(false);
+  expect(result.current.error).toBe(null);
+
+  let executeResult: any;
+  await act(async () => {
+    executeResult = await result.current.executeReport(reportId);
+  });
+
+  expect(result.current.loading).toBe(false);
+  expect(result.current.error).toBe(null);
+  expect(executeResult).toEqual(mockExecuteResponse);
+  expect(fetchMock.calls()).toHaveLength(1);
+});
+
+test('handles execution errors', async () => {
+  const reportId = 123;
+  const errorMessage = 'Report not found';
+  fetchMock.post(`glob:*/api/v1/report/${reportId}/execute`, {
+    status: 404,
+    body: { message: errorMessage },
+  });
+
+  const { result } = renderHook(() => useExecuteReportSchedule());
+
+  await act(async () => {
+    try {
+      await result.current.executeReport(reportId);
+    } catch (error) {
+      // Expected to throw
+    }
+  });
+
+  expect(result.current.loading).toBe(false);
+  expect(result.current.error).toBe(errorMessage);
+});
+
+test('calls success callback on successful execution', async () => {
+  const reportId = 123;
+  const onSuccess = jest.fn();
+  fetchMock.post(
+    `glob:*/api/v1/report/${reportId}/execute`,
+    mockExecuteResponse,
+  );
+
+  const { result } = renderHook(() => useExecuteReportSchedule());
+
+  await act(async () => {
+    await result.current.executeReport(reportId, onSuccess);
+  });
+
+  expect(onSuccess).toHaveBeenCalledWith(mockExecuteResponse);
+});
+
+test('calls error callback on failed execution', async () => {
+  const reportId = 123;
+  const onError = jest.fn();
+  const errorMessage = 'Execution failed';
+  fetchMock.post(`glob:*/api/v1/report/${reportId}/execute`, {
+    status: 500,
+    body: { message: errorMessage },
+  });
+
+  const { result } = renderHook(() => useExecuteReportSchedule());
+
+  await act(async () => {
+    try {
+      await result.current.executeReport(reportId, undefined, onError);
+    } catch (error) {
+      // Expected to throw
+    }
+  });
+
+  expect(onError).toHaveBeenCalledWith(errorMessage);
+});
diff --git 
a/superset-frontend/src/features/alerts/hooks/useExecuteReportSchedule.ts 
b/superset-frontend/src/features/alerts/hooks/useExecuteReportSchedule.ts
new file mode 100644
index 0000000000..e980442af4
--- /dev/null
+++ b/superset-frontend/src/features/alerts/hooks/useExecuteReportSchedule.ts
@@ -0,0 +1,86 @@
+/**
+ * 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, t } from '@superset-ui/core';
+
+interface ExecuteResponse {
+  execution_id: string;
+  message: string;
+}
+
+interface UseExecuteReportScheduleState {
+  loading: boolean;
+  error: string | null;
+}
+
+export function useExecuteReportSchedule() {
+  const [state, setState] = useState<UseExecuteReportScheduleState>({
+    loading: false,
+    error: null,
+  });
+
+  const executeReport = useCallback(
+    async (
+      reportId: number,
+      onSuccess?: (response: ExecuteResponse) => void,
+      onError?: (error: string) => void,
+    ) => {
+      setState({ loading: true, error: null });
+
+      try {
+        const response = await SupersetClient.post({
+          endpoint: `/api/v1/report/${reportId}/execute`,
+        });
+
+        const result = response.json as ExecuteResponse;
+        setState({ loading: false, error: null });
+
+        if (onSuccess) {
+          onSuccess(result);
+        }
+
+        return result;
+      } catch (error) {
+        let errorMessage = t('An error occurred while triggering the report');
+
+        if (error && typeof error === 'object' && 'json' in error) {
+          const errorJson = error.json as any;
+          if (errorJson?.message) {
+            errorMessage = errorJson.message;
+          }
+        }
+
+        setState({ loading: false, error: errorMessage });
+
+        if (onError) {
+          onError(errorMessage);
+        }
+
+        throw error;
+      }
+    },
+    [],
+  );
+
+  return {
+    executeReport,
+    loading: state.loading,
+    error: state.error,
+  };
+}
diff --git a/superset-frontend/src/pages/AlertReportList/index.tsx 
b/superset-frontend/src/pages/AlertReportList/index.tsx
index c77433e1f5..ffc2a71125 100644
--- a/superset-frontend/src/pages/AlertReportList/index.tsx
+++ b/superset-frontend/src/pages/AlertReportList/index.tsx
@@ -59,6 +59,7 @@ import { isUserAdmin } from 
'src/dashboard/util/permissionUtils';
 import Owner from 'src/types/Owner';
 import AlertReportModal from 'src/features/alerts/AlertReportModal';
 import { AlertObject, AlertState } from 'src/features/alerts/types';
+import { useExecuteReportSchedule } from 
'src/features/alerts/hooks/useExecuteReportSchedule';
 import { QueryObjectColumns } from 'src/views/CRUD/types';
 import { Icons } from '@superset-ui/core/components/Icons';
 import { WIDER_DROPDOWN_WIDTH } from 'src/components/ListView/utils';
@@ -157,12 +158,16 @@ function AlertList({
     addDangerToast,
   );
 
+  // Execute hook for Fire Now functionality
+  const { executeReport } = useExecuteReportSchedule();
+
   const [alertModalOpen, setAlertModalOpen] = useState<boolean>(false);
   const [currentAlert, setCurrentAlert] = useState<Partial<AlertObject> | 
null>(
     null,
   );
   const [currentAlertDeleting, setCurrentAlertDeleting] =
     useState<AlertObject | null>(null);
+  const [executingIds, setExecutingIds] = useState<Set<number>>(new Set());
 
   // Actions
   function handleAlertEdit(alert: AlertObject | null) {
@@ -246,6 +251,51 @@ function AlertList({
     [alerts, setResourceCollection, updateResource],
   );
 
+  const handleExecuteReport = useCallback(
+    async (alert: AlertObject) => {
+      const alertId = alert.id;
+      if (!alertId || executingIds.has(alertId)) {
+        return;
+      }
+
+      // Add to executing set
+      setExecutingIds(prev => new Set(prev).add(alertId));
+
+      try {
+        await executeReport(
+          alertId,
+          response => {
+            addSuccessToast(
+              t('%(alertType)s "%(alertName)s" triggered successfully', {
+                alertType: alert.type,
+                alertName: alert.name,
+              }),
+            );
+          },
+          error => {
+            addDangerToast(
+              t('Failed to trigger %(alertType)s "%(alertName)s": %(error)s', {
+                alertType: alert.type,
+                alertName: alert.name,
+                error,
+              }),
+            );
+          },
+        );
+      } catch (error) {
+        // Error already handled by onError callback
+      } finally {
+        // Remove from executing set
+        setExecutingIds(prev => {
+          const newSet = new Set(prev);
+          newSet.delete(alertId);
+          return newSet;
+        });
+      }
+    },
+    [executeReport, executingIds, addSuccessToast, addDangerToast],
+  );
+
   const columns = useMemo(
     () => [
       {
@@ -397,6 +447,16 @@ function AlertList({
                   onClick: handleEdit,
                 }
               : null,
+            allowEdit
+              ? {
+                  label: 'trigger-now-action',
+                  tooltip: t('Trigger Now'),
+                  placement: 'bottom',
+                  icon: 'ThunderboltOutlined',
+                  loading: executingIds.has(original.id),
+                  onClick: () => handleExecuteReport(original),
+                }
+              : null,
             allowEdit && canDelete
               ? {
                   label: 'delete-action',
@@ -424,7 +484,14 @@ function AlertList({
         id: QueryObjectColumns.ChangedBy,
       },
     ],
-    [canDelete, canEdit, isReportEnabled, toggleActive],
+    [
+      canDelete,
+      canEdit,
+      isReportEnabled,
+      toggleActive,
+      executingIds,
+      handleExecuteReport,
+    ],
   );
 
   const subMenuButtons: SubMenuProps['buttons'] = [];
diff --git a/superset/commands/report/exceptions.py 
b/superset/commands/report/exceptions.py
index 8688627810..4fa575ee88 100644
--- a/superset/commands/report/exceptions.py
+++ b/superset/commands/report/exceptions.py
@@ -309,3 +309,15 @@ class ReportScheduleForbiddenError(ForbiddenError):
 
 class ReportSchedulePruneLogError(CommandException):
     message = _("An error occurred while pruning logs ")
+
+
+class ReportScheduleExecuteNowFailedError(CommandException):
+    message = _("Report Schedule execute now failed.")
+
+
+class ReportScheduleCeleryNotConfiguredError(CommandException):
+    status = 503
+    message = _(
+        "Report Schedule execution requires a Celery backend to be configured. 
"
+        "Please configure a Celery broker (Redis or RabbitMQ) and worker 
processes."
+    )
diff --git a/superset/commands/report/execute_now.py 
b/superset/commands/report/execute_now.py
new file mode 100644
index 0000000000..9491e96f48
--- /dev/null
+++ b/superset/commands/report/execute_now.py
@@ -0,0 +1,147 @@
+# 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 logging
+from typing import Optional
+from uuid import uuid4
+
+from flask import current_app
+
+from superset import security_manager
+from superset.commands.base import BaseCommand
+from superset.commands.exceptions import CommandException
+from superset.commands.report.exceptions import (
+    ReportScheduleCeleryNotConfiguredError,
+    ReportScheduleExecuteNowFailedError,
+    ReportScheduleForbiddenError,
+    ReportScheduleNotFoundError,
+)
+from superset.daos.report import ReportScheduleDAO
+from superset.exceptions import SupersetSecurityException
+from superset.reports.models import ReportSchedule
+from superset.utils.decorators import transaction
+
+logger = logging.getLogger(__name__)
+
+
+class ExecuteReportScheduleNowCommand(BaseCommand):
+    """
+    Execute a report schedule immediately (manual trigger).
+
+    This command validates permissions and triggers immediate execution
+    of a report or alert via Celery task, similar to scheduled execution
+    but without waiting for the cron schedule.
+    """
+
+    def __init__(self, model_id: int) -> None:
+        self._model_id = model_id
+        self._model: Optional[ReportSchedule] = None
+
+    @transaction()
+    def run(self) -> str:
+        """
+        Execute the command and return execution UUID for tracking.
+
+        Returns:
+            str: Execution UUID that can be used to track the execution status
+
+        Raises:
+            ReportScheduleNotFoundError: Report schedule not found
+            ReportScheduleForbiddenError: User doesn't have permission to 
execute
+            ReportScheduleExecuteNowFailedError: Execution failed to start
+        """
+        try:
+            self.validate()
+            if not self._model:
+                raise ReportScheduleExecuteNowFailedError()
+
+            # Generate execution UUID for tracking
+            execution_id = str(uuid4())
+
+            # Trigger immediate execution via Celery
+            logger.info(
+                "Manually executing report schedule %s (id: %d), execution_id: 
%s",
+                self._model.name,
+                self._model.id,
+                execution_id,
+            )
+
+            # Import the existing execute task to avoid circular imports
+            from superset.tasks.scheduler import execute
+
+            # Set async options similar to scheduler but for immediate 
execution
+            async_options = {"task_id": execution_id}
+            if self._model.working_timeout is not None and 
current_app.config.get(
+                "ALERT_REPORTS_WORKING_TIME_OUT_KILL", True
+            ):
+                async_options["time_limit"] = (
+                    self._model.working_timeout
+                    + 
current_app.config.get("ALERT_REPORTS_WORKING_TIME_OUT_LAG", 10)
+                )
+                async_options["soft_time_limit"] = (
+                    self._model.working_timeout
+                    + current_app.config.get(
+                        "ALERT_REPORTS_WORKING_SOFT_TIME_OUT_LAG", 5
+                    )
+                )
+
+            # Execute the task
+            try:
+                execute.apply_async((self._model.id,), **async_options)
+            except Exception as celery_ex:
+                # Check for common Celery configuration issues
+                error_msg = str(celery_ex).lower()
+                if any(
+                    keyword in error_msg
+                    for keyword in [
+                        "no broker",
+                        "broker connection",
+                        "kombu",
+                        "redis",
+                        "rabbitmq",
+                        "celery",
+                        "not registered",
+                        "connection refused",
+                    ]
+                ):
+                    logger.error("Celery backend not configured: %s", 
str(celery_ex))
+                    raise ReportScheduleCeleryNotConfiguredError() from 
celery_ex
+                else:
+                    logger.error("Celery task execution failed: %s", 
str(celery_ex))
+                    raise ReportScheduleExecuteNowFailedError() from celery_ex
+
+            return execution_id
+
+        except CommandException:
+            raise
+        except Exception as ex:
+            logger.exception(
+                "Unexpected error executing report schedule %d", self._model_id
+            )
+            raise ReportScheduleExecuteNowFailedError() from ex
+
+    def validate(self) -> None:
+        """Validate the report schedule exists and user has permission to 
execute it."""
+        # Validate model exists
+        self._model = ReportScheduleDAO.find_by_id(self._model_id)
+        if not self._model:
+            raise ReportScheduleNotFoundError()
+
+        # Check ownership using the same pattern as delete command
+        try:
+            security_manager.raise_for_ownership(self._model)
+        except SupersetSecurityException as ex:
+            raise ReportScheduleForbiddenError() from ex
diff --git a/superset/reports/api.py b/superset/reports/api.py
index a0ebabb202..f88368a2fb 100644
--- a/superset/reports/api.py
+++ b/superset/reports/api.py
@@ -29,13 +29,16 @@ from superset.charts.filters import ChartFilter
 from superset.commands.report.create import CreateReportScheduleCommand
 from superset.commands.report.delete import DeleteReportScheduleCommand
 from superset.commands.report.exceptions import (
+    ReportScheduleCeleryNotConfiguredError,
     ReportScheduleCreateFailedError,
     ReportScheduleDeleteFailedError,
+    ReportScheduleExecuteNowFailedError,
     ReportScheduleForbiddenError,
     ReportScheduleInvalidError,
     ReportScheduleNotFoundError,
     ReportScheduleUpdateFailedError,
 )
+from superset.commands.report.execute_now import 
ExecuteReportScheduleNowCommand
 from superset.commands.report.update import UpdateReportScheduleCommand
 from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
 from superset.dashboards.filters import DashboardAccessFilter
@@ -48,6 +51,7 @@ from superset.reports.schemas import (
     get_delete_ids_schema,
     get_slack_channels_schema,
     openapi_spec_methods_override,
+    ReportScheduleExecuteResponseSchema,
     ReportSchedulePostSchema,
     ReportSchedulePutSchema,
 )
@@ -76,6 +80,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
         RouteMethod.RELATED,
         "bulk_delete",
         "slack_channels",  # not using RouteMethod since locally defined
+        "execute",  # not using RouteMethod since locally defined
     }
     class_permission_name = "ReportSchedule"
     method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
@@ -588,3 +593,77 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
         except SupersetException as ex:
             logger.error("Error fetching slack channels %s", str(ex))
             return self.response_422(message=str(ex))
+
+    @expose("/<int:pk>/execute", methods=("POST",))
+    @protect()
+    @safe
+    @permission_name("write")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: 
f"{self.__class__.__name__}.execute",
+        log_to_statsd=False,
+    )
+    def execute(self, pk: int) -> Response:
+        """Execute a report schedule immediately.
+        ---
+        post:
+          summary: Execute a report schedule immediately
+          parameters:
+          - in: path
+            schema:
+              type: integer
+            name: pk
+            description: The report schedule pk
+          responses:
+            200:
+              description: Report schedule execution started
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      execution_id:
+                        type: string
+                        description: UUID to track the execution status
+                      message:
+                        type: string
+                        description: Success message
+            403:
+              $ref: '#/components/responses/403'
+            404:
+              $ref: '#/components/responses/404'
+            422:
+              $ref: '#/components/responses/422'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            execution_id = ExecuteReportScheduleNowCommand(pk).run()
+            response_schema = ReportScheduleExecuteResponseSchema()
+            return self.response(
+                200,
+                **response_schema.dump(
+                    {
+                        "execution_id": execution_id,
+                        "message": "Report schedule execution started 
successfully",
+                    }
+                ),
+            )
+        except ReportScheduleNotFoundError:
+            return self.response_404()
+        except ReportScheduleForbiddenError:
+            return self.response_403()
+        except ReportScheduleCeleryNotConfiguredError as ex:
+            logger.error(
+                "Celery backend not configured for report schedule execution: 
%s",
+                str(ex),
+            )
+            return self.response(503, message=str(ex))
+        except ReportScheduleExecuteNowFailedError as ex:
+            logger.error(
+                "Error executing report schedule %s: %s",
+                self.__class__.__name__,
+                str(ex),
+                exc_info=True,
+            )
+            return self.response_422(message=str(ex))
diff --git a/superset/reports/schemas.py b/superset/reports/schemas.py
index cfccc579bc..bc03fc543f 100644
--- a/superset/reports/schemas.py
+++ b/superset/reports/schemas.py
@@ -413,3 +413,15 @@ class SlackChannelSchema(Schema):
     name = fields.String()
     is_member = fields.Boolean()
     is_private = fields.Boolean()
+
+
+class ReportScheduleExecuteResponseSchema(Schema):
+    """
+    Schema for the response when executing a report schedule immediately.
+    """
+
+    class Meta:
+        unknown = EXCLUDE
+
+    execution_id = fields.String(description="UUID to track the execution 
status")
+    message = fields.String(description="Success message")
diff --git a/tests/integration_tests/reports/api_tests.py 
b/tests/integration_tests/reports/api_tests.py
index 58bde34ac7..2b33762857 100644
--- a/tests/integration_tests/reports/api_tests.py
+++ b/tests/integration_tests/reports/api_tests.py
@@ -2049,3 +2049,88 @@ class TestReportSchedulesApi(SupersetTestCase):
         )
 
         assert json.loads(report_schedule.extra_json) == extra_json
+
+    @pytest.mark.usefixtures("create_report_schedules")
+    @patch("superset.tasks.scheduler.execute.apply_async")
+    def test_execute_report_schedule(self, mock_execute):
+        """
+        ReportSchedule Api: Test execute report schedule
+        """
+        report_schedule = (
+            db.session.query(ReportSchedule)
+            .filter(ReportSchedule.name == "name1")
+            .one_or_none()
+        )
+
+        self.login(ADMIN_USERNAME)
+        uri = f"api/v1/report/{report_schedule.id}/execute"
+        rv = self.client.post(uri)
+        assert rv.status_code == 200
+        data = json.loads(rv.data.decode("utf-8"))
+        assert "execution_id" in data
+        assert "message" in data
+        assert data["message"] == "Report schedule execution started 
successfully"
+
+        # Verify the task was called
+        mock_execute.assert_called_once()
+
+    @pytest.mark.usefixtures("create_report_schedules")
+    def test_execute_report_schedule_not_found(self):
+        """
+        ReportSchedule Api: Test execute report schedule not found
+        """
+        self.login(ADMIN_USERNAME)
+        uri = "api/v1/report/9999999/execute"
+        rv = self.client.post(uri)
+        assert rv.status_code == 404
+
+    @pytest.mark.usefixtures("create_report_schedules")
+    def test_execute_report_schedule_not_owned(self):
+        """
+        ReportSchedule Api: Test execute report schedule not owned
+        """
+        report_schedule = (
+            db.session.query(ReportSchedule)
+            .filter(ReportSchedule.name == "name1")
+            .one_or_none()
+        )
+
+        self.login(GAMMA_USERNAME)
+        uri = f"api/v1/report/{report_schedule.id}/execute"
+        rv = self.client.post(uri)
+        assert rv.status_code == 403
+
+    def test_execute_report_schedule_disabled(self):
+        """
+        ReportSchedule Api: Test execute report schedule 404s when feature is 
disabled
+        """
+        self.login(ADMIN_USERNAME)
+        with patch("superset.is_feature_enabled", return_value=False):
+            uri = "api/v1/report/1/execute"
+            rv = self.client.post(uri)
+            assert rv.status_code == 404
+
+    @pytest.mark.usefixtures("create_report_schedules")
+    @patch("superset.tasks.scheduler.execute.apply_async")
+    def test_execute_report_schedule_celery_error(self, mock_execute):
+        """
+        ReportSchedule Api: Test execute report schedule with Celery backend 
error
+        """
+        # Simulate Celery backend not configured
+        mock_execute.side_effect = Exception(
+            "kombu.exceptions.ConnectionError: broker connection"
+        )
+
+        report_schedule = (
+            db.session.query(ReportSchedule)
+            .filter(ReportSchedule.name == "name1")
+            .one_or_none()
+        )
+
+        self.login(ADMIN_USERNAME)
+        uri = f"api/v1/report/{report_schedule.id}/execute"
+        rv = self.client.post(uri)
+        assert rv.status_code == 503
+        data = json.loads(rv.data.decode("utf-8"))
+        assert "Celery backend" in data["message"]
+        assert "broker" in data["message"].lower()

Reply via email to