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


##########
superset-frontend/src/explore/components/SaveModal.tsx:
##########
@@ -277,11 +315,14 @@ class SaveModal extends Component<SaveModalProps, 
SaveModalState> {
 
       // Go to new dashboard url
       if (gotodash && dashboard) {
+        let url = dashboard.url;

Review Comment:
   Tab redirection only works when action is 'saveas', but it should also work 
for 'overwrite' action when a user overwrites a chart and wants to go to the 
dashboard. Currently, when overwriting a chart and going to the dashboard, the 
selectedTab will not navigate to the correct tab even if one was selected.
   ```suggestion
           let url = dashboard.url;
           // Always append tab hash if a tab is selected, for both 'saveas' 
and 'overwrite'
   ```



##########
superset-frontend/src/explore/components/SaveModal.tsx:
##########
@@ -134,6 +142,7 @@ class SaveModal extends Component<SaveModalProps, 
SaveModalState> {
           this.setState({
             dashboard: { label: result.dashboard_title, value: result.id },
           });
+          await this.loadTabs(dashboardId);

Review Comment:
   Tab selector should only be visible when action is 'saveas' according to 
line 622, but it's also loading tabs for the 'overwrite' action at line 145. 
This creates unnecessary API calls when tabs won't be used. Consider only 
loading tabs when `this.state.action === 'saveas'`.
   ```suggestion
             if (this.state.action === 'saveas') {
               await this.loadTabs(dashboardId);
             }
   ```



##########
superset-frontend/src/explore/components/SaveModal.tsx:
##########
@@ -292,6 +333,115 @@ class SaveModal extends Component<SaveModalProps, 
SaveModalState> {
     }
   }
 
+  /* Adds a new chart to a new row on the specified dashboard tab.
+   * @param {number} dashboardId - ID of the dashboard.
+   * @param {number} chartId - ID of the chart to add.
+   * @param {string} tabId - ID of the dashboard tab where the chart is added.
+   * @param {string | undefined}  sliceName - Chart name

Review Comment:
   The comment says "Adds a new chart to a new row" but the function doesn't 
always create a new row - it reuses existing rows with available space. Update 
the comment to reflect the actual behavior:
   ```typescript
   /* Adds a chart to the specified dashboard tab. If an existing row has 
space, the chart is added there; otherwise, a new row is created.
    * @param {number} dashboardId - ID of the dashboard.
    * @param {number} chartId - ID of the chart to add.
    * @param {string} tabId - ID of the dashboard tab where the chart is added.
    * @param {string | undefined} sliceName - Chart name
    */
   ```
   ```suggestion
     /* Adds a chart to the specified dashboard tab. If an existing row has 
space, the chart is added there; otherwise, a new row is created.
      * @param {number} dashboardId - ID of the dashboard.
      * @param {number} chartId - ID of the chart to add.
      * @param {string} tabId - ID of the dashboard tab where the chart is 
added.
      * @param {string | undefined} sliceName - Chart name
   ```



##########
superset-frontend/src/explore/components/SaveModal.test.jsx:
##########
@@ -429,3 +441,169 @@ test('dispatches removeChartState when saving and going 
to dashboard', async ()
   // Clean up
   removeChartStateSpy.mockRestore();
 });
+
+test('disables tab selector when no dashboard selected', () => {
+  const { getByRole, getByTestId } = setup();
+  fireEvent.click(getByRole('radio', { name: 'Save as...' }));
+  const tabSelector = getByTestId('mock-tree-select');
+  expect(tabSelector).toBeInTheDocument();
+  expect(tabSelector).toBeDisabled();
+});
+
+test('renders tab selector when saving as', async () => {
+  const { getByRole, getByTestId } = setup();
+  fireEvent.click(getByRole('radio', { name: 'Save as...' }));
+  const selection = getByTestId('mock-async-select');
+  fireEvent.change(selection, { target: { value: '1' } });
+  const tabSelector = getByTestId('mock-tree-select');
+  expect(tabSelector).toBeInTheDocument();
+  expect(tabSelector).toBeDisabled();
+});
+
+test('onDashboardChange triggers tabs load for existing dashboard', async () 
=> {
+  const dashboardId = mockEvent.value;
+
+  fetchMock.get(`glob:*/api/v1/dashboard/${dashboardId}/tabs`, {
+    json: {
+      result: {
+        tab_tree: [
+          { value: 'tab1', title: 'Main Tab' },
+          { value: 'tab2', title: 'Tab' },
+        ],
+      },
+    },
+  });
+  const component = new PureSaveModal(defaultProps);
+  const loadTabsMock = jest
+    .fn()
+    .mockResolvedValue([{ value: 'tab1', title: 'Main Tab' }]);
+  component.loadTabs = loadTabsMock;
+  await component.onDashboardChange({
+    value: dashboardId,
+    label: 'Test Dashboard',
+  });
+  expect(loadTabsMock).toHaveBeenCalledWith(dashboardId);
+});
+
+test('onTabChange correctly updates selectedTab via forceUpdate', () => {
+  const component = new PureSaveModal(defaultProps);
+
+  component.state = {
+    ...component.state,
+    tabsData: [
+      {
+        value: 'tab1',
+        title: 'Main Tab',
+        key: 'tab1',
+        children: [
+          {
+            value: 'tab2',
+            title: 'Analytics Tab',
+            key: 'tab2',
+          },
+        ],
+      },
+    ],
+  };
+
+  component.setState = function (stateUpdate) {
+    if (typeof stateUpdate === 'function') {
+      this.state = { ...this.state, ...stateUpdate(this.state) };
+    } else {
+      this.state = { ...this.state, ...stateUpdate };
+    }
+  }.bind(component);
+
+  component.onTabChange('tab2');
+
+  expect(component.state.selectedTab).toEqual({
+    value: 'tab2',
+    label: 'Analytics Tab',
+  });
+});
+
+test('chart placement logic finds row with available space', () => {
+  const GRID_COLUMN_COUNT = 12; // From the actual implementation
+  const chartWidth = 4; // From the actual implementation
+
+  // Test case 1: Row has space (8 + 4 = 12 <= 12)
+  const positionJson1 = {
+    tab1: {
+      type: 'TABS',
+      id: 'tab1',
+      children: ['row1'],
+    },
+    row1: {
+      type: 'ROW',
+      id: 'row1',
+      children: ['CHART-1'],
+      meta: {},
+    },
+    'CHART-1': {
+      type: 'CHART',
+      id: 'CHART-1',
+      meta: { width: 8 },
+    },
+  };
+
+  // Test case 2: Row is full (12 + 4 = 16 > 12)
+  const positionJson2 = {
+    ...positionJson1,
+    'CHART-1': {
+      ...positionJson1['CHART-1'],
+      meta: { width: 12 },
+    },
+  };
+
+  // Test case 3: Multiple charts in row
+  const positionJson3 = {
+    tab1: {
+      type: 'TABS',
+      id: 'tab1',
+      children: ['row1'],
+    },
+    row1: {
+      type: 'ROW',
+      id: 'row1',
+      children: ['CHART-1', 'CHART-2'],
+      meta: {},
+    },
+    'CHART-1': {
+      type: 'CHART',
+      id: 'CHART-1',
+      meta: { width: 6 },
+    },
+    'CHART-2': {
+      type: 'CHART',
+      id: 'CHART-2',
+      meta: { width: 4 },
+    },
+  };
+
+  const findRowWithSpace = (positionJson, tabChildren) => {
+    for (const childKey of tabChildren) {
+      const child = positionJson[childKey];
+      if (child?.type === 'ROW') {
+        const rowChildren = child.children || [];
+        const totalWidth = rowChildren.reduce((sum, key) => {
+          const component = positionJson[key];
+          return sum + (component?.meta?.width || 0);
+        }, 0);
+
+        if (totalWidth + chartWidth <= GRID_COLUMN_COUNT) {
+          return childKey;
+        }
+      }
+    }
+    return null;
+  };
+
+  // Test case 1: Should find row with space
+  expect(findRowWithSpace(positionJson1, ['row1'])).toBe('row1');
+
+  // Test case 2: Should not find row (full)
+  expect(findRowWithSpace(positionJson2, ['row1'])).toBeNull();
+
+  // Test case 3: Should find row (6 + 4 + 4 = 14 > 12, so no space)

Review Comment:
   The comment is misleading. It says "6 + 4 + 4 = 14" but the test data only 
has two charts (width 6 and 4). The calculation should be: existing charts 6 + 
4 = 10, plus new chart width 4 = 14 total, which exceeds GRID_COLUMN_COUNT 
(12). Update the comment to:
   ```javascript
   // Test case 3: Should not find row (6 + 4 = 10, adding 4 = 14 > 12)
   ```
   ```suggestion
     // Test case 3: Should not find row (6 + 4 = 10, adding 4 = 14 > 12)
   ```



##########
superset-frontend/src/explore/components/SaveModal.tsx:
##########
@@ -292,6 +333,115 @@ class SaveModal extends Component<SaveModalProps, 
SaveModalState> {
     }
   }
 
+  /* Adds a new chart to a new row on the specified dashboard tab.
+   * @param {number} dashboardId - ID of the dashboard.
+   * @param {number} chartId - ID of the chart to add.
+   * @param {string} tabId - ID of the dashboard tab where the chart is added.
+   * @param {string | undefined}  sliceName - Chart name
+   * */
+  addChartToDashboardTab = async (
+    dashboardId: number,
+    chartId: number,
+    tabId: string,
+    sliceName: string | undefined,
+  ) => {
+    try {
+      const dashboardResponse = await SupersetClient.get({
+        endpoint: `/api/v1/dashboard/${dashboardId}`,
+      });
+
+      const dashboard = dashboardResponse.json.result;
+
+      let positionJson = dashboard.position_json;
+      if (typeof positionJson === 'string') {
+        positionJson = JSON.parse(positionJson);
+      }
+      positionJson = positionJson || {};
+
+      const chartKey = `CHART-${chartId}`;
+      const chartWidth = 4;
+
+      // Find a row in the tab with available space
+      const tabChildren = positionJson[tabId]?.children || [];
+      let targetRowKey: string | null = null;
+
+      for (const childKey of tabChildren) {
+        const child = positionJson[childKey];
+        if (child?.type === 'ROW') {
+          const rowChildren = child.children || [];
+          const totalWidth = rowChildren.reduce((sum: number, key: string) => {
+            const component = positionJson[key];
+            return sum + (component?.meta?.width || 0);
+          }, 0);
+
+          if (totalWidth + chartWidth <= GRID_COLUMN_COUNT) {
+            targetRowKey = childKey;
+            break;
+          }
+        }
+      }
+
+      const updatedPositionJson = { ...positionJson };
+
+      // Create a new row if no existing row has space
+      if (!targetRowKey) {
+        targetRowKey = `ROW-${nanoid()}`;
+        updatedPositionJson[targetRowKey] = {
+          type: 'ROW',
+          id: targetRowKey,
+          children: [],
+          parents: ['ROOT_ID', 'GRID_ID', tabId],
+          meta: {
+            background: 'BACKGROUND_TRANSPARENT',
+          },
+        };
+
+        if (positionJson[tabId]) {
+          updatedPositionJson[tabId] = {
+            ...positionJson[tabId],
+            children: [...(positionJson[tabId].children || []), targetRowKey],
+          };
+        } else {
+          throw new Error(`Tab ${tabId} not found in positionJson`);
+        }
+      }
+
+      updatedPositionJson[chartKey] = {
+        type: 'CHART',
+        id: chartKey,
+        children: [],
+        parents: ['ROOT_ID', 'GRID_ID', tabId, targetRowKey],
+        meta: {
+          width: chartWidth,
+          height: 50,
+          chartId,
+          sliceName: sliceName ?? `Chart ${chartId}`,
+        },
+      };
+
+      // Add chart to the target row
+      updatedPositionJson[targetRowKey] = {
+        ...updatedPositionJson[targetRowKey],
+        children: [
+          ...(updatedPositionJson[targetRowKey].children || []),
+          chartKey,
+        ],
+      };
+
+      const response = await SupersetClient.put({
+        endpoint: `/api/v1/dashboard/${dashboardId}`,
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          position_json: JSON.stringify(updatedPositionJson),
+        }),
+      });
+
+      return response;
+    } catch (error) {
+      throw new Error('Error adding chart to dashboard tab:', error);
+    }
+  };

Review Comment:
   Missing test coverage for the new `addChartToDashboardTab` method. While 
there's a test for the placement logic, there's no integration test that 
verifies the full method execution, including API calls, error handling, and 
the dashboard update. Consider adding a test that mocks the API responses and 
verifies the method works end-to-end.



##########
superset-frontend/src/explore/components/SaveModal.tsx:
##########
@@ -292,6 +333,115 @@ class SaveModal extends Component<SaveModalProps, 
SaveModalState> {
     }
   }
 
+  /* Adds a new chart to a new row on the specified dashboard tab.
+   * @param {number} dashboardId - ID of the dashboard.
+   * @param {number} chartId - ID of the chart to add.
+   * @param {string} tabId - ID of the dashboard tab where the chart is added.
+   * @param {string | undefined}  sliceName - Chart name
+   * */
+  addChartToDashboardTab = async (
+    dashboardId: number,
+    chartId: number,
+    tabId: string,
+    sliceName: string | undefined,
+  ) => {
+    try {
+      const dashboardResponse = await SupersetClient.get({
+        endpoint: `/api/v1/dashboard/${dashboardId}`,
+      });
+
+      const dashboard = dashboardResponse.json.result;
+
+      let positionJson = dashboard.position_json;
+      if (typeof positionJson === 'string') {
+        positionJson = JSON.parse(positionJson);
+      }
+      positionJson = positionJson || {};
+
+      const chartKey = `CHART-${chartId}`;
+      const chartWidth = 4;
+
+      // Find a row in the tab with available space
+      const tabChildren = positionJson[tabId]?.children || [];
+      let targetRowKey: string | null = null;
+
+      for (const childKey of tabChildren) {
+        const child = positionJson[childKey];
+        if (child?.type === 'ROW') {
+          const rowChildren = child.children || [];
+          const totalWidth = rowChildren.reduce((sum: number, key: string) => {
+            const component = positionJson[key];
+            return sum + (component?.meta?.width || 0);
+          }, 0);
+
+          if (totalWidth + chartWidth <= GRID_COLUMN_COUNT) {
+            targetRowKey = childKey;
+            break;
+          }
+        }
+      }
+
+      const updatedPositionJson = { ...positionJson };
+
+      // Create a new row if no existing row has space
+      if (!targetRowKey) {
+        targetRowKey = `ROW-${nanoid()}`;
+        updatedPositionJson[targetRowKey] = {
+          type: 'ROW',
+          id: targetRowKey,
+          children: [],
+          parents: ['ROOT_ID', 'GRID_ID', tabId],
+          meta: {
+            background: 'BACKGROUND_TRANSPARENT',
+          },
+        };
+
+        if (positionJson[tabId]) {
+          updatedPositionJson[tabId] = {
+            ...positionJson[tabId],
+            children: [...(positionJson[tabId].children || []), targetRowKey],
+          };
+        } else {
+          throw new Error(`Tab ${tabId} not found in positionJson`);
+        }
+      }
+
+      updatedPositionJson[chartKey] = {
+        type: 'CHART',
+        id: chartKey,
+        children: [],
+        parents: ['ROOT_ID', 'GRID_ID', tabId, targetRowKey],
+        meta: {
+          width: chartWidth,
+          height: 50,
+          chartId,
+          sliceName: sliceName ?? `Chart ${chartId}`,
+        },
+      };
+
+      // Add chart to the target row
+      updatedPositionJson[targetRowKey] = {
+        ...updatedPositionJson[targetRowKey],
+        children: [
+          ...(updatedPositionJson[targetRowKey].children || []),
+          chartKey,
+        ],
+      };
+
+      const response = await SupersetClient.put({
+        endpoint: `/api/v1/dashboard/${dashboardId}`,
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          position_json: JSON.stringify(updatedPositionJson),
+        }),
+      });
+
+      return response;
+    } catch (error) {
+      throw new Error('Error adding chart to dashboard tab:', error);

Review Comment:
   Incorrect Error constructor syntax. The Error constructor takes a single 
message string as its first argument, not a message followed by an error 
object. This should be:
   ```typescript
   throw new Error(`Error adding chart to dashboard tab: ${error}`);
   ```
   or wrap the original error:
   ```typescript
   throw error;
   ```
   ```suggestion
         throw new Error(`Error adding chart to dashboard tab: ${error 
instanceof Error ? error.message : String(error)}`);
   ```



##########
superset-frontend/src/explore/components/SaveModal.tsx:
##########
@@ -292,6 +333,115 @@ class SaveModal extends Component<SaveModalProps, 
SaveModalState> {
     }
   }
 
+  /* Adds a new chart to a new row on the specified dashboard tab.
+   * @param {number} dashboardId - ID of the dashboard.
+   * @param {number} chartId - ID of the chart to add.
+   * @param {string} tabId - ID of the dashboard tab where the chart is added.
+   * @param {string | undefined}  sliceName - Chart name
+   * */
+  addChartToDashboardTab = async (
+    dashboardId: number,
+    chartId: number,
+    tabId: string,
+    sliceName: string | undefined,
+  ) => {
+    try {
+      const dashboardResponse = await SupersetClient.get({
+        endpoint: `/api/v1/dashboard/${dashboardId}`,
+      });
+
+      const dashboard = dashboardResponse.json.result;
+
+      let positionJson = dashboard.position_json;
+      if (typeof positionJson === 'string') {
+        positionJson = JSON.parse(positionJson);
+      }
+      positionJson = positionJson || {};
+
+      const chartKey = `CHART-${chartId}`;
+      const chartWidth = 4;
+
+      // Find a row in the tab with available space
+      const tabChildren = positionJson[tabId]?.children || [];
+      let targetRowKey: string | null = null;
+
+      for (const childKey of tabChildren) {
+        const child = positionJson[childKey];
+        if (child?.type === 'ROW') {
+          const rowChildren = child.children || [];
+          const totalWidth = rowChildren.reduce((sum: number, key: string) => {
+            const component = positionJson[key];
+            return sum + (component?.meta?.width || 0);
+          }, 0);
+
+          if (totalWidth + chartWidth <= GRID_COLUMN_COUNT) {
+            targetRowKey = childKey;
+            break;
+          }
+        }
+      }
+
+      const updatedPositionJson = { ...positionJson };
+
+      // Create a new row if no existing row has space
+      if (!targetRowKey) {
+        targetRowKey = `ROW-${nanoid()}`;
+        updatedPositionJson[targetRowKey] = {
+          type: 'ROW',
+          id: targetRowKey,
+          children: [],
+          parents: ['ROOT_ID', 'GRID_ID', tabId],
+          meta: {
+            background: 'BACKGROUND_TRANSPARENT',
+          },
+        };
+
+        if (positionJson[tabId]) {
+          updatedPositionJson[tabId] = {
+            ...positionJson[tabId],
+            children: [...(positionJson[tabId].children || []), targetRowKey],
+          };
+        } else {
+          throw new Error(`Tab ${tabId} not found in positionJson`);
+        }
+      }
+
+      updatedPositionJson[chartKey] = {
+        type: 'CHART',
+        id: chartKey,
+        children: [],
+        parents: ['ROOT_ID', 'GRID_ID', tabId, targetRowKey],
+        meta: {
+          width: chartWidth,
+          height: 50,

Review Comment:
   Magic numbers 4 (chart width) and 50 (chart height) should be extracted as 
named constants for better maintainability. Consider defining these at the top 
of the file or importing from a constants file:
   ```typescript
   const DEFAULT_CHART_WIDTH = 4;
   const DEFAULT_CHART_HEIGHT = 50;
   ```



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