Repository: metabase/metabase
Pull request #58651: WRK-411 Migrate actions to execute API
Author: login: stasgavrylov
Labels: name: no-backport
description: Do not backport this PR to any branch
Created: 2025-05-29T16:03:27Z | Merged: 2025-05-30T10:23:21Z



Diff:
diff --git a/enterprise/frontend/src/metabase-enterprise/api/table-data-edit.ts b/enterprise/frontend/src/metabase-enterprise/api/table-data-edit.ts
index e9de408f4125..44623a2a79b9 100644
--- a/enterprise/frontend/src/metabase-enterprise/api/table-data-edit.ts
+++ b/enterprise/frontend/src/metabase-enterprise/api/table-data-edit.ts
@@ -1,6 +1,8 @@
 import type {
   TableDeleteRowsRequest,
   TableDeleteRowsResponse,
+  TableExecuteActionRequest,
+  TableExecuteActionResponse,
   TableInsertRowsRequest,
   TableInsertRowsResponse,
   TableUndoRedoRequest,
@@ -79,6 +81,27 @@ export const tableDataEditApi = EnterpriseApi.injectEndpoints({
         actions: Array<WritebackAction | TableAction>;
       }) => response?.actions,
     }),
+    executeAction: builder.mutation<
+      TableExecuteActionResponse,
+      TableExecuteActionRequest
+    >({
+      query: ({ actionId, parameters }) => ({
+        method: "POST",
+        url: `/api/ee/data-editing/action/v2/execute`,
+        body: {
+          input: parameters,
+          // Here we pass a dummy table id, because the BE doesn't allow scope to be optional,
+          // but it's not actually used for this case.
+          scope: { "table-id": 1 },
+          action_id: actionId,
+        },
+      }),
+      transformResponse: (response: {
+        outputs: [{ "rows-affected": number }];
+      }) => {
+        return response?.outputs?.[0];
+      },
+    }),
   }),
 });
 
@@ -89,4 +112,5 @@ export const {
   useTableUndoMutation,
   useTableRedoMutation,
   useGetActionsQuery,
+  useExecuteActionMutation,
 } = tableDataEditApi;
diff --git a/enterprise/frontend/src/metabase-enterprise/data_editing/tables/types.ts b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/types.ts
index a77e0ab4a2a5..5fdeefbebf3f 100644
--- a/enterprise/frontend/src/metabase-enterprise/data_editing/tables/types.ts
+++ b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/types.ts
@@ -1,8 +1,10 @@
 import type {
   ConcreteTableId,
   DatasetColumn,
+  ParametersForActionExecution,
   RowValue,
   TableId,
+  WritebackActionId,
 } from "metabase-types/api";
 
 export type RowCellsWithPkValue = Record<DatasetColumn["name"], RowValue>;
@@ -74,3 +76,12 @@ export type TableOperation = [string, Record<string, RowValue>];
 export type TableUndoRedoResponse = {
   outputs?: ExecuteOutput<"created" | "updated" | "deleted">[];
 };
+
+export type TableExecuteActionRequest = {
+  actionId: WritebackActionId;
+  parameters: ParametersForActionExecution;
+};
+
+export type TableExecuteActionResponse = {
+  "rows-affected": number;
+};
diff --git a/enterprise/frontend/src/metabase-enterprise/table-actions/execution/TableActionExecuteModalContent.tsx b/enterprise/frontend/src/metabase-enterprise/table-actions/execution/TableActionExecuteModalContent.tsx
index 62b8ecd3f136..a39001fd8984 100644
--- a/enterprise/frontend/src/metabase-enterprise/table-actions/execution/TableActionExecuteModalContent.tsx
+++ b/enterprise/frontend/src/metabase-enterprise/table-actions/execution/TableActionExecuteModalContent.tsx
@@ -1,16 +1,22 @@
 import { merge } from "icepick";
 import { useCallback, useMemo } from "react";
 
-import { executeAction } from "metabase/actions/actions";
 import ActionParametersInputForm from "metabase/actions/containers/ActionParametersInputForm";
+import {
+  getActionErrorMessage,
+  getActionExecutionMessage,
+} from "metabase/actions/utils";
 import { skipToken, useGetActionQuery } from "metabase/api";
 import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper";
 import ModalContent from "metabase/components/ModalContent";
 import { useDispatch } from "metabase/lib/redux";
 import { checkNotNull } from "metabase/lib/types";
+import { addUndo } from "metabase/redux/undo";
 import type { TableActionsExecuteFormVizOverride } from "metabase/visualizations/types/table-actions";
