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


##########
superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx:
##########
@@ -457,6 +457,38 @@ function FiltersConfigModal({
     }
   }, [form, erroredFilters]);
 
+  /**
+   * Helper function to find dependency validation errors in form fields.
+   * This function improves code maintainability by centralizing the logic
+   * for detecting cyclic dependency errors and extracting the problematic 
filter ID.
+   * 
+   * @param fields - Array of form field error objects from Ant Design Form
+   * @returns Object containing error status and filter ID, or null if no 
dependency errors
+   */
+  const findDependencyError = (fields: any[]) => {

Review Comment:
   [nitpick] Extract `findDependencyError` into a shared utility (e.g., 
`utils.ts`) so that it can be unit tested in isolation and reused across 
components.



##########
superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx:
##########
@@ -457,6 +457,38 @@ function FiltersConfigModal({
     }
   }, [form, erroredFilters]);
 
+  /**
+   * Helper function to find dependency validation errors in form fields.
+   * This function improves code maintainability by centralizing the logic
+   * for detecting cyclic dependency errors and extracting the problematic 
filter ID.
+   * 
+   * @param fields - Array of form field error objects from Ant Design Form
+   * @returns Object containing error status and filter ID, or null if no 
dependency errors
+   */
+  const findDependencyError = (fields: any[]) => {

Review Comment:
   Consider strongly typing the `fields` parameter (e.g., use Ant Design's 
FieldError type) and destructuring `field.name` into meaningful variables 
instead of relying on numeric indexes to improve readability and reduce 
fragility.



##########
superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx:
##########
@@ -584,3 +584,199 @@ test('modifies the name of a filter', async () => {
     ),
   );
 });
+
+test('prevents saving cyclic dependencies created indirectly', async () => {
+  // Test case: Reproduce exact scenario from GitHub issue #33908
+  // This test verifies that users cannot create A→B dependency, save it,
+  // then later add B→A dependency and save again (which would create a cycle)
+  
+  const nativeFilterState = [
+    buildNativeFilter('NATIVE_FILTER-1', 'state', []),
+    buildNativeFilter('NATIVE_FILTER-2', 'country', []),
+  ];
+  const state = {
+    ...defaultState(),
+    dashboardInfo: {
+      metadata: { native_filter_configuration: nativeFilterState },
+    },
+    dashboardLayout,
+  };
+  const onSave = jest.fn();
+  defaultRender(state, {
+    ...props,
+    createNewOnOpen: false,
+    onSave,
+  });
+
+  // STEP 1: Set up Filter 1 → depends on Filter 2 (should succeed)
+  const filterTabs = screen.getAllByRole('tab');
+  userEvent.click(filterTabs[0]); // Select first filter
+
+  // Enable the dependencies option for Filter 1
+  const dependenciesCheckbox = screen.getByRole('checkbox', { name: 
DEPENDENCIES_REGEX });
+  userEvent.click(dependenciesCheckbox);
+
+  // Set Filter 1 to depend on Filter 2
+  const dependencySelect = screen.getByRole('combobox', { name: /values 
dependent on/i });
+  userEvent.click(dependencySelect);
+  const filter2Option = screen.getByText('country');
+  userEvent.click(filter2Option);
+
+  // Save this configuration (should work as there's no cycle yet)
+  userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
+  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
+
+  // Verify the save was successful with correct dependency structure
+  expect(onSave).toHaveBeenCalledWith(
+    expect.objectContaining({
+      filterConfig: expect.objectContaining({
+        'NATIVE_FILTER-1': expect.objectContaining({
+          dependsOn: ['NATIVE_FILTER-2'], // Filter 1 now depends on Filter 2
+        }),
+        'NATIVE_FILTER-2': expect.objectContaining({
+          dependsOn: [], // Filter 2 has no dependencies
+        }),
+      }),
+    }),
+  );
+
+  // Reset mock for second save attempt
+  onSave.mockClear();
+
+  // STEP 2: Simulate reopening the modal and trying to create reverse 
dependency
+  // This reproduces the bug scenario: user reopens modal to edit Filter 2
+  defaultRender(state, {
+    ...props,
+    createNewOnOpen: false,
+    onSave,
+  });
+
+  const filterTabs2 = screen.getAllByRole('tab');
+  userEvent.click(filterTabs2[1]); // Select second filter (Filter 2)
+
+  // Try to set Filter 2 to depend on Filter 1 (creating A→B→A cycle)
+  const dependenciesCheckbox2 = screen.getByRole('checkbox', { name: 
DEPENDENCIES_REGEX });
+  userEvent.click(dependenciesCheckbox2);
+
+  const dependencySelect2 = screen.getByRole('combobox', { name: /values 
dependent on/i });
+  userEvent.click(dependencySelect2);
+  const filter1Option = screen.getByText('state');
+  userEvent.click(filter1Option);
+
+  // STEP 3: Attempt to save the cyclic configuration (should be blocked)
+  userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
+
+  // Verify that save was prevented due to cyclic dependency detection
+  await waitFor(() => {
+    expect(onSave).not.toHaveBeenCalled();
+  }, { timeout: 3000 });
+
+  // Verify that user receives appropriate error feedback
+  await waitFor(() => {
+    expect(screen.getByText(/cyclic dependency 
detected/i)).toBeInTheDocument();
+  }, { timeout: 3000 });
+
+  // Verify that focus is moved to the problematic filter for user guidance
+  await waitFor(() => {
+    const activeTab = screen.getByRole('tab', { selected: true });
+    expect(activeTab).toHaveTextContent('country'); // Should focus on Filter 2
+  }, { timeout: 3000 });

Review Comment:
   [nitpick] Consider reducing the `waitFor` timeout from 3000ms to a lower 
value to speed up CI runs, as the UI should update quickly after validation.
   ```suggestion
     }, { timeout: 1000 });
   
     // Verify that user receives appropriate error feedback
     await waitFor(() => {
       expect(screen.getByText(/cyclic dependency 
detected/i)).toBeInTheDocument();
     }, { timeout: 1000 });
   
     // Verify that focus is moved to the problematic filter for user guidance
     await waitFor(() => {
       const activeTab = screen.getByRole('tab', { selected: true });
       expect(activeTab).toHaveTextContent('country'); // Should focus on 
Filter 2
     }, { timeout: 1000 });
   ```



-- 
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: notifications-unsubscr...@superset.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org
For additional commands, e-mail: notifications-h...@superset.apache.org

Reply via email to