+import { useExecuteActionMutation } from "metabase-enterprise/api";
 import type {
   ParametersForActionExecution,
+  WritebackAction,
   WritebackActionId,
 } from "metabase-types/api";
 
@@ -49,21 +55,33 @@ export const TableActionExecuteModalContent = ({
     }
   }, [action, actionOverrides]);
 
+  const [executeAction] = useExecuteActionMutation();
+
   const handleSubmit = useCallback(
-    (parameters: ParametersForActionExecution) => {
+    async (parameters: ParametersForActionExecution) => {
       const fullParameters = {
         ...initialValues,
         ...parameters,
       };
 
-      return dispatch(
-        executeAction({
-          action: checkNotNull(action),
-          parameters: fullParameters,
-        }),
-      );
+      const result = await executeAction({
+        actionId: actionId as number,
+        parameters: fullParameters,
+      });
+      if (!result.error) {
+        const message = getActionExecutionMessage(
+          actionWithOverrides as WritebackAction,
+          result.data,
+        );
+        dispatch(addUndo({ message, toastColor: "success" }));
+        return { success: true, message };
+      }
+
+      const message = getActionErrorMessage(result.error);
+      dispatch(addUndo({ message, toastColor: "error" }));
+      return { success: false, error: result.error, message };
     },
-    [initialValues, dispatch, action],
+    [executeAction, initialValues, actionId, actionWithOverrides, dispatch],
   );
 
   const handleSubmitSuccess = useCallback(() => {


Reviews:
user: login: crisptrutski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T16:37:36Z
user: login: stasgavrylov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T08:45:29Z
user: login: stasgavrylov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T08:48:02Z

Review comments:
diff_hunk: @@ -49,21 +55,33 @@ export const TableActionExecuteModalContent = ({
     }
   }, [action, actionOverrides]);
 
+  const [executeAction] = useExecuteActionMutation();
+
   const handleSubmit = useCallback(
-    (parameters: ParametersForActionExecution) => {
+    async (parameters: ParametersForActionExecution) => {
       const fullParameters = {
         ...initialValues,
         ...parameters,
       };
 
-      return dispatch(
-        executeAction({
-          action: checkNotNull(action),
-          parameters: fullParameters,
-        }),
-      );
+      const result = await executeAction({
+        actionId: actionId as number,
+        parameters: fullParameters,
path: enterprise/frontend/src/metabase-enterprise/table-actions/execution/TableActionExecuteModalContent.tsx
user: login: crisptrutski
body: What do you think about renaming this key from `parameters` to `input` so its consistent with the API?
created_at: 2025-05-29T16:30:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,6 +81,27 @@ export const tableDataEditApi = EnterpriseApi.injectEndpoints({
         actions: Array<WritebackAction | TableAction>;
       }) => response?.actions,
     }),
+    executeAction: builder.mutation<
+      TableExecuteActionResponse,
+      TableExecuteActionRequest
+    >({
+      query: ({ actionId, parameters }) => ({
+        method: "POST",
+        url: `/api/ee/data-editing/action/v2/execute`,
+        body: {
+          input: parameters,
+          // Here we pass a dummy table id, because the BE doesn't allow scope to be optional,
+          // but it's not actually used for this case.
+          scope: { "table-id": 1 },
path: enterprise/frontend/src/metabase-enterprise/api/table-data-edit.ts
user: login: crisptrutski
body: I think that scope should be added as an argument for `executeAction`. Even though we don't (currently) need scope for legacy "model actions", they are used for v2 actions (e.g. the actions grouped by table in the new picker). 

As far as I can tell this function is always getting called either from the Table page or from a Dashcard for an Editable, is that right? In that case we should pass the corresponding `table-id` or `dashcard-id` parameter as the scope - exactly like the scopes we send for data edits.
created_at: 2025-05-29T16:37:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,21 +55,33 @@ export const TableActionExecuteModalContent = ({
     }
   }, [action, actionOverrides]);
 
+  const [executeAction] = useExecuteActionMutation();
+
   const handleSubmit = useCallback(
-    (parameters: ParametersForActionExecution) => {
+    async (parameters: ParametersForActionExecution) => {
       const fullParameters = {
         ...initialValues,
         ...parameters,
       };
 
-      return dispatch(
-        executeAction({
-          action: checkNotNull(action),
-          parameters: fullParameters,
-        }),
-      );
+      const result = await executeAction({
+        actionId: actionId as number,
+        parameters: fullParameters,
path: enterprise/frontend/src/metabase-enterprise/table-actions/execution/TableActionExecuteModalContent.tsx
user: login: stasgavrylov
body: The thing is, it's called "action parameters" all across the app, that's why I've decided to leave the name as is. I think it gives more context, on what we're actually executing – action with parameters.
created_at: 2025-05-30T08:45:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,6 +81,27 @@ export const tableDataEditApi = EnterpriseApi.injectEndpoints({
         actions: Array<WritebackAction | TableAction>;
       }) => response?.actions,
     }),
+    executeAction: builder.mutation<
+      TableExecuteActionResponse,
+      TableExecuteActionRequest
+    >({
+      query: ({ actionId, parameters }) => ({
+        method: "POST",
+        url: `/api/ee/data-editing/action/v2/execute`,
+        body: {
+          input: parameters,
+          // Here we pass a dummy table id, because the BE doesn't allow scope to be optional,
+          // but it's not actually used for this case.
+          scope: { "table-id": 1 },
path: enterprise/frontend/src/metabase-enterprise/api/table-data-edit.ts
user: login: stasgavrylov
body: Makes sense. I didn't consider that I'll use the same function for the dashcard, and currently the component calling `executeAction` doesn't have the table id. I'll merge it as is in this PR and will extract the scope outside when I migrate dashcard actions.
created_at: 2025-05-30T08:48:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: trunk-io[bot]
created_at: 2025-05-29T17:20:33Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://raster.shields.io/badge/14807_tests-dd2ef193-purple)](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment) &ensp; [![Static Badge](https://raster.shields.io/badge/30-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/1-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment&runConclusion=QUARANTINED)


| Failed Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>Write Actions on Dashboards (mysql) adding and executing actions hidden fields adds a query action and runs it</code> | The error occurred when trying to read a property 'forEach' of an undefined value. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment&runConclusion=FAILURE&runId=0a08da1f-324e-5be5-81ce-87a640a7d357) |
| <code>Write Actions on Dashboards (mysql) adding and executing actions hidden fields adds an implicit action and runs it</code> | The error occurred when trying to read a property 'forEach' of an undefined value. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment&runConclusion=FAILURE&runId=0f81924f-65fd-59bf-94eb-7e89fd6672e3) |
| <code>Write Actions on Dashboards (postgres) adding and executing actions adds an implicit delete action to a dashboard and runs it</code> | The error occurred when trying to read a property 'forEach' of an undefined value. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment&runConclusion=FAILURE&runId=1770eb7c-aa55-5cc9-af07-82df037bd13d) |
| <code>Write Actions on Dashboards (postgres) adding and executing actions adds an implicit update action to a dashboard and runs it</code> | The error occurred when trying to read a property 'forEach' of an undefined value. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment&runConclusion=FAILURE&runId=2436ef6c-d54a-5d9c-8747-dc6bf6c56e48) |

[... and 26 more](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment&runConclusion=FAILURE)


| Flaky Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>scenarios > dashboard > visualizer > cartesian should work with more than two datasets (VIZ-693)</code> | cy.wait() timed out waiting for the first request to the route saveDashboard-saveDashboardCards, which was not triggered. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment&runConclusion=FAILURE&runId=aed9f290-2515-538c-b710-de3d7f405542) |
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/58651?repo=metabase/metabase&commitHash=dd2ef1933fdf857eb0f10a8d85da6695431b69c6&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-30T09:00:51Z
body: 
  ## e2e tests failed on `dd2ef1933fdf857eb0f10a8d85da6695431b69c6-1`

  [e2e test run](https://github.com/metabase/metabase/actions/runs/15342973889?pr=58651)

  File | Test Name
  ---- | ---------
  `actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > adding and executing actions > adds a custom query action to a dashboard and runs it
`actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > adding and executing actions > adds an implicit create action to a dashboard and runs it
`actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > adding and executing actions > adds an implicit update action to a dashboard and runs it
`actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > adding and executing actions > adds an implicit delete action to a dashboard and runs it
`actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > adding and executing actions > hidden fields > adds an implicit action and runs it
`actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > adding and executing actions > hidden fields > adds a query action and runs it
`actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > Actions Data Types > can update various data types via implicit actions
`actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > Actions Data Types > can insert various data types via implicit actions
`actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > Actions Data Types > does not show json, enum, or binary columns for implicit actions
`actions-on-dashboards.cy.spec.js` | Write Actions on Dashboards (mysql) > Actions Data Types > properly loads and updates date and time fields for implicit update actions
and more... | and more...

author_association: CONTRIBUTOR